JwtFilter
이번 프로젝트에서는 스프링 시큐리티를 사용하기로 했다.
또한 회원가입/로그인 시 Jwt를 발급하고 쿠키로 Jwt를 받아서 인증을 하는 방식을 채택했다.
그래서 Jwt를 검증하는 필터를 만들 필요가 생겼다.
OncePerRequestFilter는 스프링 시큐리티에서 제공하는 필터가 아니다.
그래서 인증 로직이 시큐리티 컨텍스트에 어거지로 넣는 느낌이 들어서 이게 맞나 싶긴 하다.
하지만 시큐리티 필터들도 기본적으로는 org.springframework.web.filter 에서 파생되지 않을까 싶었고
시간이 부족하기 때문에 우선은 이렇게 구현하기로 했다.
시큐리티는 더 공부하면 더 좋은 방법을 찾을 수 있을지 모르겠다.
구현
JwtAuthFilter
OncePerRequestFilter를 상속받은 JwtAuthFilter를 만든다.
스프링 시큐리티와 따로 노는 느낌이 드는건 이 필터를 빈 등록하면 모든 요청에 대해 필터링을 한다.
시큐리티에서 승인한 URL까지 필터링을 하기 때문에 골치였다.
결국 ExcludeUrlsRequestMatcher를 따로 구현하고, shouldNotFilter를 오버라이드 해서 필터링 하지 않는 URL들을 설정했다.
JwtUtils에서 Jwt를 검증하고, 만약 잘못된 요청이라면 예외를 던져서 catch 문에서 에러 메시지를 내려줬다.
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
private final RequestMatcher excludeUrlsMatcher;
public JwtAuthFilter(JwtUtils jwtUtils, RequestMatcher excludeUrlsMatcher) {
this.jwtUtils = jwtUtils;
this.excludeUrlsMatcher = excludeUrlsMatcher;
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return excludeUrlsMatcher.matches(request);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Cookie[] cookies = request.getCookies();
try {
String jwt = jwtUtils.getJwtFromCookie(cookies);
Authentication authentication = jwtUtils.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
} catch (BadCredentialsException e) {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(UTF_8.name());
response.setStatus(SC_UNAUTHORIZED);
response.getWriter().write(e.getMessage());
}
}
}
ExcludeUrlsRequestMatcher
RequestMatcher를 구현하는 구현체를 만들어서 JwtAuthFilter에서 필터링 하지 않는 URL들을 지정했다.
public class ExcludeUrlsRequestMatcher implements RequestMatcher {
private String[] excludeUrls;
public ExcludeUrlsRequestMatcher(String... excludeUrls) {
this.excludeUrls = excludeUrls;
}
@Override
public boolean matches(HttpServletRequest request) {
String requestURI = request.getRequestURI();
AntPathMatcher pathMatcher = new AntPathMatcher();
return Arrays.stream(excludeUrls)
.anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, requestURI));
}
}
SecurityConfig
@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final ObjectMapper objectMapper;
private final UserDetailsService userDetailsService;
private final JwtUtils jwtUtils;
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring()
.requestMatchers("/favicon.ico")
.requestMatchers("/error")
.requestMatchers(toH2Console()); // TODO: 배포 시 제거
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/**").permitAll()
)
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jsonAuthFilter(), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(e -> {
e.authenticationEntryPoint(new Http401Handler(objectMapper));
e.accessDeniedHandler(new Http403Handler(objectMapper));
})
.csrf(AbstractHttpConfigurer::disable)
.build();
}
@Bean
public JwtAuthFilter jwtAuthFilter() {
return new JwtAuthFilter(jwtUtils, new ExcludeUrlsRequestMatcher(
"/api/auth/**",
"/api/v1/auth/**",
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**"));
}
@Bean
public JsonAuthFilter jsonAuthFilter() {
JsonAuthFilter filter = new JsonAuthFilter("/api/auth/login", objectMapper);
filter.setAuthenticationManager(authenticationManager());
filter.setAuthenticationSuccessHandler(new LoginSuccessHandler(objectMapper, jwtUtils));
filter.setAuthenticationFailureHandler(new LoginFailHandler(objectMapper));
return filter;
}
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return new ProviderManager(provider);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new SCryptPasswordEncoder(
16,
8,
1,
32,
64
);
}
}
결과
이렇게 해두면 ExcludeUrlsRequestMatcher 로 설정한 URL을 제외한 모든 요청에 대해 Jwt 인증을 수행한다.
그런데 스프링 시큐리티 세팅과 너무 따로 노는 느낌이 들어서 좋은 방법이 있다면 수정해보고 싶다.
'백엔드 > Spring Security' 카테고리의 다른 글
비로그인 사용자의 권한 처리: 메서드 시큐리티 적용 (0) | 2024.05.14 |
---|---|
시큐리티 환경에서 테스트 2: 컨트롤러 테스트하기 (0) | 2024.04.25 |
시큐리티 환경에서 테스트 1: @WithMockUser 커스텀 (0) | 2024.04.24 |