jwt에 대한 깊은 이해도 없이 열심히 사용하고 있던 나 자신.
스터디를 하며 jwt를 파헤치다가 jwt의 생성 및 검증 방식에 대해 알아보다가 재미있어서 글로 한번 정리 해 보고자 한다.
JWT?
JWT(JSON Web Token)는 웹 표준으로, 서버와 클라이언트 사이에서 정보를 안전하게 전송하기 위한 웹 토큰이다.
JSON형태의 데이터를 안전하게 전송하고 검증하는 기능을 제공하며, 다양한 암호화 알고리즘을 사용할 수 있다.

JWT는 Header, Payload, Signature 부분으로 나뉜다.
토큰의 타입이나 서명에 사용된 알고리즘의 정보를 담는다.
JWT에서 대부분 사용하는HS256는 HMAC+ SHA256을 의미한다.
페이로드(본문)에는 정보가 담기며, 이 정보를 claim이라고 부른다.
claim에는 세 가지 종류가 존재하며, registered claim, public claim, private claim으로 나뉜다.
registered claim의 종류
- iat(issued at): 해당 토큰이 발급된 시간
- exp(Expiration Time): 해당 토큰의 만료 시간
- aud(Audience): 해당 토큰을 발급받을 대상
- sub(Subject): 토큰 제목
- nbf(Not Before): 해당 토큰의 활성화 날짜. 이 시간 이전에는 해당 토큰을 사용할 수 없음을 보장한다
- iss(issuer): 토큰 발급자
- jti(JWT id): 토큰의 식별자. 여러 issuer가 토큰을 발급할 경우 이를 구분하기 위한 값이다
주로 iat, exp정도만 들어가며, 나머지는 사용해 본 적이 없다.
public claim의 경우 key를 URI형태로 짓는다고 하며, private claim은 클라이언트-서버가 합의하여 사용하는 클레임이다.
시그니처는 서명에 대한 정보로, 본 토큰의 위변조를 판별한다
첫 번째 사진처럼 Header와 Payload에 대한 정보를 인코딩하여 Secret key를 이용하여 HS256방식으로 해싱한 값이다.
HS256?
HS256는HMACwithSHA256이다.
SHA256은 해싱 알고리즘이며 암호화나 복호화의 개념이 아니기 때문에 키가 필요하지 않다.
HMAC은 해싱 기법을 이용하여 메세지의 위변조를 체크하는 기법이며, 대칭 키를 사용한다.
HMAC = Hash(Message, Key) + Message
위의 식에서 해싱할 때SHA-256알고리즘을 사용하기 때문에HS256으로 불리는 것이다.
256은 해싱한 값이 256bit(64자)라는 뜻이다!
그리고 각 부분을 BASEA64로 인코딩하여 .으로 연결하면 JWT Token이 생성되게 된다.
//build.gradle
//jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
public class JwtTestService {
private final static String SECRET_KEY = "mysecretmysecretmysecretmysecretmysecretmysecret";
private final static SecretKey key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
public static String createJwtToken() {
long nowMillis = System.currentTimeMillis();
Date now = new Date(System.currentTimeMillis());
// JWT를 생성하는 부분
return Jwts.builder()
.claim("userId", "j2noo") // private claim
.claim("token-type", "acess-token") // private claim
.setIssuedAt(now) // 발행 시간 설정
.setExpiration(new Date(nowMillis + 360000)) // 만료 시간 설정
.signWith(key) // 서명 설정
.compact(); // JWT 문자열로 변환
}
위 코드는 SECRET_KEY를 이용하여 하나의 JWT Token을 만드는 코드이다. 이 코드의 반환값은 다음과 같다.
eyJhbGciOiJIUzM4NCJ9.eyJ1c2VySWQiOiJqMm5vbyIsInRva2VuLXR5cGUiOiJhY2Vzcy10b2tlbiIsImlhdCI6MTcxOTA5NTQwMywiZXhwIjoxNzE5MDk1NzYzfQ.EOZFHohDm67qc6dQQYT65_Xro5kFzr6h-ERAaLi1B5vAQKQMEABH7yKZresQ0gtL
위 토큰은 .을 기준으로 Header, Payload, Signature로 나뉜 것을 확인 할 수 있다.
토큰의 내용이 암호화 된 것처럼 보이지만, 실제로는 BASE64 인코딩을 거쳤을 뿐 Signature를 제외하면 전혀 암호화 되어 있지 않다.

따라서 위의 사진처럼, JWT Token을 알고 있다면, SECRET KEY를 몰라도 BASE64로 디코딩만 하면 Header, Payload의 내용을 알 수 있다는 것이다.
// JWT 검증 후 클레임 추출
public static boolean validateJwtToken(String token) {
try {
SecretKey key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
Claims claims = Jwts.parser()
.verifyWith(key) // 여기서 jwt 토큰 검증(서명이 일치하는지 체크)
.build()
.parseSignedClaims(token)
.getPayload();
// 토큰에서 클레임을 추출하고
String userId = claims.get("userId", String.class);
System.out.println("userId: " + userId);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
위 코드에서 검증이 이루어지는 부분은 verifyWith이다.
BASE64 디코딩Header.Payload를 다시 key를 이용하여 해싱Signature와 일치하는지 검증해싱은 암호화/복호화와는 다르다.
암호화/복호화에는 키가 필요하며, 해싱은 키가 필요하지 않다.
JWT에서 사용하는 해싱 기법은 단방향 알고리즘 이며, 해싱한 값을 원본 값으로 돌릴 수 없다는 뜻이다.
따라서 JWT Token을 검증할 때 다음과 같은 방법을 사용하는 것이다.
Header.Payload를Secret key를 사용한HS256로 변환하여 해싱 값 생성Signature와 같은지 비교
아래의 방법이 왜 불가능하기 때문이다.
Signature를 해싱하기 전의Header.Payload로 되돌림(불가능!!!)Header.Payload와 같은지 비교
일반적으로 사용자의 비밀번호를 DB에 저장할 때는 해싱한 값을 저장한다.
그리고 사용자가 비밀번호를 잊어버렸을 때, 기존의 비밀번호를 알려주는 것이 아니라 새로운 비밀번호를 만들 것을 제안한다.
해싱의 원리를 이해한다면, 왜 기존의 비밀번호를 알려주지 못하는지 알 것 같다.