JWT 복습

Godtaek·2024년 3월 20일
0

CS

목록 보기
4/4

1. 서론

프로젝트를 진행하며 Jwt를 개발했지만, 기억에 남지.... 않았다.... 그래서 코드를 다시 보며 복습하기 위한 글

프로젝트 환경
1. java 17
2. Spring 3.0.6

jwt란?

Json Web Token : 웹표준 (RFC 7519) 으로서 두 개체에서 JSON 객체를 사용하여 가볍고 자가수용적인 (self-contained) 방식으로 정보를 안전성 있게 전달

무상태성,자가수용성이 특징으로, 서버에서 세션으로 관리할 필요없이 인증 / 인가를 진행할 수 있다. 세션을 사용하지 않기 때문에, 분산 환경, MSA에서도 유용하다.

Json을 활용하여 경량화 / 유연성 특징을 가진다. 필요한 정보만을 토큰에 담아 데이터 양을 최소화하고, 추가 데이터를 token에 추가함으로 유연하게 토큰을 구성할 수 있다.

헤더 - 페이로드(정보) - 서명(signature) 세 단계로 구분된다.

  1. header : jwt의 유형과 해싱 알고리즘 방식등 메타데이터가 저장되어 있다. Base64로 인코딩되어 있다.
  2. payload : claims를 활용하여 정보를 key-value형태로 담는다.
  3. signature : 헤더와 payload를 인코딩한 뒤, 비밀키를 이용해 서명. jwt가 발신자에 의해 발급되었으며, jwt 내용이 변경되지 않았다는 것을 보장

개발 코드

public class JwtTokenProvider {
    public Boolean validate(String token, String userEmail, String key) {
        String userEmailByToken = getUserEmail(token,key);
        return userEmailByToken.equals(userEmail)&&!isTokenExpired(token,key);
    }

    public String generateToken(String userEmail, String key, Long expiredTime) {
        return doGenerateToken(userEmail, key, expiredTime);
    }

    public String getUserEmail(String token, String key) {
        return extractClaims(token, key).get("userEmail",String.class);
    }

    public String doGenerateToken(String userEmail, String key, Long expiredTime) {
        Claims claims = Jwts.claims();
        claims.put("userEmail", userEmail);
        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis()+expiredTime))
                .signWith(getKey(key), SignatureAlgorithm.HS256)
                .compact();
    }

    public Claims extractClaims(String token, String key) {
        return Jwts.parserBuilder()
                .setSigningKey(getKey(key))
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isTokenExpired(String token, String key) {
        Date expiration = extractClaims(token, key).getExpiration();
        return expiration.before(new Date());
    }

    private static Key getKey(String key) {
        byte[] keyByte = key.getBytes(StandardCharsets.UTF_8);
        return Keys.hmacShaKeyFor(keyByte);
    }
}

2. 함수 분석

key와 expiredTime은 환경변수로 지정되어 있는 상태.
key를 설정할 때, key의 길이는 충분히 길어야 한다.(512비트 이상을 권장)

doGenerateToken

    public String doGenerateToken(String userEmail, String key, Long expiredTime) {
        Claims claims = Jwts.claims();
        claims.put("userEmail", userEmail);
        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis()+expiredTime))
                .signWith(getKey(key), SignatureAlgorithm.HS256)
                .compact();
    }

JWT를 생성하는 함수.

Claims는 Jwt에서 정보를 담는 클래스다. Json으로 되어있다. Jwt에 담는 정보 한 조각을 클레임이라고 한다. 인증할 때, 클레임을 extract하여 필요한 정보를 얻는 셈.

클레임에는 registered claims, public claims, private claims가 존재한다.

  • registered claims: 등록된 클레임. 사전 정의되어 있으며, 변경할 필요는 없지만, 변경할 수 있다.
    - 위의 코드에서 IssuedAt, Expiration 등은 registered claims
  • public claims / private claims : 사용자가 직접 지정하는 claims.
    - public claims에는 고유값을 통해 충돌을 방지해야 하지만, private claims는 그럴 필요가 없다.

public / priavte claims에 대해서

velog나 국내 블로그를 찾아봤는데, public, priavte claims에 대해서 내가 이해한 거랑 다르게 설명하는 것 같다.

내가 이해한 것은

public에서는 UUID / URI 등 충돌하지 않는 것을 담아낸다는 것. 즉,

{
	"id" : "id1",
  	"email" : "123@velog.com"
}

등 충돌하지 않는 고유값이 들어간다는 것이고

private에서는 충돌이 허용되는 값. 예를 들면 부서 이름이나, 권한 같은 것들이 들어가는 것으로 이해했다.

{
	"department" : "HR"
}

여튼 doGenerateToken에서는 payload에 담을 claims를 설정하고, 비밀키값을 설정(signature)한다.

extractClaims

    public Claims extractClaims(String token, String key) {
        return Jwts.parserBuilder()
                .setSigningKey(getKey(key))
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
    
    public String getUserEmail(String token, String key) {
        return extractClaims(token, key).get("userEmail",String.class);
    }

받은 토큰에서 claims를 추출하여 원하는 정보를 얻는 과정. getUserEmail처럼 extractClaims에서 claim을 추출한 뒤, token에 담긴 정보를 가져올 수 있다.

validate

    public Boolean validate(String token, String userEmail, String key) {
        String userEmailByToken = getUserEmail(token,key);
        return userEmailByToken.equals(userEmail)&&!isTokenExpired(token,key);
    }

    private Boolean isTokenExpired(String token, String key) {
        Date expiration = extractClaims(token, key).getExpiration();
        return expiration.before(new Date());
    }

jwt 토큰이 유효한지 검증하는 함수. userEmail이 받은 email과 동일하며, 아직 파기되지 않았다면 유효한 것이다.

만약 여기를 넘지 못한다면, spring security filter 설정에 의해 다음 단계로 넘어가지 못하도록 했다.

3. 마치며

잘못된 부분이 있다면 알려주시면 감사하겠습니다!

예전 개발했던 코드를 보니, 새삼 리팩토링이 필요하다... 라는 생각이 들었다.


jwt : https://velopert.com/2389
jwt : https://www.iana.org/assignments/jwt/jwt.xhtml
jwt-claims : https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-token-claims
public vs private : https://stackoverflow.com/questions/49215866/what-is-difference-between-private-and-public-claims-on-jwt

profile
성장하는 개발자가 되겠습니다

0개의 댓글