스프링 시큐리티 + jwt 사용중 로그인 cannot be cast 오류

박찬규·2023년 6월 22일

GoodJobProject

목록 보기
6/9

프로젝트를 진행하면서 스프링 시큐리티 + 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;
	}

조건문을 바꿔주니 회원 인증이 잘 동작했다!

0개의 댓글