저번 글에서 JWT 토큰을 발급 받는 과정까지 진행했었습니다.
이번 글에서는 JWT 인증 정보를 컨트롤러에서 어떻게 활용해야 할지,
필터에서 처리된 인증 정보를 어떻게 @AuthenticationPrincipal로 가져올 수 있는지에 대해 작동 방식과 사용 예시를 알아보도록 하겠습니다.
먼저 스프링에서는 필터를 적용하면 필터가 호출 된 다음에 디스패처 서블릿이 호출된다.
필터 흐름 : HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
필터는 체인으로 구성되어서, 중간에 필터를 여러개 자유롭게 추가할 수 있다.
JWT 토큰을 추출하고 검사 후 인증객체를 생성하는 과정 또한 필터로 진행할 수 있다.
@RequiredArgsConstructor
public class JwtAuthFilter extends GenericFilterBean {
private final JwtTokenProvider jwtTokenProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 헤더에서 JWT 토큰 추출
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
// 유효성 검사 후 SecurityContext에 저장
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
// 다음 필터로 넘어가기
chain.doFilter(request, response);
}
}
순서는 아래와 같다
1. HTTP 요청 헤더에서 JWT 토큰 추출 (Authorization 헤더)
2. 토큰 유효성 검증 및 사용자 정보 추출
3. 인증 객체 생성(UsernamePasswordAuthenticationToken)
4. SecurityContextHolder에 인증 객체 저장
1,2,3번은 저번에 작성해준 JwtTokenProvide
에 이어서 작성했다.
// JWT 토큰에서 인증 정보 조회
public Authentication getAuthentication(String token) {
UserDetails userDetails = customUserDetailService.loadUserByUsername(this.getUserPk(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// JWT 토큰의 유효성 + 만료일자 확인
public boolean validateToken(String jwtToken) {
try {
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
return !claimsJws.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
// Request의 Header에서 token 값을 가져오기
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7); // "Bearer " 이후의 토큰을 반환
}
return null;
}
// 토큰에서 회원 정보 추출
public String getUserPk(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
resolveToken()
: Authorization 헤더에서 토큰 추출
나는 이 과정을 생략했어서 계속 403에러가 났었다...꼭 토큰 추출해줘야된다...
validateToken()
: 토큰의 유효성 검사
getAuthentication()
: 인증 정보를 추출하여 Authentication 객체를 생성
여기까지 진행후에 Filter에서 SecurityContext에 Authentication 객체를 setAuthentication()
으로 저장해주면 컨트롤러 같은 계층에서 요청시에 참조가 가능하다.
@AllArgsConstructor
public class CustomUserDetails implements UserDetails {
private Long id;
private String nickname;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return nickname;
}
public Long getId() {
return id;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetails를 implement 해주면 되고 난 권한정보는 생략하고 필요한 것만 세팅해줬다.
프로젝트에서 필요한것들로 커스텀해주면 된다.
@Service
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService {
private final UserRepository userRepository;
// pk 받아서 해당 유저를 찾아 CustomUserDetails로 반환
@Override
public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {
User user =
userRepository
.findById(Long.parseLong(id))
.orElseThrow(() -> new UsernameNotFoundException(USER_NOT_FOUND.getMessage()));
return new CustomUserDetails(user.getId(), user.getNickname());
}
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable) // CSRF 비활성화
.sessionManagement(
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 비활성화
.authorizeHttpRequests(
authorize ->
authorize
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html")
.permitAll() // Swagger 경로는 누구나 접근 가능
.requestMatchers("/api/v1/auth/**")
.permitAll()
.anyRequest()
.authenticated() // 그 외의 경로는 인증된 사용자만 접근 가능
)
.addFilterBefore(
new JwtAuthFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter.class); // JWT 필터 추가
return http.build();
}
위의 과정을 완료하면 스프링 security holder에 인증 객체가 저장이 된 것으로 컨트롤러에서 사용할 수 있다.
@PostMapping("/profile")
public ResponseEntity<ApiResponse<?>> profile(
@Valid @RequestBody AddUserProfileRequest request,
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
userService.updateProfile(request, customUserDetails.getId());
return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE);
}
Filter가 요청을 처리하면서 Authentication 객체를 SecurityContextHolder에 저장하게되고
컨트롤러에서는 @AuthenticationPrincipal을 통해 CustomUserDetails 객체를 바로 주입받아 위와 같이 사용할 수 있다
authorization header를 포함한 상태에서(자물쇠모양) 요청을 보내면 실행이 되는 것 확인완료!
여기까지 카카오 소셜 로그인 + jwt 토큰을 사용한 인증 + spring security에서 CustomUserDetails 활용에 대해 알아보았다.
📌 앞으로...
참고 블로그 🙇
https://velog.io/@win-luck/Springboot-카카오-소셜로그인-Jwt-토큰-발급-및-API-검증