[Security] JWT 관련 stateless 설정

정석·2024년 10월 4일

Spring

목록 보기
20/21
post-thumbnail

1. SecurityConfig

모든 설정에 앞서 시큐리티 config 파일 설정을 진행한다.

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
public class WebSecurityConfig {

    private final JwtSecurityFilter jwtSecurityFilter;

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        return http
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(jwtSecurityFilter, SecurityContextHolderAwareRequestFilter.class)
                .formLogin(AbstractHttpConfigurer::disable)
                .anonymous(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .logout(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/auth/**").permitAll()
                        .anyRequest().authenticated()
                )
                .build();
    }
}
  • PasswordEncoder:
    - security의 passwordEncoder 를 사용하며 BCryptPasswordEncoder 를 사용하기 위해 설정한다. BCrypt는 보안에 강력한 해시 함수로, 비밀번호를 안전하게 관리할 수 있다.
  • sessionManagement:
    - stateless 구조를 위해 다음과 같이 설정한다. stateless 구조는 서버가 사용자 상태를 관리하지 않으며, 모든 요청에 JWT 토큰을 사용해 인증을 진행한다.
  • addFilterBefore:
    - JWT 를 사용할 예정이므로 jwtSecurityFilterSecurityContextHolderAwareRequestFilter보다 먼저 실행하여, 요청이 들어올 때 JWT 토큰을 먼저 검증하고 인증된 사용자인지 확인한다.
  • authorizeHttpRequests:
    - /auth/로 들어오는 모든 API를 허용하여 로그인과 회원가입 시엔 필터를 통과하고, 나머지 경로에선 인증을 시도한다. JWT가 필요한 요청에만 인증 절차를 적용한다.

2. jwtSecurityFilter

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtSecurityFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(
            HttpServletRequest httpRequest,
            @NonNull HttpServletResponse httpResponse,
            @NonNull FilterChain chain
    ) throws ServletException, IOException {
        String authorizationHeader = httpRequest.getHeader("Authorization");

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            String jwt = jwtUtil.substringToken(authorizationHeader);
            try {
                Claims claims = jwtUtil.extractClaims(jwt);
                Long userId = Long.valueOf(claims.getSubject());
                String email = claims.get("email", String.class);
                UserRole userRole = UserRole.of(claims.get("userRole", String.class));
                String nickName = claims.get("nickName", String.class);

                if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                    AuthUser authUser = new AuthUser(userId, email, userRole, nickName);

                    JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(authUser);
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }

            } catch (SecurityException | MalformedJwtException e) {
                log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.", e);
                httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다.");
            } catch (ExpiredJwtException e) {
                log.error("Expired JWT token, 만료된 JWT token 입니다.", e);
                httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다.");
            } catch (UnsupportedJwtException e) {
                log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e);
                httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다.");
            } catch (Exception e) {
                log.error("Internal server error", e);
                httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            }
        }
        chain.doFilter(httpRequest, httpResponse);
    }
}

중간에 userId가 null 값이 아니고, SecurityContextHolder에 Authentication 값이 존재하지 않을 경우는 아직 인증된 정보가 추가되지 않은 것이기에 JwtAuthenticationToken을 생성하여 SecurityContextHolder에 저장한다.


3. JwtAuthenticationToken

public class JwtAuthenticationToken extends AbstractAuthenticationToken {

    private final AuthUser authUser;

    public JwtAuthenticationToken(AuthUser authUser) {
        super(authUser.getAuthorities());
        this.authUser = authUser;
        setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return authUser;
    }
}

Spring Security에서 JWT를 사용한 인증 토큰 객체를 정의하는 클래스이다.

  • JwtAuthenticationToken은 인증이 완료된 사용자 정보를 담고 있으며, AuthUser 객체를 principal로 반환한다.
  • getCredentials()에서 JWT 기반 인증을 사용하므로 자격 증명(Credentials)은 null을 반환한다.
  • setAuthenticated(true)는 이 토큰이 이미 인증된 상태임을 나타낸다.

0개의 댓글