JWT를 이용할 경우, 다음의 필터 구성을 한다.
- JWT 검증 -> 로그인 + JWT 발급 -> 인가 -> 예외처리
- UsernamePasswordAuthenticationFilter 앞에 JwtAuthenticationFilter 삽입
- JWT 의 커스텀 필터 예
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String token = resolveToken(request);
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication authentication =
jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext()
.setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (bearer != null && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}
또한, userNamePasswordAuthenticationFilter 를 그대로 쓰지 않고, 수정하여 사용한다.
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
private final JwtTokenProvider jwtTokenProvider;
public JwtLoginFilter(
AuthenticationManager authenticationManager,
JwtTokenProvider jwtTokenProvider
) {
setAuthenticationManager(authenticationManager);
setFilterProcessesUrl("/login");
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response
) {
String username = request.getParameter("username");
String password = request.getParameter("password");
UsernamePasswordAuthenticationToken authRequest =
new UsernamePasswordAuthenticationToken(username, password);
return getAuthenticationManager().authenticate(authRequest);
}
@Override
protected void successfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult
) throws IOException {
String accessToken =
jwtTokenProvider.createAccessToken(authResult);
response.setContentType("application/json");
response.getWriter().write("""
{
"accessToken": "%s"
}
""".formatted(accessToken));
}
}
최종적으로 필터 등록을 하는 예는 다음과 같다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final JwtTokenProvider jwtTokenProvider;
private final AuthenticationConfiguration authConfig;
public SecurityConfig(
JwtAuthenticationFilter jwtAuthenticationFilter,
JwtTokenProvider jwtTokenProvider,
AuthenticationConfiguration authConfig
) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
this.jwtTokenProvider = jwtTokenProvider;
this.authConfig = authConfig;
}
/**
* mvcMatcher 사용 시 반드시 필요
*/
@Bean
HandlerMappingIntrospector handlerMappingIntrospector() {
return new HandlerMappingIntrospector();
}
@Bean
SecurityFilterChain filterChain(
HttpSecurity http,
HandlerMappingIntrospector introspector
) throws Exception {
AuthenticationManager authenticationManager =
authConfig.getAuthenticationManager();
JwtLoginFilter jwtLoginFilter =
new JwtLoginFilter(authenticationManager, jwtTokenProvider);
// mvcMatcher 빌더
MvcRequestMatcher.Builder mvc =
new MvcRequestMatcher.Builder(introspector);
http
// 기본 인증 방식 제거
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
// 세션, CSRF
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(sm ->
sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 권한 설정
.authorizeHttpRequests(auth -> auth
.requestMatchers(
PathRequest.toStaticResources().atCommonLocations()
).permitAll()
.requestMatchers("/login").permitAll()
// 🔐 SpEL ① 본인 리소스 접근만 허용
.requestMatchers("/users/{id}")
.access(new WebExpressionAuthorizationManager(
"#id == authentication.principal.id"
))
// 🔐 SpEL ② ADMIN + 계정 활성화 사용자만 허용
.requestMatchers("/admin/reports/**")
.access(new WebExpressionAuthorizationManager(
"hasRole('ADMIN') and authentication.principal.enabled"
))
// ===============================
// ✅ RegexRequestMatcher 예제
// ===============================
// /api/v1, /api/v2, /api/v3 ... 버전 API
.requestMatchers(
new RegexRequestMatcher(
"^/api/v[0-9]+/.*",
null
)
).hasRole("USER")
// ===============================
// ✅ MvcRequestMatcher 예제
// ===============================
// Spring MVC 매핑 규칙 그대로 사용
.requestMatchers(
mvc.pattern(HttpMethod.GET, "/orders/{orderId}")
).hasRole("USER")
.requestMatchers(
mvc.pattern("/admin/**")
).hasRole("ADMIN")
.anyRequest().authenticated()
)
// 필터 등록 (순서 중요)
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
)
.addFilterAt(
jwtLoginFilter,
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
}
- SpEL (Spring Expression Language)를 이용할 경우, 복합 권한을 체크할 수 있다.
- 이 경우에는 .access() 를 이용한다.
- mvc.pattern 을 통해 실제 컨트롤러에 매핑된 url을 이용할 수 있다 (인자값 까지 포함된 경우)
- regex 의 경우, url 경로에 v숫자 가 들어가 있는 경우에 활용 할 수 있다.
'Backend > SpringBoot' 카테고리의 다른 글
| [Spring Security] CSRF 토큰 사용 (0) | 2025.12.16 |
|---|---|
| [Spring Security] MethodSecurity 및 커스텀 핸들러 (0) | 2025.12.16 |
| [Spring Security] 쿠키 및 세션 (0) | 2025.12.12 |
| @SQLRestriction 및 논리 삭제 (0) | 2025.12.11 |
| Retryable, 낙관적 락 간편 적용하기 (0) | 2025.12.09 |