본문 바로가기
Spring

[Spring] SpringSecurity - JWT

by worldcenter 2024. 11. 11.

 

 

SpringSecurity Filter를 사용하여 JWT 로그인을 어떤 식으로 구현하였는지 인증 아키텍처와 소스코드를 비교하면서 살펴보겠습니다.

 

 

SecurityConfiguration 구현

@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함(Security Configuration 지정)
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;
    private final AuthenticationConfiguration authenticationConfiguration;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
        filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
        return filter;
    }

    @Bean
    public JwtAuthorizationFilter jwtAuthorizationFilter() {
        return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf((csrf) -> csrf.disable());

        // 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
        http.sessionManagement((sessionManagement) ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .requestMatchers("/").permitAll() // 메인 페이지 요청 허가
                        .requestMatchers("/api/user/**").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );

        // 로그인 페이지
        http.formLogin((formLogin) ->
                formLogin
                        .loginPage("/api/user/login-page").permitAll()
        );

        // 필터 관리
        http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

 

  • @EnableWebSecurity 어노테이션은 스프링 시큐리티를 활성화하고 웹 보안 설정을 구성하는 데 사용
  • addFilterBefore() 메소드를 사용해 필터 체인 순서를 결정

 

 

JwtAuthorizationFilter

@Slf4j(topic = "JWT 검증 및 인가")
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;

    public JwtAuthorizationFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) {
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {

        String tokenValue = jwtUtil.getJwtFromHeader(req);

        if (StringUtils.hasText(tokenValue)) {

            if (!jwtUtil.validateToken(tokenValue)) {
                log.error("Token Error");
                return;
            }

            Claims info = jwtUtil.getUserInfoFromToken(tokenValue);

            try {
                setAuthentication(info.getSubject());
            } catch (Exception e) {
                log.error(e.getMessage());
                return;
            }
        }

        filterChain.doFilter(req, res);
    }

    // 인증 처리
    public void setAuthentication(String username) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = createAuthentication(username);
        context.setAuthentication(authentication);

        SecurityContextHolder.setContext(context);
    }

    // 인증 객체 생성
    private Authentication createAuthentication(String username) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}

 

  • JwtAuthorizationFilter는 JWT 토큰을 검증하여 사용자 인증을 처리하고, 인증된 정보를 SecurityContextHolder에 저장하는 역할
  • 보통 SpringSecurity 인증 흐름은 UsernamePasswordAuthenticationFilter가 사용자의 아이디와 비밀번호를 받아 인증을 수행하고 인증이 성공하면 AuthenticationManager와 SecurityContextHolder가 사용자 정보를 인증된 객체로 저장
  • 그러나 SpringSecurity 기본 인증 방식과 다르게, 요청에 포함된 JWT 토큰을 검증하여, 사용자 세션을 유지할 필요 없이 인증된 사용자 정보를 UsernamePasswordAuthenticationToken을 생성하여 SecurityContextHolder에 저장(AuthenticationManager 사용 X)

 

 

JwtAuthenticationFilter

@Slf4j(topic = "로그인 및 JWT 생성")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final JwtUtil jwtUtil;

    public JwtAuthenticationFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
        setFilterProcessesUrl("/api/user/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);

            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            requestDto.getUsername(),
                            requestDto.getPassword(),
                            null
                    )
            );
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) {
        String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
        UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();

        String token = jwtUtil.createToken(username, role);
        response.addHeader(JwtUtil.AUTHORIZATION_HEADER, token);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
        response.setStatus(401);
    }

}

 

  • JwtAuthenticationFilter는 로그인 요청을 처리하고, 인증 성공 시 JWT 토큰을 생성하여 클라이언트에 반환하는 역할을 하는 필터
  • UsernamePasswordAuthenticationFilter를 상속받아 인증을 시도합니다.
  • AuthenticationManager에 UsernamePasswordAuthenticationToken을 전달하여 인증을 수행
  • 인증이 성공하였을 때 사용자 정보를 SecurityContextHolder에 저장하지 않고 JWT만 생성하여 응답 헤더에 포함하여 전송

 

 

결론

  • JwtAuthenticationFilter에서는 직접적으로 SecurityContextHolder를 사용하지 않음. 대신, AuthenticationManager를 통해 인증이 성공한 후 JWT를 생성해 클라이언트에게 반환하는 방식
  • SpringSecurity의 기본 세션 기반 인증에서는 SecurityContextHolder에 인증 정보를 저장하여 세션을 통해 인증 상태를 유지. 그러나, JWT 기반 인증에서는 각 요청마다 JWT가 포함되어 전달되므로 SecurityContextHolder에 인증 정보를 저장할 필요가 없음
  • JwtAuthorizationFilter는 JWT를 검증하고 사용자 정보를 SecurityContextHolder에 설정하여 요청 단위의 인증 상태를 유지