@AuthenticationPrincipal에 null이 들어온다?

허진혁·2023년 6월 1일
2

문제점

스프링 시큐리티를 통한 인증을 해야하는데 유저 정보가 null로 받아져요.

@Authentication을 통한 인증 유저를 가져오는 과정에서 겪은 문제를 해결해나가는 과정이에요.

전혀 예상하지 못한 결과였기 때문에 @AuthenticationPrincipal 관련 글들을 정말 많이 찾아봤습니다. 결국 @AuthenticationPrincipal 어노테이션 작동 과정을 통해 찾았어요.

전체적인 흐름

다음은 스프링 시큐리티의 전체적인 동작 과정이에요.(전체적인 흐름을 이해하기 편하도록 추가했습니다)

먼저 @AuthenticationPrincipal의 과정을 살펴볼 거에요.

AuthenticationPrincipalArgumentResolver

이 클래스 부터 들어가서 확인해보니

이부분에서 걸려 null이 리턴되는 거였어요.

다음과 같은 순서를 기반으로

1번에서 @AuthenticationPrincipal을 찾고 true라면 resloveArgument 메서드에 파라미터를 주입해 주는 거에요.

그렇다면 1번은 통과한 것을 알 수 있고, 2번의 과정에서 null이 나왔음을 유추해보았어요.

2번에서 SecurityContextHolder.getContext().getAuthentication()가 의미하는 것은 UserDetailsService의 loadUserByUsername() 메서드의 반환값이 UserDetail과 같아야 해요. (이 값은 위의 스프링 시큐리티 동작과정을 보면 UserDetail 임을 확인하면 되요.)

그렇다면 SecurityContextHolder에 setAuthentication()을 해주는 곳(=인증 로직을 넣어주는 곳)을 보아야 해요.

JwtAuthenticationFilter

저는 다음과 같이 구현했어요.

public class JwtAuthenticationFilter extends GenericFilterBean {

    private static final String AUTHORIZATION_HEADER = "Authorization";
    private static final String BEARER_TYPE = "Bearer";

    private final JwtTokenProvider jwtTokenProvider;
    private final RedisTemplate redisTemplate;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        // 1. Request Header 에서 JWT 토큰 추출
        String token = resolveToken((HttpServletRequest) request);

        // 2. validateToken 으로 토큰 유효성 검사
        if (token != null && jwtTokenProvider.validateToken(token)) {
            // (추가) Redis 에 해당 accessToken logout 여부 확인
            String isLogout = (String)redisTemplate.opsForValue().get(token);
            if (ObjectUtils.isEmpty(isLogout)) {
                // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장
                Authentication authentication = jwtTokenProvider.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        chain.doFilter(request, response);
    }

    // Request Header 에서 토큰 정보 추출
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_TYPE)) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

JwtTokenProvider

이제 jwtTokenProvider의 getAuthentication 메서드를 통해 어떤 객체가 생성 되는지를 알아봐요.

public Authentication getAuthentication(String accessToken) {
        Claims claims = parseClaims(accessToken);
        if (claims.get(AUTHORITIES_KEY) == null) {
            throw new SystemException(ErrorCode.INVALID_TOKEN);
        }

        // 클레임에서 권한 정보 가져오기
        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());

        // UserDetails 객체를 만들어서 Authentication 리턴
        UserDetails principal = new User(claims.getSubject(), "", authorities);
        return new UsernamePasswordAuthenticationToken(principal, "", authorities);
    }

🤔 반환 타입도 UserDetails라 같은데 그대로 null이 들어오길래, 몇일 고민했는데 드디어 @AuthenticationPrincipal 사용 시 파라미터가 제대로 주입 되지 않는 이유를 발견헀어요!!

❗️ 저는 인증 객체를 저장 하는 과정에서 매번 DB에 접근해서 User 정보를 가져왔던게 아니라, 토큰에서 추출한 정보만으로 인증 객체를 만든거에요.

그래서 getAuthentication() 메서드를 다음과 같이 바꿨어요.

public Authentication getAuthentication(String accessToken) {
        // 토큰 복호화
        Claims claims = parseClaims(accessToken);
        UserDetails userDetails = memberDetailService.loadUserByUsername(claims.getSubject());
        return new UsernamePasswordAuthenticationToken(userDetails, accessToken, userDetails.getAuthorities());
    }

드디어 값이 나왔어요 !! 😀😀😀😀😀

좌충우돌 해결과정을 봐주셔서 감사합니다 🕵️‍♂️

profile
Don't ever say it's over if I'm breathing

0개의 댓글