[Spring Security, JWT, Redis] Access Token 인증 구현

3Beom's 개발 블로그·2023년 8월 13일
0

프로젝트에서 Spring Security + JWT + Redis 방식으로 인증/인가 로직을 구현하였고, 전체 과정을 기록으로 남겨두려 한다.


Access Token 인증/인가 과정

  • 본 과정은 SecurityConfig에 인증이 필요하다고 등록한 URL에 접근했을 때, AuthenticationFilter에서 Access Token을 통해 인증/인가 로직을 수행하는 과정이다.
  • 따라서 AuthenticationFilter를 구현한 후 SecurityConfig에 등록한다.

AuthenticationFilter 구현

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;
    private final ObjectMapper objectMapper;
    private final String UTF_8 = "utf-8";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
        FilterChain filterChain) throws ServletException, IOException {
        try {
            // 1. Request Header 로부터 Access Token을 추출한다.
            String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);

            // 2. 추출한 Token의 유효성 검사를 진행한다.
            if (token != null && jwtTokenProvider.validateToken(token)) {
                // Token이 유효할 경우, Authentication 객체를 생성하여 SecurityContext에 저장한다.
                Authentication authentication = jwtTokenProvider.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
            filterChain.doFilter(request, response);
        } catch (TokenException e) {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setCharacterEncoding(UTF_8);
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);

            response.getWriter().write(
                objectMapper.writeValueAsString(
                    ResponseDto.create(e.getMessage())
                )
            );
        }
    }
}
  • JWT를 활용한 인증을 위해 JwtAuthenticationFilter를 구현한다.
  • OncePerRequestFilter를 상속받아 구현된 클래스는 AuthenticationFilter로 등록할 수 있다.
    • GenericFilterBean을 상속받아 구현할 수도 있지만, 해당 클래스를 상속받을 경우 한 번의 요청에 여러번 불필요하게 중복되어 인증 과정이 수행될 수 있다.
    • 반면 OncePerRequestFilter를 상속받아 구현할 경우, 한 번의 요청당 한 번만 인증 과정이 수행된다.
    • 자세한 내용은 GenericFilterBean vs OncePerRequestFilter 키워드로 구글링해보면 된다!
  • JwtTokenProvider의 메서드들을 활용하여 Access Token의 유효성을 검증하고, Authentication 객체를 생성해 SecurityContext에 저장한다.
    • Access Token 유효성이 검증되면 인증은 수행된 것으로 볼 수 있다.
    • Access Token으로부터 생성된 Authentication에 권한 정보가 저장되어 있어 이후 인가 과정에 활용된다.
  • Access Token이 유효하고 SecurityContext에 Authentication 객체도 잘 저장되면 filterChain.doFilter() 메서드가 호출되며 그 다음 Filter로 넘어가게 된다.
  • 만약 Access Token 유효성 검증 과정에서 예외가 발생할 경우, catch 하여 적절하게 응답할 수 있도록 설정한다.
    • 이 때 유의해야 할 점은 예외가 발생했을 경우 filterChain.doFilter() 메서드가 실행되지 않도록 try 문 내에 포함시켜야 한다는 점이다.

    • 예외가 발생하면 이후의 Filter 들을 거치지 않고 바로 예외 상황에 맞는 응답을 하도록 설정하는 것이다.

      (이렇게 안하니까 내가 설정한 예외 응답이 아닌 응답이 계속 떴다..ㅠ)

    • 이후 인증/인가 관련 예외처리 과정 에서 자세히 다룬다.
      👉 [Spring Security, JWT, Redis] 인증/인가 관련 예외 처리

SecurityConfig에 Filter 등록

  • JwtAuthenticationFilter를 구현했으면 해당 Filter를 SecurityConfig에 등록해야 한다.
// SecurityConfig

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtTokenProvider jwtTokenProvider;
    private final ObjectMapper objectMapper;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .httpBasic().disable()
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()

            .authorizeRequests()
            .antMatchers(HttpMethod.GET, "/~~~").permitAll()
            .antMatchers("/~~~").authenticated()
            .regexMatchers(HttpMethod.POST, "/~~~").authenticated()
            .anyRequest().authenticated()
            .and()

            // addFilterBefore() 메서드를 통해 Filter를 등록할 수 있다.
            .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, objectMapper),
                UsernamePasswordAuthenticationFilter.class);;

        return http.build();
    }
}
  • addFilterBefore() 메서드를 통해 Filter를 등록할 수 있으며, UsernamePasswordAuthenticationFilter 앞에 JwtAuthenticationFilter 를 등록하는 것이다.
  • 이렇게 등록하는 이유는 JwtAuthenticationFilter를 통해 SecurityContext에 Authentication 객체가 저장될 경우, 기존 Spring Security에서 UsernamePasswordAuthenticationFilter로 수행하던 인증 과정이 생략되기 때문이다.
profile
경험과 기록으로 성장하기

0개의 댓글