아래 코드를 보고 생겼던 의문, subject를 우리가 user의 UUID로 만들었는데, DB에 저장된 UUID와 추출된 subject가 동일한지 확인하는 코드가 왜 없지?
private boolean isJwtValid(String jwt) {
boolean returnValue = true;
String subject = null;
try {
subject = Jwts.parser().setSigningKey(env.getProperty("token.secret"))
.parseClaimsJws(jwt).getBody()
.getSubject();
}catch (Exception e){
returnValue=false;
}
if(subject==null||subject.isEmpty()){
returnValue = false;
}
System.out.println("subject = " + subject);
return returnValue;
}
지난 주 스터디때 토론을 나눴을땐 아래와 같은 이야기가 나왔다.
uuid 확인과 별도로 현재는 jwt형식이 맞는지만 검증하는 것이 아닐까?
그래서 jwt형식의 다른 토큰을 넣고 코드를 돌려봤으나 401에러가 발생했다. 인증에 실패한것이다.이게 잘못됐던건가?
따라서 해당 isJwt코드를 조금 더 파보고 싶었다.
공식 문서가 없어서 소스코드를 그냥 타고 들어가봤다.
JWS란?
Json Web Signature의 약자로 JSON 형식의 데이터를 서명하는 표준화된 방법을 정의한 스펙이다. 이를 통해 JWS는 데이터의 무결성과 원본 확인을 하는데 사용된다.
서명에 대한 개념을 간단히 이야기하자면, 내가 한게 내가 맞다! 인증 목적으로 쓰일 수도 있고, 오직 나만 할 수 있는 서명을 통해 데이터가 변경되지 않는, 데이터의 무결성을 확인하는 목적으로 활용된다.
JWS는 주로 인증과 권한 부여를 위한 JWT(JSON Web Token)를 생성하고 검증하는데 사용됩니다. JWT는 JWS를 확장하여, 토큰에 클레임 정보를 포함시켜 더 많은 정보를 전달하고 이를 서명하여 검증할 수 있는 기능을 제공합니다. JWT는 웹 애플리케이션에서 사용자 인증, API 호출 권한 부여 등에 널리 사용됩니다.
라고 한다.
아무튼! jwt 생성 과정을 보면 아래와 같은데 주요 작업 플로우는 대표적으로 3가지 과정이다.
1. 토큰화할 base인 subject를 정하자.
2. 만료 시간을 세팅하자.
3. singWith(): 서명을 만들 알고리즘과 서명에 사용할 키를 설정하자.
String token = Jwts.builder()
.setSubject(userDetails.getUserId())
.setExpiration(new Date(System.currentTimeMillis()+
Long.parseLong(env.getProperty("token.expirationTime"))))
.signWith(SignatureAlgorithm.HS512,env.getProperty("token.secret"))
.compact();
이를 바탕으로 해당 토큰이 유효한지 확인하는 과정이 필요한데, 토큰을 만들때와 반대로
1. 서명을 만들때 사용한 key를 불러오자.
2. 해당 토큰이 유효한지 확인하자.
두 가지 과정이 필요하다.
try {
subject = Jwts.parser().setSigningKey(env.getProperty("token.secret"))
.parseClaimsJws(jwt).getBody()
.getSubject();
}
첫 줄에서 키 세팅은 끝났고 우리가 의문이 들었던 것이
jwt -> subject로 바꿀때, 즉 복호화에 성공한 값인 subject가 uuid와 동일한지 확인하는 작업이 없었기 때문이다.
따라서, parseClaimJws의 구현을 찾아보았다.
@Override
public Jws<Claims> parseClaimsJws(String claimsJws) {
return parse(claimsJws, new JwtHandlerAdapter<Jws<Claims>>() {
@Override
public Jws<Claims> onClaimsJws(Jws<Claims> jws) {
return jws;
}
});
}
여기까지만 보면 잘 모르겠다. parse함수를 보자.
@Override
public <T> T parse(String compact, JwtHandler<T> handler)
throws ExpiredJwtException, MalformedJwtException, SignatureException {
Assert.notNull(handler, "JwtHandler argument cannot be null.");
Assert.hasText(compact, "JWT String argument cannot be null or empty.");
Jwt jwt = parse(compact);
if (jwt instanceof Jws) {
Jws jws = (Jws) jwt;
Object body = jws.getBody();
if (body instanceof Claims) {
return handler.onClaimsJws((Jws<Claims>) jws);
} else {
return handler.onPlaintextJws((Jws<String>) jws);
}
} else {
Object body = jwt.getBody();
if (body instanceof Claims) {
return handler.onClaimsJwt((Jwt<Header, Claims>) jwt);
} else {
return handler.onPlaintextJwt((Jwt<Header, String>) jwt);
}
}
}
parse(compact) 를 통해 주어진 string으로 jwt를 만든다.

과정은 알겠다.
이제 uuid와 비교하지 않음을 확인하기 위해 user service를 종료하고 새로 로그인을 했다.
이후 health-check를 했을때 이전 서비스의 토큰은 유효할까?
만료기간은 지나지 않았고, 서비스만 종료되었다.
그 결과 놀랍게도

아래는 java 기반의 jwt 공식 깃허브이니 필요할때 참고해보자. 영어 공부에 도움이 된다.
https://github.com/jwtk/jjwt#quickstart
GitHub - jwtk/jjwt: Java JWT: JSON Web Token for Java and Android
Java JWT: JSON Web Token for Java and Android. Contribute to jwtk/jjwt development by creating an account on GitHub.
github.com