ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JWT 실습 프로젝트 - 2 (Security 기본 설정, JWT 및 Security 설정)
    SPRING/스프링 시큐리티 -JWT 2021. 9. 17. 00:46

    SilverNine님의 JWT 강의를 듣고 정리하였습니다.

     

    Security 기본 설정

    @EnableWebSecurity // 기본적인 Web 보안 활성화
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    // @EnableGlobalMethodSecurity(prePostEnabled = true) 어노테이션은 메소드 단위로
    // @PreAuthorize 검증 어노테이션을 사용하기 위해 추가합니다.
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        private final TokenProvider tokenProvider;
        private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
        private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
    
        public SecurityConfig(
                TokenProvider tokenProvider,
                JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
                JwtAccessDeniedHandler jwtAccessDeniedHandler
        ) {
            this.tokenProvider = tokenProvider;
            this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
            this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Override
        public void configure(WebSecurity web) {
            // Spring Security 로직을 수행하지 않고 아래 요청에 접근
            web.ignoring() 
                    .antMatchers(
                            "/h2-console/**"
                            ,"/favicon.ico"
                            ,"/error"
                    );
        }
    
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
            httpSecurity
                    // token을 사용하는 방식이기 때문에 csrf를 disable합니다.
                    .csrf().disable()
                    .exceptionHandling()
                    .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                    .accessDeniedHandler(jwtAccessDeniedHandler)
    
                    // enable h2-console
                    .and()
                    .headers()
                    .frameOptions()
                    .sameOrigin()
    
                    // 세션을 사용하지 않기 때문에 STATELESS로 설정
                    .and()
                    .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    
                    .and()
                    .authorizeRequests() //  HttpServletRequest를 사용하는 요청들에 대한 접근제한을 설정
                    .antMatchers("/api/hello").permitAll() // 접근 허용
                    .antMatchers("/api/authenticate").permitAll()
                    .antMatchers("/api/signup").permitAll()
    
                    .anyRequest().authenticated()
    
                    .and()
                    .apply(new JwtSecurityConfig(tokenProvider));
        }
    }

    TokenProvider

    @Component
    public class TokenProvider implements InitializingBean {
    
        private final Logger logger = (Logger) LoggerFactory.getLogger(TokenProvider.class);
    
        private static final String AUTHORITIES_KEY = "auth";
    
        private final String secret;
        private final long tokenValidityInMilliseconds;
    
        private Key key;
    
        public TokenProvider(
                @Value("${jwt.secret}") String secret,
                @Value("${jwt.token-validity-in-seconds}") long tokenValidityInSeconds) {
            this.secret = secret;
            this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000;
        }
    
        @Override
        public void afterPropertiesSet() {
            byte[] keyBytes = Decoders.BASE64.decode(secret);
            this.key = Keys.hmacShaKeyFor(keyBytes);
        }
    
        // Authentication 객체에 포함되어 있는 권한 정보들을 담은 토큰을 생성하고 jwt.token-validity-in-seconds 값을 이용해 토큰의 만료 시간을 지정
        public String createToken(Authentication authentication) {
            String authorities = authentication.getAuthorities().stream()
                    .map(GrantedAuthority::getAuthority)
                    .collect(Collectors.joining(","));
    
            long now = (new Date()).getTime();
            Date validity = new Date(now + this.tokenValidityInMilliseconds);
    
            return Jwts.builder()
                    .setSubject(authentication.getName())
                    .claim(AUTHORITIES_KEY, authorities)
                    .signWith(key, SignatureAlgorithm.HS512)
                    .setExpiration(validity)
                    .compact();
        }
    
        // getAuthentication 메소드는 토큰에 담겨있는 권한 정보들을 이용해 Authentication 객체를 리턴합니다.
        public Authentication getAuthentication(String token) {
            Claims claims = Jwts
                    .parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
    
            Collection<? extends GrantedAuthority> authorities =
                    Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                            .map(SimpleGrantedAuthority::new)
                            .collect(Collectors.toList());
    
            User principal = new User(claims.getSubject(), "", authorities);
    
            return new UsernamePasswordAuthenticationToken(principal, token, authorities);
        }
    
        //validateToken 메소드는 토큰을 검증하는 역할을 수행합니다.
        public boolean validateToken(String token) {
            try {
                Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
                return true;
            } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
                logger.info("잘못된 JWT 서명입니다.");
            } catch (ExpiredJwtException e) {
                logger.info("만료된 JWT 토큰입니다.");
            } catch (UnsupportedJwtException e) {
                logger.info("지원되지 않는 JWT 토큰입니다.");
            } catch (IllegalArgumentException e) {
                logger.info("JWT 토큰이 잘못되었습니다.");
            }
            return false;
        }
    }

     

    TokenProvider는 jwt.secret, jwt-token-validity-in-seconds 값을 주입받는다.

    생성자에서 setting 한 이후 afterPropertiesSet()을 오버라이드하는데 의존성 주입 이후 주입받은 secret 값을 base64로 decode하여 key변수에 할당하기 위함이다.

     

    createToken 메소드 : 인증객체에 포함되어 있는 정보들을 담은 토큰을 생성하고, jwt-token-validity-in-seconds 값을 이용해 토큰의 만료시간을 지정함.

    getAuthentication 메소드 : 토큰에 담겨있는 권한 정보를 이용하여 인증객체를 리턴한다.

    validaeToken 메소드 : 토큰을 검증하는 역할을 수행한다.

     

    JWT FIlter

    public class JwtFilter extends GenericFilterBean {
    
        private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class);
    
        public static final String AUTHORIZATION_HEADER = "Authorization";
    
        private TokenProvider tokenProvider;
    
        public JwtFilter(TokenProvider tokenProvider) {
            this.tokenProvider = tokenProvider;
        }
    
    
        // doFilter는 jwt 토큰의 인증 정보를 현재 실행중인 스레드(security Context에 저장합니다.)
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
                throws IOException, ServletException {
            HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
            String jwt = resolveToken(httpServletRequest);
            String requestURI = httpServletRequest.getRequestURI();
    
            if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
                Authentication authentication = tokenProvider.getAuthentication(jwt);
                SecurityContextHolder.getContext().setAuthentication(authentication);
                logger.debug("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI);
            } else {
                logger.debug("유효한 JWT 토큰이 없습니다, uri: {}", requestURI);
            }
    
            filterChain.doFilter(servletRequest, servletResponse);
        }
    
        // resolveToken 메소드는 HttpServletRequest 객체의 Header에서 token을 꺼내는 역할을 수행합니다.
        private String resolveToken(HttpServletRequest request) {
            String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
            if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
                return bearerToken.substring(7);
            }
            return null;
        }
    
    
    
    }

    JwtFilter Bean은 TokenProvider를 주입받음.

    resolveToken 메소드 : HttpServletRequest 객체의 Header에서 token을 꺼내는 역할을 수행함.

    doFilter 메소드 : jwt 토큰의 인증 정보를 현재 실행중인 스레드(Security Context)에 저장함.

     

    JWtSecurityConfig

    // JwtSecurityConfig.java 는 SecurityConfigurerAdapter를 extends하며
    // configure메소드를 오버라이드하여 위에서 만든 JwtFilter를 Security 로직에 적용하는 역할을 수행합니다
    public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    
    
        private TokenProvider tokenProvider;
    
        public JwtSecurityConfig(TokenProvider tokenProvider) {
            this.tokenProvider = tokenProvider;
        }
    
        @Override
        public void configure(HttpSecurity http) {
            JwtFilter customFilter = new JwtFilter(tokenProvider);
            http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
        }
    
    }

    JwtFilter를 Security 로직에 적용하는 역할을 수행함.

     

    JwtAuthenticationEntryPoint

    // 유효한 자격증명을 제공하지 않고 접근하려 할때 401 UNAUTHORIZED 에러를 리턴하기 위해
    // AuthenticationEntryPoint를 구현한 JwtAuthenticationEntryPoint 클래스를 작성합니다.
    @Component
    public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    
        @Override
        public void commence(HttpServletRequest request,
                             HttpServletResponse response,
                             AuthenticationException authException) throws IOException {
            // 유효한 자격증명을 제공하지 않고 접근하려 할때 401
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
        }
    }

    - 유효하지 않은 자격증명으로 접근하려고 할때 UNAUTHORIZED 에러 리턴

     

    JwtAccessDeniedHandler

    // 필요한 권한이 존재하지 않은 경우 403 FORBIDDEN 에러를 리턴하기 위해
    // AccessDeniedHandler를 구현한 JwtAccessDeniedHandler 클래스를 작성합니다.
    @Component
    public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
            //필요한 권한이 없이 접근하려 할때 403
            response.sendError(HttpServletResponse.SC_FORBIDDEN);
        }
    
    }

    - 필요한 권한이 존재하지 않은 경우 403 Forbidden 에러를 리턴하기 위해 AccessDeniedHandler를 작성

     

     

Designed by Tistory.