프로젝트를 진행하면서 스프링 시큐리티 + jwt 토큰을 이용한 회원 인증을 구현하던 중, 500에러가 발생해 로그를 확인하니 SecurityContext에서 User객체를 받아오는 과정에서 문제가 발생했음을 알 수 있었다.
우선 어떤식으로 회원을 인증하는가부터 보면,
나는 스프링 시큐리티에서 제공하는 편리한 어노테이션을 이용하고싶어서 jwt 토큰과 User객체를 사용해 회원 인증을 구현했다.
아래와 같이 매 요청마다 쿠키에 들어있는 accessToken을 확인하는 필터를 만들어서 SpringContext에 User정보를 넣어주는 방식이다.
public class JwtAuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 쿠키에서 accessToken 값을 가져온다.
Cookie accessToken = cookieUt.getCookie(request, "accessToken");
if (accessToken != null) {
String token = accessToken.getValue();
try {
if (jwtProvider.verify(token)) {
Map<String, Object> claims = jwtProvider.getClaims(token);
long id = (int) claims.get("id");
opMember = memberService.findById(id);
if (opMember.isPresent()) {
forceAuthentication(opMember.get());
}
}
} catch (ExpiredJwtException e) {
String userId = String.valueOf(opMember.get().getId());
Long ttl = redisUt.getExpire(userId);
if (ttl < 0) { // 리프레시 토큰까지 만료되었거나 키가 존재하지 않는 경우
// 재로그인
response.sendRedirect("/member/login");
}
// 새로운 액세스 토큰 발급
String newAccessToken = jwtProvider.genToken(opMember.get().toClaims());
response.addCookie(cookieUt.createCookie("accessToken", newAccessToken));
}
}
filterChain.doFilter(request, response);
}
// 강제로 로그인 처리하는 메서드 (로그인한 사용자의 정보를 가져옴)
private void forceAuthentication(Member member) {
User user = new User(member.getUsername(), member.getPassword(), member.getAuthorities());
// 스프링 시큐리티 객체에 저장할 authentication 객체를 생성
UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken.authenticated(
user,
null,
member.getAuthorities()
);
// 스프링 시큐리티 내에 authentication 객체를 저장할 context 생성
SecurityContext context = SecurityContextHolder.createEmptyContext();
// context 에 유저정보 저장
context.setAuthentication(authentication);
// 스프링 시큐리티에 context 등록
SecurityContextHolder.setContext(context);
}
}
forceAuthentication() 에서 User 객체를 만들어서 Security Context에 로그인한 유저 정보를 넣어줬는데,
이를 꺼내 쓰는 과정에서 오류가 발생했다.
@Component
@RequestScope
public class Rq {
public Rq(JwtProvider jwtProvider, CookieUt cookieUt, CustomDetailsService customDetailsService, MemberService memberService, HttpServletRequest req, HttpServletResponse resp) {
this.jwtProvider = jwtProvider;
this.cookieUt = cookieUt;
this.customDetailsService = customDetailsService;
this.memberService = memberService;
this.req = req;
this.resp = resp;
SecurityContext context = SecurityContextHolder.getContext();
if (context != null && context.getAuthentication() != null) {
this.user = (User) context.getAuthentication().getPrincipal();
} else {
this.user = null;
}
}
}
this.user = (User) context.getAuthentication().getPrincipal();
분명 jwt 필터에서 User객체를 넣어줬는데, 해당 객체를 꺼내는 이 코드에서 cannot be cast 에러가 발생한 것..!
에러메시지는 아래와 같다.
java.lang.ClassCastException: class java.lang.String cannot be cast to class
org.springframework.security.core.userdetails.User (java.lang.String is in module java.base of loader 'bootstrap';
org.springframework.security.core.userdetails.User is in unnamed module of loader 'app')
사용자의 쿠키에 값이 없을 경우를 생각해서 context != null && ... 조건을 줬는데... 여전히 String이 저장되는것이 의아해서 디버그를 찍어보니 감을 잡을 수 있었다.

Principal=anonymousUser는 시큐리티에서 인증되지 않은 사용자를 나타내는 디폴트 값이다. anonymousUser라는 문자열로 표시된다.
즉, 권한이 없는 사람의 디폴트 값이 문자열로 존재하기 때문에
if (context != null && context.getAuthentication() != null) {
this.user = (User) context.getAuthentication().getPrincipal();
} else {
this.user = null;
}
이 조건문에서 걸러내지 못 했던 것..!
// 변경 전 코드
SecurityContext context = SecurityContextHolder.getContext();
if (context != null && context.getAuthentication() != null) {
this.user = (User) context.getAuthentication().getPrincipal();
} else {
this.user = null;
}
// 변경 후 코드
SecurityContext context = SecurityContextHolder.getContext();
Object principal = context.getAuthentication().getPrincipal();
if (principal instanceof User) {
this.user = (User) principal;
} else {
this.user = null;
}
조건문을 바꿔주니 회원 인증이 잘 동작했다!