Knowledge/Trouble Shooting

java.lang.NullPointerException: Cannot invoke "org.board.exercise_board.User.Security.CustomUserDetails.getUsername()" because "customUserDetails" is null

똑똑한망치 2024. 4. 2. 22:10
728x90
반응형

😂 문제 상황

Spring Security + JWT 를 사용하여 현재 로그인이 되어 있는 사용자에 대한 정보를 얻기 위해 

@AuthenticationPrincipal 어노테이션을 사용했다.

하지만 해당 사용자에 대한 정보는 넘어오지 않고 Null 값만 넘어왔다.

 

문제의 그 Controller 이다.

 

 

내가 구현한 코드는 아래와 같다.

 

JwtAuthenticationFilter.java

@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter extends GenericFilterBean  {

  private final JwtTokenProvider jwtTokenProvider;

  public static final String TOKEN_HEADER = "Authorization";
  public static final String TOKEN_PREFIX = "Bearer";

  @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)) {
      Authentication authentication = jwtTokenProvider.getAuthentication(token);
      SecurityContextHolder.getContext().setAuthentication(authentication);
    }
    chain.doFilter(request, response);
  }


  // RequestHeader 에서 토큰 정보 추출
  private String resolveToken(HttpServletRequest request) {
    String bearerToken = request.getHeader(TOKEN_HEADER);

    if(!ObjectUtils.isEmpty(bearerToken) && bearerToken.startsWith(TOKEN_PREFIX)) {
      return bearerToken.substring(TOKEN_PREFIX.length());
    }
    return null;
  }
}

 

 

 

JwtTokenProvider.java

@Component
@Slf4j
public class JwtTokenProvider {

  private static final String AUTHORITIES_KEY = "auth";
  private static final String BEARER_TYPE = "Bearer";

  //유효시간 = 1시간
  private static final long ACCESS_TOKEN_EXPIRE_TIME = 60 * 60 * 1000L;

  // 유효시간 = 7일
  private static final long REFRESH_TOKEN_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000L;
  private final Key key;

  public JwtTokenProvider(@Value("${jwt.secretKey}") String secretKey) {
    byte[] keyBytes = Decoders.BASE64.decode(secretKey);
    this.key = Keys.hmacShaKeyFor(keyBytes);
  }

  /**
   * 유저 정보로 생성된 Authenticaiton으로 AccessToken, RefreshToken 생성
   *
   * @param authentication
   * @return
   */
  public JwtToken createToken(Authentication authentication) {

    // 권한 가져오기
    String authorities = authentication.getAuthorities().stream()
        .map(GrantedAuthority::getAuthority)
        .collect(Collectors.joining(","));

    long now = (new Date()).getTime();

    // AccessToken 생성
    Date accessTokenExpiratedTime = new Date(now + ACCESS_TOKEN_EXPIRE_TIME);

    String accessToken = Jwts.builder()
        .setSubject(authentication.getName())
        .claim(AUTHORITIES_KEY, authorities)
        .setExpiration(accessTokenExpiratedTime)
        .signWith(key, SignatureAlgorithm.HS256)
        .compact();


    // refreshToken 생성
    String refreshToken = Jwts.builder()
        .setExpiration(new Date(now + REFRESH_TOKEN_EXPIRE_TIME))
        .signWith(key, SignatureAlgorithm.HS256)
        .compact();

    return JwtToken.builder()
        .grantType(BEARER_TYPE)
        .accessToken(accessToken)
        .refreshToken(refreshToken)
        .build();
  }


  // Jwt 토큰을 복호화하여 토큰에 들어있는 인증 정보를 꺼내는 메서드
  public Authentication getAuthentication(String accessToken) {
    // Jwt 토큰 복호화
    Claims claims = parseClaims(accessToken);

    if (claims.get(AUTHORITIES_KEY) == null) {
      throw new RuntimeException("권한 정보가 없는 토큰입니다.");
    }

    // 해당 사용자의 권한 정보 가져오기
    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);
  }


  /**
   * 토큰 정보 검증
   * @param token
   * @return
   */
  public boolean validateToken(String token) {
    try {
      Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
      return true;
    } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
      log.info("Invalid JWT Token", e);
    } catch (ExpiredJwtException e) {
      log.info("Expired JWT Token", e);
    } catch (UnsupportedJwtException e) {
      log.info("Unsupported JWT Token", e);
    } catch (IllegalArgumentException e) {
      log.info("JWT claims string is empty.", e);
    }
    return false;
  }

  private Claims parseClaims(String accessToken) {
    try {
      return Jwts.parserBuilder()
          .setSigningKey(key)
          .build()
          .parseClaimsJws(accessToken)
          .getBody();
    } catch (ExpiredJwtException e) {
      return e.getClaims();
    }
  }
}

 

 

문제가 발생했던 부분은 이 부분이였다.

 

❗ 인증 객체를 저장하는 과정에서 DB에서 해당 User 정보를 가져오는 것이 아닌 새로 생성함으로써 발생하는 문제였다.

 

 

 

😁 해결

  // Jwt 토큰을 복호화하여 토큰에 들어있는 인증 정보를 꺼내는 메서드
  public Authentication getAuthentication(String accessToken) {
    // Jwt 토큰 복호화
    Claims claims = parseClaims(accessToken);

    if (claims.get(AUTHORITIES_KEY) == null) {
      throw new RuntimeException("권한 정보가 없는 토큰입니다.");
    }

    // 해당 사용자의 권한 정보 가져오기
    Collection<? extends GrantedAuthority> authorities =
        Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList());

    // UserDetails 객체 생성해서 Authentication 리턴
    UserDetails principal = customUserDetailsService.loadUserByUsername(claims.getSubject());
    return new UsernamePasswordAuthenticationToken(principal, accessToken, authorities);
  }

 

새로 User 객체를 생성하는 것이 아니라, 해당 사용자의 정보를 토대로 UserDetails 객체를 생성하였다.

반응형