ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring boot] Spring Security + JWT + Redis (2/3)
    프로젝트/Share Your Trip 2024. 2. 6. 21:36
    이번 포스팅에서는 Share Your Trip 프로젝트에 Spring Security를 적용해 볼 것이다. 또한, JWT와 함께 사용하여 Spring Security를 커스터 마이징 해볼 것이다.

    Spring Security Filter

    • Share Your Trip은 Rest API 서버로 formLogin 방식을 사용하지 않아 formLogin(AbstractHttpConfigurer::disable)을 설정했다.
    • httpBasic은 username과 userpassword를 텍스트로 전송하는 방식이다. 해당 프로젝트에서는 JWT 방식을 사용하여 httpBasic(AbstractHttpConfigurer::disable)을 설정했다.
    • stateless한 JWT 사용으로 인해 Spring Security에서 기본적으로 제공하는 Session을 사용할 이유가 없다. 그래서 sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)를 설정했다.
    • stateless JWT 사용으로 인해 Session 기반의 공격 기법인 CSRF 방어 기능을 비활성하기 위해 csrf(AbstractHttpConfigurer::disable)을 설정했다.
    • "/api/users/login", "/api/users/", "/api/users/regenerateToken"의 API 주소를 가진 컨트롤러 메소드의 경우 인증이 필요하지 않기에 열어두었다.
    • 그 외에는 인증이 필요한 메소드로 anyRequest().authenticated()로 설정하여 보안성을 강화했다.
    • JWT를 사용하기 때문에 커스텀 필터가 필요한데 AuthenticationFilter인 UsernamePasswordAuthenticationFilter를 사용하지 않기 때문에 해당 필터 앞에 JwtAuthenticationFilter 커스텀 인증 필터를 설정했다.
    • PasswordEncoder의 경우 BCryptPasswordEncoder를 사용했는데 다른 인코더에 비해 서버 리소스를 적게 먹는 다고 한다.
    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class SecurityConfig {
        private final JwtTokenProvider jwtTokenProvider;
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
            return httpSecurity
                    .csrf(AbstractHttpConfigurer::disable)
                    .formLogin(AbstractHttpConfigurer::disable)
                    .httpBasic(AbstractHttpConfigurer::disable)
                    .sessionManagement((sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                    .authorizeRequests((authorize) -> authorize
                            .requestMatchers(
                                    new AntPathRequestMatcher("/api/users/login", HttpMethod.POST.name()),
                                    new AntPathRequestMatcher("/api/users", HttpMethod.POST.name()),
                                    new AntPathRequestMatcher("/api/users/regenerateToken", HttpMethod.POST.name())
                            ).permitAll()
                            .anyRequest().authenticated()
                    )
                    .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class)
                    .build();
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }

    Custom Authentication Filter

    • UsernamePasswordAuthenticationFilter를 사용하지 않는 이유에 대해서 알아보도록 한다.
    • 기본적인 Spring Security의 경우 사용자로부터 요청을 받으면 인증을 할 때 Username and Password Authentication Mechanism을 사용하여 로그인 폼으로 보내게 된다. 이때, 해당 역할을 수행하는 필터가 UsernamePasswordAuthentication이다.
    • 하지만, Share Your Trip은 REST API 서버로 로그인 폼 방식의 인증을 사용하지 않을 것이기 때문에 UsernamePasswordAuthentication 이전에 커스텀 필터를 만들어 해당 역할을 대신할 필터가 필요하게 된다.
    • 그래서 커스텀 필터인 JwtAuthenticationFilter를 통해 인증과 인가를 관리해보려고 한다.
    • 필터로 등록하기 위해 OncePerRequestFilter를 상속받고 있는데 이 외에도 GenericFilterBean이 존재한다.
    • 같은 서블릿에서 다른 서블릿을 생성하여 호출하는 경우가 존재하는데 이때 필터 로직을 다시 한 번 수행하게 되어 GenericFilterBean를 상속받은 경우 중복되어 실행된다.
    • OncePerRequestFilter를 사용할 경우 하나의 요청에 한 번의 처리만 가능하게 보장하는 필터로 Spring Security 공식 문에서도 CustomFilter를 만들 때 해당 필터를 상속받아 사용하라고 권장하고 있다.
     

    Architecture :: Spring Security

    The Security Filters are inserted into the FilterChainProxy with the SecurityFilterChain API. Those filters can be used for a number of different purposes, like authentication, authorization, exploit protection, and more. The filters are executed in a spec

    docs.spring.io

    Spring Security Document

    @RequiredArgsConstructor
    public class JwtAuthenticationFilter extends OncePerRequestFilter {
        private final JwtTokenProvider jwtTokenProvider;
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            String token = resolveToken(request);
    
            if(token != null && jwtTokenProvider.validateToken(token)) {
                Authentication authentication = jwtTokenProvider.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
            filterChain.doFilter(request, response);
        }
    
        // Request Header에서 토큰 정보 추출
        private String resolveToken(HttpServletRequest request) {
            String bearerToken = request.getHeader("Authorization");
            if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) {
                return bearerToken.substring(7);
            }
            return null;
        }
    }

     

    • 사용자 요청이 들어오면 request Header의 Authorization에서 JWT accessToken토큰을 추출한다.
    • JWT 혹은 OAuth의 AccessToken의 경우 RFC6750에 따라 통상적으로 Authorizaion 헤더에 "Bearer {accessToken}" 형식으로 넘겨주곤 한다.
    • 만약, 토큰이 존재하지 않으면 다음 필터로 넘기고 토큰이 존재하고 유효하다면 JwtTokenProvider에게 토큰을 넘겨 Authentication 객체를 생성하고 SecurityContext에 넣고 있는 것을 알 수 있다.
    • JwtTokenProvider는 AuthenticationManager가 사용하는 AuthenticationProvider와 같은 역할로 커스텀 필터를 사용하고 있으므로  AuthenticationProvider 또한 JWT에 맞게 클래스를 생생했다.
    @Component
    public class JwtTokenProvider {
        private final CustomUserDetailService customUserDetailService;
        private final Key key;
        @Value("${jwt.access-expiration-time}")
        private long accessTokenExpiresIn;
        @Value("${jwt.refresh-expiration-time}")
        private long refreshTokenExpiresIn;
        @Autowired
        public JwtTokenProvider(@Value("${jwt.secret}") String secretKey, CustomUserDetailService customUserDetailService) {
            this.customUserDetailService = customUserDetailService;
            byte[] keyBytes = Decoders.BASE64.decode(secretKey);
            this.key = Keys.hmacShaKeyFor(keyBytes);
        }
    
        public JwtToken generateToken(Authentication authentication) {
            String authorities = authentication.getAuthorities().stream()
                    .map(GrantedAuthority::getAuthority)
                    .collect(Collectors.joining(","));
    
            long now = (new Date()).getTime();
    
            String accessToken = Jwts.builder()
                    .setSubject(authentication.getName())
                    .claim("role", authorities)
                    .setExpiration(new Date(now + accessTokenExpiresIn))
                    .signWith(key, SignatureAlgorithm.HS256)
                    .compact();
    
            String refreshToken = Jwts.builder()
                    .setExpiration(new Date(now + refreshTokenExpiresIn))
                    .signWith(key, SignatureAlgorithm.HS256)
                    .compact();
    
            return JwtToken.builder()
                    .grantType("Bearer")
                    .accessToken(accessToken)
                    .refreshToken(refreshToken)
                    .build();
        }
    
        public Authentication getAuthentication(String token) {
            Claims claims = parseClaims(token);
    
            if(claims.get("role") == null) {
                throw new RestApiException(CommonResponseCode.INVALID_PARAMETER);
            }
    
            Collection<? extends GrantedAuthority> authorities =
                    Arrays.stream(claims.get("role").toString().split(","))
                            .map(SimpleGrantedAuthority::new)
                            .collect(Collectors.toList());
    
            UserDetails principal = new User(claims.getSubject(), "", authorities);
            return new UsernamePasswordAuthenticationToken(principal, "", authorities);
        }
    
        public boolean validateToken(String token) throws JwtException {
            parseClaims(token);
            return true;
        }
    
        private Claims parseClaims(String token) {
            return Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        }
    }

    Token Validation

    • 생성자부터 살펴보면 customUserDetailService를 주입하고 있는데 추후에 설명하도록 하겠다. 이후, 사전에 Base64로 인코딩한 32Bytes 길이의 secretKey를 decode하고 비밀키를 생성하기 위해 hmacShaKeyFor()를 호출하고 있다.
    • 이후, JwtAuthenticationFilter에서 가장 먼저 실행되는 JwtTokenProvider의 validateToken을 보면 parseClaims()를 호출하고 발생하는 에러를 던지고 있다.
    • parseClaims()는 Jwt parser 객체를 생성하고 생성한 비밀키를 셋팅한다. 그리고 Jws로 AccessToken을 넘겨주어 파싱 과정을 거쳐 payload에 있는 값들을 가져오게 된다.
    • 이 과정에서 토큰이 유효하지 않다면 에러를 발생시키고 각 에러를 잡아 처리하는 과정을 볼 수 있다.

    Authentication

    • JwtAuthenticationFilter에서 사용자가 보낸 accessToken을 바탕으로 Authentication 인증 객체를 생성하는 과정에 대해서 보겠다.
    • 우선, parseClaims()를 통해 accessToken을 검증하고 payload를 가져온다. 그리고 claims의 role 존재 유무를 체크하여 정상적인 토큰인지를 판단하게 된다.
    • UserDetails의 구현체인 User에 id에 해당되는 claim의 subject와 authoities를 넘겨줘서 객체를 생성한다.
    • 이후, UserDetails와 authorities를 이용하여 Authentication 객체를 생성하고 반환한다.
    • JwtAuthenticationFilter가 Authentication을 받아서 SecurityContextHolder의 Context에 저장하고 다음 필터 과정을 수행하게 된다. 

    Token 생성

    • Security Filter Chain에서 "/api/user/login"은 인증 필터를 거치지 않게 설정을 해두었다. 해당 컨트롤러는 userService의 login()를 호출하게 된다.
    • 사용자로부터 id와 pw를 받아서 UsernamePasswordAuthenticationToken을 생성하게 된다. 이때, 인증 여부를 나타태는 authenticated의 값은 false이다.
    • 해당 토큰 값을 AuthenticationManager에게 넘겨주면  AuthenticationProvider가 authentication 작업을 수행할 것이다.
    • AbstractUserDetailsAuthenticationProvider의 retrieveUser() 메소드에서 loadUserByUsername()를 통해서 검증할 UserDetails 값을 가져오기 때문에 기존의 방식이 아닌 Share Your Trip의 DB랑 연동을 시켜야한다.
    @Override
    public UserDto.UserInfoResponseDTO login(final UserDto.LoginRequestDTO requestDTO) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(requestDTO.getUserId(), requestDTO.getUserPassword());
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
    
        User user = userMapper.findById(requestDTO.getUserId())
                .orElseThrow(() -> new RestApiException(CustomResponseCode.USER_NOT_FOUND));
        JwtToken jwtToken = jwtTokenProvider.generateToken(authentication);
    
        return UserDto.UserInfoResponseDTO.builder()
                .userId(user.getUserId())
                .userName(user.getUserName())
                .joinDate(user.getJoinDate())
                .profile(user.getProfile())
                .role(user.getRole())
                .email(user.getEmail())
                .accessToken(jwtToken.getAccessToken())
                .refreshToken(jwtToken.getRefreshToken())
                .build();
    }
    • UserDetailsService 인터페이스를 구현하는 CustomUserDetailService 클래스를 생성하여 loadUserByUsername()를 override 했다.
    • userMapper를 이용하여 Share Your Trip의 DB에서 사용자를 조회하고 이를 CustomUserDetails 객체로 다시 만들어서 반환하고 있는데 해당 객체는 UserDetails를 상속한 객체이다.
    • user 클래스는 Share Your Trip에서 사용하는 user 객체이며 Spring Security에서 사용하는 UserDetails 객체로 만들어주기 위해서 상속받고 있다.
    @Service
    @RequiredArgsConstructor
    public class CustomUserDetailService implements UserDetailsService {
        private final UserMapper userMapper;
        @Override
        public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
            User user = userMapper.findById(userId)
                    .orElseThrow(() -> new RestApiException(CommonResponseCode.UNAUTHORIZED_REQUEST));
            return CustomUserDetails.builder()
                    .user(user)
                    .build();
        }
    }
    public class CustomUserDetails implements UserDetails {
        private final User user;
    
        @Builder
        public CustomUserDetails(User user) {
            this.user = user;
        }
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            Collection<GrantedAuthority> collection = new ArrayList<>();
            collection.add(new GrantedAuthority() {
                @Override
                public String getAuthority() {
                    return user.getRole().value();
                }
            });
            return collection;
        }
    • 위 과정들을 통해 인증을 완료하면 유효한 Authentication이 생성이되고 이를 JwtTokenProvider의 generateToken()를 통해 accessToken과 refreshToken을 생성한다.
    • 이를 사용자 응답 DTO에 맞게 변환을 해주고 controller로 넘겨 토큰 생성 과정을 마무리하게 된다.

    회원가입

    • "/api/users" POST 요청의 경우도 유저를 생성하는 API로 spring security에서 인증을 거치지 않도록 예외 처리해두었다.
    • Body에 JSON 형식으로 파라미터를 채워 유저 생성 테스트를 해본 결과 성공적으로 생성된 것을 알 수 있다.

     로그인

    • 해당 파라미터를 그대로 사용하여 로그인 API인 "/api/users/login"을 호출했다.
    • 그 결과 올바르게 값들이 넘어오는 것을 확인할 수 있다.

    인증이 필요한 API 호출

    • Authorization 헤더에 AccessToken을 삽입하여 인증이 필요한 API를 호출한 결과가 올바르게 응답하는 것을 알 수 있다.

    • Authorization 헤더를 제거하고 인증이 필요한 API를 호출한 결과가 403에러가 응답하는 것을 알 수 있다. Filter 에러 처리의 경우 추후에 진행하려고 한다.

    Authentication에서 값 추출

    • Controller에서 요청한 클라이언트의 id값이 필요한 경우가 있다. 그 경우 컨트롤러의 파라미터로 Authentication을 설정하여 인증 정보를 가져올 수 있다.
    @GetMapping("/{userid}")
    public ResponseEntity<Object> SelectUser(@PathVariable("userid") @UserId final String userId, Authentication authentication) {
        System.out.println(authentication.getName());
        UserDto.UserInfoResponseDTO responseDTO = service.findById(userId);
        return SuccessResponse.createSuccess(SuccessCode.READ_USER_SUCCESS, responseDTO);
    }

    인가 처리

    • API를 호출할 때, 요청한 사용자의 권한에 맞게 API 인가 처리가 이뤄져야 했다. 또한, 게시글을 삭제할 경우 요청한 사용자의 게시글이 아닌 경우는 처리가 되면 안됐기 때문에 인가 처리가 필요했다.
    • Spring Security에서는 두 가지 방법을 제공하고 있었다. Secured 방식과 prePost 방식이 존재하였고 이에 대해서 알아보고 적용해보도록 하겠다.
    • 해당 방식들을 적용하기 위해, Security Filter Chain에 @EnableMethodSecurity 어노테이션에 securedEnabled를 true 옵션을 주었다. prePost의 경우 default로 true 값을 가지고 있어서 따로 값을 주지 않았다.
    @Configuration
    @EnableWebSecurity
    @EnableMethodSecurity(securedEnabled = true)
    @RequiredArgsConstructor
    public class SecurityConfig {
        private final JwtTokenProvider jwtTokenProvider;
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {

    @Secured()

    • Secured 방식의 경우 컨트롤러 위에 @Secured 어노테이션을 붙이고 인자로 받은 유저 권한이 있는 사용자만 허용이 가능하게 할 수 있다.
    @Secured("ROLE_ADMIN")
    @GetMapping("/{userid}")
    public ResponseEntity<Object> SelectUser(@PathVariable("userid") @UserId final String userId, Authentication authentication) {
    	System.out.println(authentication.getAuthorities());
    	UserDto.UserInfoResponseDTO responseDTO = service.findById(userId);
    	return SuccessResponse.createSuccess(SuccessCode.READ_USER_SUCCESS, responseDTO);
    }
    • ROLE_ADMIN으로 변경하고 API를 호출하니 정상적으로 응답이 오는 것을 확인할 수 있다.

    @Pre/PostAuthorize

    • PreAuthorize의 경우 Controller를 실행하기전에 권한을 확인하는 방식이며 PostAuthorize의 경우 Controller를 실행하고 나서의 권한 체크가 이뤄진다.
    • returnObject의 결과의 userId와 accessToken에서 추출한 유저의 아이디가 다를 경우 요청이 거절되고 ROLE_ADMIN의 경우 정상적으로 처리되게 설정해놨다.
    @PostAuthorize("(returnObject.body.data.userId == principal.username) or hasRole('ROLE_ADMIN')")
    @GetMapping("/{userid}")
    public ResponseEntity<Object> SelectUser(@PathVariable("userid") @UserId final String userId, Authentication authentication) {
        UserDto.UserInfoResponseDTO responseDTO = service.findById(userId);
        return SuccessResponse.createSuccess(SuccessCode.READ_USER_SUCCESS, responseDTO);
    }
    • USER_ROLE을 가진 chanhong 계정의 AccessToken으로 타인의 계정인 chan9784를 조회한 결과 아래와 같이 거절되었다.

    • 본인의 계정(chanhong)을 조회한 결과 올바른 응답을 출력하는 것을 알 수 있다.

    • ADMIN 계정인 chan9784의 accessToken을 이용하여 타인의 계정인 chanhong을 조회한 결과 올바른 결과 값을 반환하는 것을 알 수 있다.

    예외 처리

    • Spring Security Filter에서 발생하는 예외는 @RestControllerAdvice에서 처리할 수 없다. 왜냐하면 Filter 에러는 Controller에 도달하기전에 에러를 발생시켜 @RestControllerAdvice가 에러를 처리할 수 없기 때문이다.
    • Spring Security는 AuthenticationException을 발생시켰을 때, AuthenticationEntryPoint에서 예외 처리를 시도한다. 또한, AccessDeniedException을 발생시켰을 때는 AccessDeniedHandler가 예외를 처리하게 된다. 따라서, AuthenticationEntryPoint와 AccessDeniedHandler 구현체들을 활용한다면 @RestControllerAdvice에서 에러를 처리할 수 있을 것이다.
    • 일단, @RestControllerAdvice에서 Spring Security 관련 에러를 처리할 수 있도록 에러 처리 메소드들을 등록했다.
        @ExceptionHandler({SignatureException.class, MalformedJwtException.class, UnsupportedJwtException.class})
        public ResponseEntity<Object> handleJwtException(final Exception e) {
            final ResponseCode responseCode = CommonResponseCode.INVALID_PARAMETER;
            return handleExceptionInternal(responseCode, "유효하지 않은 토큰입니다.");
        }
    
        @ExceptionHandler(ExpiredJwtException.class)
        public ResponseEntity<Object> handleJwtAuthException(final ExpiredJwtException e) {
            final ResponseCode responseCode = CommonResponseCode.UNAUTHORIZED_REQUEST;
            return handleExceptionInternal(responseCode, "만료된 토큰입니다.");
        }
    
        @ExceptionHandler(AccessDeniedException.class)
        public ResponseEntity<Object> handleAuthException(final AccessDeniedException e) {
            final ResponseCode responseCode = CommonResponseCode.FORBIDDEN_ACCESS;
            return handleExceptionInternal(responseCode, "권한이 존재하지 않습니다.");
        }

     

    • AuthenticationEntryPoint와 AccessDeniedHandler의 구현체를 만들어주었다. @ExceptionHanlder가 처리할 수 있도록 HandlerExceptionResolver에서 @Qualifier를 활용하여 handlerExceptionResolver를 지정해주었다.
    @Component
    public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
        private final HandlerExceptionResolver resolver;
    
        public JwtAuthenticationEntryPoint(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
            this.resolver = resolver;
        }
    
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
            resolver.resolveException(request, response, null, authException);
        }
    }
    @Component
    public class JwtAccessDeniedHandler implements AccessDeniedHandler {
        private final HandlerExceptionResolver resolver;
    
        public JwtAccessDeniedHandler(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
            this.resolver = resolver;
        }
    
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
            resolver.resolveException(request, response, null, accessDeniedException);
        }
    }
    • 또한, Spring Security에서 발생하는 에러들을 해당 구현체들이 처리할 수 있도록 Security Filter Chain에서 설정했다.
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .csrf(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .sessionManagement((sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeRequests((authorize) -> authorize
                        .requestMatchers(
                                new AntPathRequestMatcher("/api/users/login", HttpMethod.POST.name()),
                                new AntPathRequestMatcher("/api/users", HttpMethod.POST.name()),
                                new AntPathRequestMatcher("/api/users/regenerateToken", HttpMethod.POST.name())
                        ).permitAll()
                        .anyRequest().authenticated()
                )
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling(handler -> handler
                        .accessDeniedHandler(accessDeniedHandler)
                        .authenticationEntryPoint(authenticationEntryPoint))
                .build();
    }
    • 이로써 해당 구현체들을 통해서 에러를 처리할 수 있을 것이라 기대하여 테스트를 진행했다. 그 결과 JwtException의 종류인 ExpiredJwtException 에러가 출력됐지만 postman에는 401이 아닌 500 에러가 출력되는 것을 볼 수 있다.
    • JwtException 에러 클래스가 잡아서 처리한 것이 아니라 아래 메소드가 에러를 잡아 처리하는 것을 확인할 수 있었다.
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleAllException(final Exception e) {
        final ResponseCode responseCode = CommonResponseCode.INTERNAL_SERVER_ERROR;
        System.out.println(e.getMessage());
        return handleExceptionInternal(responseCode);
    }

    • JwtAuthentication Filter에서 try ~ catch로 JwtException이 발생하면 request에 발생한 에러 객체를 삽입했다.
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = resolveToken(request);
        try {
            if (token != null && jwtTokenProvider.validateToken(token)) {
                Authentication authentication = jwtTokenProvider.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (JwtException e) {
            request.setAttribute("exception", e);
        }
        filterChain.doFilter(request, response);
    }
    • 이후, 각 구현체에서 해당 객체들을 빼내어 resolver에게 던져줌으로써 상세한 에러 처리를 가능하게 했다.
    // JwtAuthenticationEntryPoint
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
        resolver.resolveException(request, response, null, (Exception) request.getAttribute("exception"));
    }
    
    
    // JwtAccessDeniedHandler
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
        resolver.resolveException(request, response, null, (Exception) request.getAttribute("exception"));
    }
    • 테스트한 결과 아래와 같이 401에러가 올바르게 응답하는 것을 알 수 있었다.

    다음 포스팅에서는 redis를 활용하여 refreshToken의 저장과 갱신을 다뤄보도록 하겠다.
Designed by Tistory.