오늘은 결제 프로젝트에서 Spring Security를 JWT 기반(stateless)으로 구성하고, Access Token / Refresh Token을 도입하면서 겪었던 이슈들과 해결 과정을 정리했다.
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST, "/api/login", "/api/signup", "/api/refresh").permitAll()
.requestMatchers("/api/**").authenticated()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(email, password)
);
MemberUserDetails userDetails = (MemberUserDetails) authentication.getPrincipal();
String accessToken = jwtProvider.createAccessToken(userDetails.getUsername(), userDetails.getMember().getRole());
String refreshToken = jwtProvider.createRefreshToken(userDetails.getUsername());
기존에는 로그인 시 내가 직접 memberRepository.findByEmail 후 passwordEncoder.matches 했는데 Spring Security 정석 흐름으로 가려면 UserDetailsService가 필요하다.
처음에 회원 없을 때 내가 만든 예외(ServiceErrorException)을 던졌더니…
로그인 호출 시 아래 예외로 감싸져서 올라오며 500이 됨:
-> 결론: UserDetailsService에서 회원 없으면 UsernameNotFoundException을 던지는 게 정석.
if (token != null && jwtProvider.validateToken(token)) {
// refresh token은 인증용이 아님
if (jwtProvider.isRefreshToken(token)) {
filterChain.doFilter(request, response);
return;
}
String email = jwtProvider.getClaims(token).getSubject();
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
memberRepository.findByEmail(email).orElseThrow(() -> new ServiceErrorException(...));
이러면 GlobalExceptionHandler에서 잡혀서 400으로 떨어지길 기대했다.
-> 해결 방향