Spring/Spring Security

[Spring Security][KoLiving] 4-2. JwtProvider

noahkim_ 2023. 10. 3. 18:06

이전 포스팅에서 JwtAuthenticationFilter는 OncePerRequestFilter를 확장하여 구현하였습니다.

JwtAuthenticationFilter 필터를 생성할 때 인증 작업을 AuthenticationManager에 위임해야 하나 말아야 하나 고민했습니다.

 

AuthenticationManager는 Filter들의 인증을 담당하는 Spring Security의 중요 컴포넌트입니다.

AuthenticationManager에 인증을 위임하게 될 경우 Spring Security의 인증 메커니즘 통합이 가능하여 확장성이나 유연성이 좋아집니다.

 

하지만 저는 AuthenticationManager에 위임하지 않고 직접 구현하였습니다.

  • JWT는 이미 잘 정의된 표준을 가지고 있으므로 표준을 준수하면서 추가적인 확장에 대한 상황이 많지 않습니다.
    • JWT는 Claim을 통해 자체적으로 정보를 가지고 있으므로 별도의 저장소 접근이 필요하지 않습니다.
    • JWT는 상태를 저장하지 않는 stateless 특징을 가지므로 서버가 관리할 필요가 없습니다.
    • JwtAuthenticationFilter는 다른 인증체계와의 독립성을 유지하기 위해 설계되었습니다,
  • JWT Token 처리를 위한 로직이 복잡하고 다양한 작업이 포함되므로 이를 모듈화하고 재사용성을 높이려 하였습니다.
    • JWT 토큰 처리는 유효성 검사 뿐만 아니라 발급부터 각종 JWT 토큰 작업이 필요합니다.
      • BlackListToken 관리, RefreshToken 관리 등의 복합적인 작업을 담당합니다.
    • 재사용성 있게 모듈화하고 관리하기 위해 설계적인 결정을 내렸습니다.
    • 복잡성을 효과적으로 관리하고 확장성 있게 하기 위해 JWT와 관련된 핵심기능을 효율적으로 처리하도록 구현했습니다.
  • 필터는 JwtProvider를 직접 호출하여 효과적으로 JWT 작업을 수행합니다.

 

1. JwtProvider

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtProvider {

    private final JwtProperties jwtProperties;

    public String generateAccessToken(JwtVo jwtVo) {
        Map<String, Object> payloads = new HashMap<>();
        payloads.put("email", jwtVo.getEmail());
        payloads.put("role", jwtVo.joinRolesToString());

        return generateJwtBuilder(payloads)
                .setSubject("Access Token (" + jwtVo.getEmail() + ")")
                .setExpiration(calculateExpiryDate(jwtProperties.getAccessValidity()))
                .compact();
    }

    public String generateRefreshToken(JwtVo jwtVo) {
        Map<String, Object> payloads = new HashMap<>();
        payloads.put("email", jwtVo.getEmail());

        return generateJwtBuilder(payloads)
                .setSubject("Refresh Token (" + jwtVo.getEmail() + ")")
                .setExpiration(calculateExpiryDate(getValidityDay(jwtProperties.getRefreshValidity())))
                .compact();
    }

    public void validateToken(String token) {
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(jwtProperties.getSecret());

        try {
            Jwts.parser()
                .setSigningKey(apiKeySecretBytes)
                .parseClaimsJws(token)
                .getBody();
        } catch (ExpiredJwtException e) {
            throw new JwtException("expired_token");
        } catch (MalformedJwtException e) {
            throw new JwtException("malformed_token");
        } catch (SignatureException e) {
            throw new JwtException("signature_invalid_token");
        } catch (UnsupportedJwtException e) {
            throw new JwtException("format_invalid_token");
        }
    }

    private JwtBuilder generateJwtBuilder(Map<String, Object> payloads) {
        Map<String, Object> headers = new HashMap<>();
        headers.put("typ", "JWT");
        headers.put("alg", jwtProperties.getAlgorithm());

        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.forName(jwtProperties.getAlgorithm());

        // 서명에 담을 데이터
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(jwtProperties.getSecret());
        Key signKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

        return Jwts.builder()
                .setHeader(headers)
                .setClaims(payloads)
                .setIssuedAt(Date.from(Instant.now()))
                .signWith(signatureAlgorithm, signKey);
    }

    private Date calculateExpiryDate(long validityHour) {
        return Date.from(Instant.now().plus(validityHour, ChronoUnit.HOURS));
    }

    private long getValidityDay(long refreshValidity) {
        return refreshValidity * 24;
    }
}
  • JWT 토큰의 생성과 유효성 검사 등의 핵심 기능을 제공하는 컴포넌트입니다.
  • 주요 메서드와 도우미 메서드로 나누어 응집도를 높였습니다.