
스프링 시큐리티 filter chain에 요청에 담긴 JWT를 검증하기 위한 커스텀 필터를 등록해야 한다. 해당 필터를 통해 요청 헤더 Authorization 키에 JWT가 존재하는 경우 JWT를 검증하고 강제로 SecurityContextHolder에 세션을 생성한다.(이 세션은 STATELESS 상태로 관리되기 때문에 해당 요청이 끝나면 소멸된다.
JWT 검증 필터
package com.example.securityjwt.jwt;
import com.example.securityjwt.dto.CustomUserDetails;
import com.example.securityjwt.entity.UserEntity;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@RequiredArgsConstructor
public class JWTFilter extends OncePerRequestFilter {
private final JWTUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// request에서 Authorization 헤드를 찾음
String authorization = request.getHeader("Authorization");
// Authorization 헤더 검증
if (authorization == null || !authorization.startsWith("Bearer ")) {
System.out.println("token null");
filterChain.doFilter(request, response);
// 조건이 해당되면 메서드 종료 (필수)
return;
}
// Bearer 부분 제거 후 순수 토큰만 획득
String token = authorization.split(" ")[1];
// 토큰 소멸 시간 검증 - 소멸 되었다면
if (jwtUtil.isExpired(token)) {
System.out.println("token expired");
filterChain.doFilter(request, response);
// 조건이 해당되면 메서드 종료 (필수)
return;
}
// 토큰에서 username과 role 획득
String username = jwtUtil.getUsername(token);
String role = jwtUtil.getRole(token);
// UserEntity를 생성하여 값 set
UserEntity userEntity = new UserEntity();
userEntity.setUsername(username);
userEntity.setPassword("temppassword");
userEntity.setRole(role);
// UserDetails에 회원 정보 객체 담기
CustomUserDetails customUserDetails = new CustomUserDetails(userEntity);
// 스프링 시큐리티 인증 토큰 생성
Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
// 세션에 사용자 등록
SecurityContextHolder.getContext().setAuthentication(authToken); // 세션 생성
filterChain.doFilter(request, response);
}
}
SecurityConfig에 JWTFilter 추가
package com.example.securityjwt.config;
import com.example.securityjwt.jwt.JWTFilter;
import com.example.securityjwt.jwt.JWTUtil;
import com.example.securityjwt.jwt.LoginFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity // 시큐리티를 위한 config 파일
@RequiredArgsConstructor
public class SecurityConfig {
// AuthenticationManager가 인자로 받을 AuthenticationConfiguration 객체 생성자 주입
private final AuthenticationConfiguration authenticationConfiguration;
private final JWTUtil jwtUtil;
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() { // 비밀번호 암호화
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// csrf disable => session을 stateless 상태로 구현하기 때문에 csrf를 방어하지 않아도 상관없다.
http
.csrf((auth) -> auth.disable());
// Form 로그인 방식 disable
http
.formLogin((auth) -> auth.disable());
// http basic 인증 방식 disable
http
.httpBasic((auth) -> auth.disable());
// 경로별 인가 작업
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/login", "/", "/join").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated()); // 다른 요청은 로그인한 사용자만 접근
http
.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);
// usernamePasswordAuthenticationFilter를 대체해서 커스텀한 필터를 넣기 때문에 at 사용
http
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class);
// 세션 설정 (stateless하게 관리)
http
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
참고자료: 개발자 유미 스프링 시큐리티 JWT