모든 설정에 앞서 시큐리티 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();
}
}
jwtSecurityFilter를 SecurityContextHolderAwareRequestFilter보다 먼저 실행하여, 요청이 들어올 때 JWT 토큰을 먼저 검증하고 인증된 사용자인지 확인한다./auth/로 들어오는 모든 API를 허용하여 로그인과 회원가입 시엔 필터를 통과하고, 나머지 경로에선 인증을 시도한다. JWT가 필요한 요청에만 인증 절차를 적용한다.@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에 저장한다.
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를 사용한 인증 토큰 객체를 정의하는 클래스이다.
AuthUser 객체를 principal로 반환한다.getCredentials()에서 JWT 기반 인증을 사용하므로 자격 증명(Credentials)은 null을 반환한다.