티스토리 뷰

JwtTokenProvider

토큰 생성하고, 토큰을 복호화 해서 정보를 추출하고, 유효성을 검증하는 클래스입니다.

 

jwt 토큰 구현 방식으로는 access Token, refresh Token 두가지 토큰을 사용하고,

인증 타입으로는 Bearer를 사용하였습니다.

 

 

최종 구현 코드

@Slf4j
@Component // spring bean 등록
public class JwtTokenProvider {
    private final String key;
    private final Key jwtKey;
    private static final String BEARER_TYPE = "Bearer";
    private static final String CLAIM_JWT_TYPE_KEY = "type";
    private static final String CLAIM_AUTHORITIES_KEY = "authorities";
    private final long tokenValidTime = 1000 * 10L * 1000;
    public  final int REFRESH_TOKEN_EXPIRES = 60 * 60 * 24 * 14 * 1000L;

    public JwtTokenProvider(@Value("${jwt.key}") String key) {
        this.key = key;
        this.jwtKey = Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8));
    }

    // accessToken, refreshToken 생성한다.
    public JwtDto generateToken(Authentication authentication) {
        final Date now = new Date();
        final Date accessTokenExpiresIn = new Date(now.getTime() + tokenValidTime);
        final Date refreshExpiration = new Date(now.getTime() + REFRESH_TOKEN_EXPIRES);

        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())
                .claim(CLAIM_JWT_TYPE_KEY, BEARER_TYPE)
                .claim(CLAIM_AUTHORITIES_KEY, authentication)
                .setIssuedAt(now)
                .setExpiration(accessTokenExpiresIn)
                .signWith(jwtKey, SignatureAlgorithm.HS512)
                .compact();

        String refreshToken = Jwts.builder()
                .setSubject(authentication.getName())
                .setExpiration(refreshExpiration)
                .claim(CLAIM_AUTHORITIES_KEY, authentication)
                .claim(CLAIM_JWT_TYPE_KEY, BEARER_TYPE)
                .setIssuedAt(now)
                .signWith(jwtKey, SignatureAlgorithm.HS512)
                .compact();
        return new JwtDto(BEARER_TYPE, accessToken, refreshToken);
    }

    // JWT 토큰에서 인증 정보를 꺼낸다.
    public Authentication getAuthentication(String token) {
        // 토큰을 복호화 해서 클레임 가져오기
        Claims claims = parseClaims(token);
        // 클레임에서 권한 정보 가져오기
        List<SimpleGrantedAuthority> authorities = Arrays.stream(claims.get(CLAIM_AUTHORITIES_KEY).toString().split(","))
                .map(authority -> new SimpleGrantedAuthority(authority))
                .collect(Collectors.toList());
        // Authentication 리턴
        User principal = new User(claims.getSubject(), "", authorities);
        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
    }

    // Request의 Header에서 token 값을 가져온다
    // 인증 타입으로 Bearer을 사용하고 있어 "Bearer " 이후의 토큰값을 가져온다.
    public String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }

    // 토큰의 유효성을 검증한다.
    public boolean validateToken(String token) {
        try {
            Claims claims = parseClaims(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();
        }
    }

}

 

 

코드에 대한 구체적인 설명은 다음과 같습니다.

1. @Component

@Component // spring bean 등록
public class JwtTokenProvider {
  • ArgumentResolver와 Interceptor 구현체가 @Configuration 어노테이션을 사용하는 클래스에서 사용되고 있기 때문에,
  • @Component 어노테이션을 사용하여 해당 구현체를 스프링 빈으로 등록해야 합니다.
    • 이를 통해 Spring에서 구현체를 관리하고, 필요한 곳에 자동으로 주입해줍니다.
  • Interceptor : HttpServletRequest 로 토큰을 추출합니다.
  • ArgumentResolver : 토큰값으로 payload 를 가져옵니다.

 

 

2. generateToken()

인증된 사용자에 대해 accessToken, refreshToken 토큰을 생성합니다.

public JwtDto generateToken(Authentication authentication) {
	// 현재 시간(now)을 기준으로 미리 정해놓은 accessToken, refreshToken 만료 시간을 더해서 계산합니다.
    final Date now = new Date();
    final Date accessTokenExpiresIn = new Date(now.getTime() + tokenValidTime);
    final Date refreshExpiration = new Date(now.getTime() + REFRESH_TOKEN_EXPIRES);
	
    // Access Token 생성
    String accessToken = Jwts.builder()
            .setSubject(authentication.getName())
            .claim(CLAIM_JWT_TYPE_KEY, BEARER_TYPE)
            .claim(CLAIM_AUTHORITIES_KEY, authentication)
            .setIssuedAt(now)
            .setExpiration(accessTokenExpiresIn)
            .signWith(jwtKey, SignatureAlgorithm.HS512)
            .compact();
	
    // Refresh Token 생성
    String refreshToken = Jwts.builder()
            .setSubject(authentication.getName())
            .setExpiration(refreshExpiration)
            .claim(CLAIM_AUTHORITIES_KEY, authentication)
            .claim(CLAIM_JWT_TYPE_KEY, BEARER_TYPE)
            .setIssuedAt(now)
            .signWith(jwtKey, SignatureAlgorithm.HS512)
            .compact();
	// JwtDto 객체를 생성하여 토큰 타입, Access Token, Refresh Token을 반환
    return new JwtDto(BEARER_TYPE, accessToken, refreshToken);
}

 

1. 현재 시간(now)을 기준으로 미리 정해놓은 accessToken, refreshToken 만료 시간을 더해서 계산합니다.

  • Access Token 의 만료 시간 : 현재시간 + tokenValidTime = 약 3시간
  • Refresh Token 의 만료 시간 : 현재시간 + REFRESH_TOKEN_EXPIRES = 14일

2. Access Token 생성

  • Jwts.builder() : JWT 빌더를 초기화합니다.
  • setSubject(authentication.getName()) : 인증된 사용자의 이름을 토큰의 주체로 설정합니다.
  • claim(CLAIM_JWT_TYPE_KEY, BEARER_TYPE) : 토큰 타입을 "Bearer"로 설정하는 클레임을 추가합니다.
  • claim(CLAIM_AUTHORITIES_KEY, authentication) : 인증된 사용자의 권한 목록을 포함하는 클레임을 추가합니다.
  • setIssuedAt(now) : 토큰 발행 시간을 현재 시간으로 설정합니다.
  • setExpiration(accessTokenExpiresIn) : 토큰의 만료 시간을 미리 정의해놓은 변수로 설정합니다.
  • signWith(jwtKey, SignatureAlgorithm.HS512) : HS512 알고리즘과 jwtKey를 사용하여 토큰에 서명합니다.
  • compact() : 최종적으로 압축된 JWT 문자열로 변환합니다.

3. Refresh Token 생성:

액세스 토큰 생성과 유사한 과정으로 리프레시 토큰을 생성합니다.

만료 시간을 리프레시 토큰에 맞게 설정하고, 리프레시 토큰에는 권한 목록이 포함되어 있습니다.

 

4. JwtDto 객체를 생성하여 토큰 타입, 액세스 토큰, 리프레시 토큰을 반환합니다.

 

 

3. getAuthentication()

JWT 토큰에서  Authentication 인증 객체를 생성하고, 사용자의 인증 정보를 꺼내옵니다.

public Authentication getAuthentication(String token) {
    // 토큰을 복호화 해서 클레임 가져오기
    Claims claims = parseClaims(token);
    // 클레임에서 권한 정보 가져오기
    List<SimpleGrantedAuthority> authorities = Arrays.stream(claims.get(CLAIM_AUTHORITIES_KEY).toString().split(","))
            .map(authority -> new SimpleGrantedAuthority(authority))
            .collect(Collectors.toList());
    // 유저 정보를 가지는 인증 객체를 반환한다.
    User principal = new User(claims.getSubject(), "", authorities);
    return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}

1. parseClaims(token)

: 토큰을 복호화해서 Claims 객체를 가져옵니다.

3-1 에서 자세히 설명하겠습니다.

 

2. claims.get(CLAIM_AUTHORITIES_KEY).toString().split(",")

: Claims 객체에서 권한 정보를 가져옵니다.

 

3. User principal = new User(claims.getSubject(), "", authorities)

: 인증된 사용자인 User 객체를 생성합니다.

방금 생성한 인증된 사용자의 권한 목록(authorities)도 함께 설정합니다.

 

5. return new UsernamePasswordAuthenticationToken(principal, token, authorities)

: User 객체, 토큰, 권한 목록을 사용하여 UsernamePasswordAuthenticationToken 객체를 생성합니다.

3-2에서 사용자 인증 과정과 함께 자세히 설명하겠습니다.

 

 

3-1. ParseClaims()

: 토큰에 저장된 클레임(Claims) 정보를 반환하는 메서드

토큰을 복호화해서 클레임 정보를 추출합니다.

// 토큰을 복호화해서 클레임 정보를 추출한다.
private Claims parseClaims(String accessToken) {
    try {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
    } catch (ExpiredJwtException e) {
        return e.getClaims();
    }
}

 

메서드를 private으로 선언하여 외부에서 접근하지 못하도록 캡슐화 하였고,

따로 메서드로 빼서 사용해 재사용성을 높였습니다.

 

1. Jwts.parserBuilder().setSigningKey(key) 로 JWT Parser 를 생성합니다.
여기서 key는 JWT 토큰을 복호화하는데 사용되는 서명 키입니다.

2. parseClaimsJws(accessToken) 로 JWT 토큰을 복호화하고, 클레임 JWS 객체를 생성합니다.

3. getBody() 로 클레임 JWS 객체에서 클레임 정보를 추출합니다.

 

 

+ JWT,  JWS 에 관련하여

JWT 규격에 따르면 
“ JWT는 권한 claims 집합을 JSW와 (또는) JWE 구조로 인코드한 JSON 객체로 표현된다. ”
라고 되어 있습니다. 
기술적으로 “JWT”는 서명되지 않은 토큰을 의미하지만, 일반적인 상황에서 JWT는 JWS나 JWS+JWE를 의미합니다.

JWS  (JSON Web Signature)
서버는 JWT를 JWS 체계로 서명해서 시그너처 signature와 함께 클라이언트로 전송합니다.

 

JWT의 구조

JWT는 크게 세 부분 [ 헤더header, 페이로드payload, 시그너처signature ] 로 구성되어 있습니다.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
.
eyJzdWIiOiJ1c2Vycy9Uek1Vb2NNRjRwIiwibmFtZSI6IlJvYmVydCBUb2tlbiBNYW4iLCJzY29wZSI6InNlbGYgZ3JvdXBzL2FkbWlucyIsImV4cCI6IjEzMDA4MTkzODAifQ
.
1pVOLQduFWW3muii1LExVBt2TK1-MdRI4QjhKryaDwc

1. Header : 토큰의 타입 (ex. JWT)과 사용되는 암호화 알고리즘을 포함하는 JSON 객체입니다.
2. Payload : 실제 전송하려는 데이터를 포함하는 JSON 객체입니다.

이 JSON 데이터는 클레임 (claims)이라고도 불립니다.

클레임은 사용자 식별자, 발급자, 만료 기간 등과 같은 정보를 포함할 수 있습니다.
3. Signature : 헤더와 페이로드를 사용하여 생성한 디지털 서명입니다. 서명은 토큰이 변조되지 않았음을 확인하고 발급자를 검증해서 토큰의 무결성을 검증합니다.

 

 

 

3-2. AuthenticationManager 인터페이스 와 UsernamePasswordAuthenticationToken 객체

AuthenticationManager

: 인증 요청을 처리하는 인터페이스

구현 클래스는 사용자가 제공한 인증 정보(사용자 이름, 비밀번호)를 받아 사용자 인증을 처리합니다.

 

UsernamePasswordAuthenticationToken

: 사용자 인증 정보를 나타내는 객체

사용자 이름, 인증 정보(비밀번호 또는 토큰) 및 인증된 사용자의 권한 목록을 포함합니다.

 

 

인증 과정은 다음과 같습니다.

1. 사용자가 인증을 요청한다.

어떤 사용자가 로그인을 시도할 때 이름과 비밀번호로 인증을 요청하게 됩니다.

 

인증 전에, 사용자의 이름과 비밀번호를 사용하여 UsernamePasswordAuthenticationToken 객체가 먼저 생성하고

사용자 이름과 비밀번호가 설정되는데,  이때 권한 목록은 비어있습니다.

UsernamePasswordAuthenticationToken authenticationRequest = 
    new UsernamePasswordAuthenticationToken(username, password);

 

2. AuthenticationManager 가 인증을 처리한다.

생성된 authenticationRequest 인증 요청을 AuthenticationManager에 전달하여 인증을 처리합니다.

 

AuthenticationManager 구현 클래스는 인증 요청을 처리하고

성공적으로 인증된 사용자에 대한 새로운 UsernamePasswordAuthenticationToken 객체를 생성합니다.

이 객체는 SecurityContextHolder로 사용할 수 있습니다.

 

 

3. 인증된 사용자

성공적으로 사용자 인증이 마치면 UsernamePasswordAuthenticationToken 객체에 인증된 사용자의 권한 목록이 추가됩니다.

이렇게 수정된 UsernamePasswordAuthenticationToken 객체는 이제 인증된 사용자를 나타냅니다.

List<GrantedAuthority> authorities = new ArrayList<>();
// Add user authorities
Authentication auth = new UsernamePasswordAuthenticationToken(userDetails, null, authorities);

 

UsernamePasswordAuthenticationToken 객체의 주요 구성 요소

  • principal : 사용자 이름
  • credentials : 사용자가 제공한 인증 정보 : token 또는 비밀번호
  • authorities : 인증된 사용자의 권한 목록

 

 

 

4. resolveToken()

HttpServletRequest의 Header에서 token 값을 가져옵니다.

public String resolveToken(HttpServletRequest request) {
    String bearerToken = request.getHeader("Authorization");
    if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
        return bearerToken.substring(7);
    }
    return null;
}

 

1. String bearerToken = request.getHeader("Authorization") :

: 요청에서 "Authorization" 헤더의 값을 가져옵니다.

이 헤더에는 "Bearer "라는 접두사와 함께 JWT 토큰이 포함되어 있습니다.

2. if (bearerToken != null && bearerToken.startsWith("Bearer "))

: 가져온 "Authorization" 헤더의 값이 null이 아니고 "Bearer " 접두사로 시작하는 경우에만 다음 단계를 수행합니다.

3. return bearerToken.substring(7);

: "Bearer " 접두사를 제거하고 실제 JWT 토큰만 반환합니다. 

 

이렇게 추출된 토큰은 필터 또는 인터셉터에서 사용되어 인증 및 인가 처리에 필요한 사용자 정보와 권한을 얻는 데 사용됩니다.

 

 

 

5. validateToken()

토큰의 유효성을 검증합니다.

public boolean validateToken(String token) {
    try {
        Claims claims = parseClaims(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;
}

 

 

 

 

 

 

 

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함