JWT Series

JWT 토큰을 생성하는데 필요한 UserDetails 가 준비되었기 때문에 이제 JWT 토큰을 실제 생성하는 JWTTokenProvider 를 생성합니다.

Spring Security 의 인증 절차는 UsernamePasswordAuthenticationFilter 에서 이루어집니다.

해당 필터에는 doFilter() 와 attemptAuthentication() 이라는 추상 메서드가 존재하는데, 실제 인증 절차는 attemptAuthentication() 에 존재하는 것을 실제 확인할 수 있습니다.


	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		String username = obtainUsername(request);
		username = (username != null) ? username.trim() : "";
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
		UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
				password);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		return this.getAuthenticationManager().authenticate(authRequest);
	}

여기서 확인해야할 것은 UsernamePasswordAuthenticationToken 입니다.


public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {


한번 더 확인하면 UsernamePasswordAuthenticationToken -> AbstractAuthenticationToken -> Authentication 으로 상속받는 것을 볼 수 있습니다.

즉, UsernamePasswordAuthenticationToken 는 인증 과정 후 SecurityContextHolder.getContext()에 등록될 Authentication 객체입니다.

그리고 이 객체를 JWTTokenProvier 에서 UserDetails 와 함께 생성해냅니다.

JWTTokenProvider


/**
 * 토큰 관련 기능
 * <p>
 * - 토큰 생성
 * - 토큰 해석
 * - 토큰 유효성 검사
 */
@Slf4j
@Component
public class JwtTokenProvider {

    @Autowired
    private JwtProp jwtProp;
    @Autowired
    private UserRepository userRepository;

    public String createToken(Long uid, String username, List<String> roles) {
        byte[] signingKey = getSigningKey();

        return Jwts.builder()
                .signWith(getShaKey(signingKey), Jwts.SIG.HS256)
                .header().add("typ", JwtConstants.TOKEN_TYPE)
                .and()
                .expiration(new Date(System.currentTimeMillis() + JwtConstants.TOKEN_EXPIRATION))
                .claim("uid", uid)
                .claim("rol", roles)
                .claim("username", username)
                .compact();
    }


    @Transactional
    public UsernamePasswordAuthenticationToken getAuthentication(String header) {
        if (header == null || header.isEmpty()) {
            return null;
        }

        try {

            String jwt = header.replace(JwtConstants.TOKEN_PREFIX, "").trim();

            Jws<Claims> parsedToken = Jwts.parser()
                    .verifyWith(getShaKey(getSigningKey()))
                    .build()
                    .parseSignedClaims(jwt);

            Claims claims = parsedToken.getPayload();

            Object roles = claims.get("rol");
            log.info("roles :" + roles);

            Long uid = (Long) claims.get("uid");
            log.info("uid : " + uid);

            String username = claims.get("username").toString();
            log.info("username : " + username);

            if (username == null || username.isEmpty()) {
                return null;
            }

            User user = User.builder()
                    .id(uid)
                    .name(username)
                    .build();

            List<UserAuth> authList = ((List<UserAuth>) roles).stream()
                    .map(auth -> new UserAuth(auth.toString(), user))
                    .toList();

            //CustomUser 에 권한 담기
            List<SimpleGrantedAuthority> authorities = ((List<UserAuth>) roles).stream()
                    .map(auth -> new SimpleGrantedAuthority(auth.getAuth()))
                    .toList();

            try {
                User userInfo = userRepository.findById(uid).orElse(null);
                if (userInfo != null) {
                    user.toBuilder()
                            .email(userInfo.getEmail())
                            .build();
                }
            } catch (Exception e) {
                log.error(e.getMessage());
                log.error("토큰 유효 -> DB 추가 정보 조회 시 에러 발생");
            }

            UserDetails userDetails = new CustomUser(user);

            return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);

        } catch (ExpiredJwtException exception) {
            log.warn("Request to parse expired JWT : {} failed : {}", header, exception.getMessage());
        } catch (UnsupportedJwtException exception) {
            log.warn("Request to parse unsupported JWT : {} failed : {}", header, exception.getMessage());
        } catch (MalformedJwtException exception) {
            log.warn("Request to parse invalid JWT : {} failed : {}", header, exception.getMessage());
        } catch (IllegalArgumentException exception) {
            log.warn("Request to parse empty or null JWT : {} failed : {}", header, exception.getMessage());
        }
        return null;
    }

    /**
     * 토큰 유효성 검사 (만료기간 검사)
     */
    public boolean validateToken(String jwt) {
        try {


            Jws<Claims> parsedToken = Jwts.parser()
                    .verifyWith(getShaKey(getSigningKey()))
                    .build()
                    .parseSignedClaims(jwt);

            log.info("#### 토큰 만료 기간 ####");
            Date expiration = parsedToken.getPayload().getExpiration();
            log.info("-> " + expiration);

            return expiration.after(new Date());
        } catch (ExpiredJwtException exception) {
            log.warn("Request to parse expired JWT failed : {}", exception.getMessage());
            return false;
        } catch (JwtException exception) {
            log.warn("Request to parse tampered JWT failed : {}", exception.getMessage());
            return false;
        } catch (NullPointerException exception) {
            log.warn("Request to parse null JWT failed : {}", exception.getMessage());
            return false;
        }
    }

    private static SecretKey getShaKey(byte[] signingKey) {
        return Keys.hmacShaKeyFor(signingKey);
    }

    private byte[] getSigningKey() {
        return jwtProp.getSecretKey().getBytes();
    }
}

JWTTokenProvider 는 3가지 메서드를 가지고 있습니다.

  • 토큰 생성
  • 토큰 해석
  • 토큰 유효성 검사

CreateToken


public String createToken(Long uid, String username, List<String> roles) {
        byte[] signingKey = getSigningKey();

        return Jwts.builder()
                .signWith(getShaKey(signingKey), Jwts.SIG.HS256)
                .header().add("typ", JwtConstants.TOKEN_TYPE)
                .and()
                .expiration(new Date(System.currentTimeMillis() + JwtConstants.TOKEN_EXPIRATION))
                .claim("uid", uid)
                .claim("rol", roles)
                .claim("username", username)
                .compact();
    }

토큰 생성과정은 간단합니다. 미리 정해놓은 secretKey 를 가지고 서명을 진행하고 헤더와 claim 을 추가해 Jwts 의 빌더로 생성하면 됩니다.

getAuthentication


public UsernamePasswordAuthenticationToken getAuthentication(String header) {
        if (header == null || header.isEmpty()) {
            return null;
        }

        try {

            String jwt = header.replace(JwtConstants.TOKEN_PREFIX, "").trim();

            Jws<Claims> parsedToken = Jwts.parser()
                    .verifyWith(getShaKey(getSigningKey()))
                    .build()
                    .parseSignedClaims(jwt);

            Claims claims = parsedToken.getPayload();

            Object roles = claims.get("rol");
            log.info("roles :" + roles);

            Long uid = (Long) claims.get("uid");
            log.info("uid : " + uid);

            String username = claims.get("username").toString();
            log.info("username : " + username);

            if (username == null || username.isEmpty()) {
                return null;
            }

            User user = User.builder()
                    .id(uid)
                    .name(username)
                    .build();

            List<UserAuth> authList = ((List<UserAuth>) roles).stream()
                    .map(auth -> new UserAuth(auth.toString(), user))
                    .toList();

            //CustomUser 에 권한 담기
            List<SimpleGrantedAuthority> authorities = ((List<UserAuth>) roles).stream()
                    .map(auth -> new SimpleGrantedAuthority(auth.getAuth()))
                    .toList();

            try {
                User userInfo = userRepository.findById(uid).orElse(null);
                if (userInfo != null) {
                    user.toBuilder()
                            .email(userInfo.getEmail())
                            .build();
                }
            } catch (Exception e) {
                log.error(e.getMessage());
                log.error("토큰 유효 -> DB 추가 정보 조회 시 에러 발생");
            }

            UserDetails userDetails = new CustomUser(user);

            return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);

        } catch (ExpiredJwtException exception) {
            log.warn("Request to parse expired JWT : {} failed : {}", header, exception.getMessage());
        } catch (UnsupportedJwtException exception) {
            log.warn("Request to parse unsupported JWT : {} failed : {}", header, exception.getMessage());
        } catch (MalformedJwtException exception) {
            log.warn("Request to parse invalid JWT : {} failed : {}", header, exception.getMessage());
        } catch (IllegalArgumentException exception) {
            log.warn("Request to parse empty or null JWT : {} failed : {}", header, exception.getMessage());
        }
        return null;
    }

여기서는 클라이언트에서 헤더와 함께 들어오는 JWT 를 해석하는 메서드입니다.

JWT 를 parse 하여 내부 정보를 바로 취득할 수 있습니다.

이후 JWT 에 담긴 정보를 토대로 실제 User 의 데이터를 추가해 UserDetails 를 생성해냅니다.

이후 UsernamePasswordAuthenticationToken(userDetails, null, authorities);

를 생성해냅니다.해당 토큰은 이후 인증이 끝나고 SecurityContextHolder.getContext() 에 등록될 Authentication 객체입니다.

ValidateToken


public boolean validateToken(String jwt) {
        try {


            Jws<Claims> parsedToken = Jwts.parser()
                    .verifyWith(getShaKey(getSigningKey()))
                    .build()
                    .parseSignedClaims(jwt);

            log.info("#### 토큰 만료 기간 ####");
            Date expiration = parsedToken.getPayload().getExpiration();
            log.info("-> " + expiration);

            return expiration.after(new Date());
        } catch (ExpiredJwtException exception) {
            log.warn("Request to parse expired JWT failed : {}", exception.getMessage());
            return false;
        } catch (JwtException exception) {
            log.warn("Request to parse tampered JWT failed : {}", exception.getMessage());
            return false;
        } catch (NullPointerException exception) {
            log.warn("Request to parse null JWT failed : {}", exception.getMessage());
            return false;
        }
    }

Token 의 Expiration 을 검사하는 메서드입니다.

토큰을 동일하게 해석하고 expiration 을 검사합니다.

다음에는 실제 Provider 를 사용하는 Filter 에 대해서 알아보겠습니다.

업데이트:

댓글남기기