현재 진행중인 프로젝트에서 인가 보안전략으로 JWT Token을 사용하던 도중만나게 된 에러로 분석을 해보자면 JWT 보안에 사용되는 서명이 일치하지 않을 경우에 발생하는 에러이다.
에러 분석
여기서 서명이 일치하지 않다는 것이 어떤 뜻인지 부터 설명을 해보자.
우리가 흔히알고 있는 JWT의 구조를 생각해보자 아마 위의 사진과 비슷한 구조를 많이 보았을 것이다.
여기서 서명부분은 우리가 SecretKey, 즉 암호화 키값을 가지고 HMA512 또는 BASE 64와 같은 알고리즘을 사용하여 암호화를 진행시킨다. 이는 서버의 인가 자원인 Token을 암호화하기 위해 반드시 거쳐야하는 작업으로 메세지가 조작되지 않았다는 무결성을 보장하기 위한 작업이기도 하다.
그렇기에 해당 에러를 맞닥뜨렸을 경우 이 서명 쪽 즉, Secret Key에 대해서 살펴보아야한다.
이 에러가 나는 경우는 크게 3가지 정도가 존재한다.
에러 발생원인
1. JWT 토큰의 생성시점의 시크릿 키와 복호화 시점의 시크릿 키가 달라 발생하는 경우
2. 토큰의 유효기간이 만료되었거나 잘못된 토큰 정보일 경우
3. 서로 다른 라이브러리의 객체를 사용하여 JWT 토큰을 생성 / 복호화하는 경우
필자의 경우는 3번 케이스에 해당하는 경우로 상당히 어이 없는 실수를 범하였다.
코드 분석
아래는 필자의 JWT 토큰 생성 코드의 일부분을 가져온 것이다.
public String createAccessToken(String name) {
Date now = new Date();
return JWT.create()
.withSubject(ACCESS_TOKEN_SUBJECT)
.withExpiresAt(new Date(now.getTime() + accessTokenExpirationPeriod))
.withClaim(NICKNAME_CLAIM, name)
.sign(Algorithm.HMAC512(secretKey));
}
현재 프로젝트에서 OAuth2.0을 이용한 회원가입을 진행하고 있기에 OAuthAttributes에서 name을 추출하여 그를 기준으로 토큰을 발급한다.
여기서 변수로 사용되는 ACCESS_TOKEN_SUBJECT는 AccessToken(접근 토큰)의 이름을 지정한 것이고, accessTokenExpirationPeriod의 경우 AccessToken의 유효기간을 초단위로 86400(하루)로 설정하였다.
아주 간단히 토큰을 발행할 수 있는 로직이며 이는 OAuthService에서 OAuth 인증 및 인가 과정이 끝나게 되면 OAuthSuccessHandler로 넘어가게 된다. 이때 AccessToken을 발행하게된다.
그래서 처음 문제점을 찾기 위해 log를 찍으며 혹시라도 메소드 호출과정에서 내가 불필요한 값을 불러 에러가 나는지 파악을 하고 있었다.
하지만 자바 컴파일러에서는 문제가 되는 원인의 코드로 아래 메소드를 보여주었다.
public Date getExprienceAccessToken(String accessToken){
Claims cl = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(accessToken).getBody();
System.out.println("claims="+cl);
return cl.getExpiration();
}
이 메서드의 경우 AccessToken의 만료기간이 정확히 적용되었는지 확인하기 위해 사용된 코드였다.
그래서 나는 이 문제를 accessToken이 넘어오는 곳과 Parsing과정에서 문제가 있다고 생각하였는데 전부 아니였다.
그러던 중 토큰을 발행하는 로직에서 사용되는 JWT class와 코드를 Parsing하여 정보를 가져오는 JWTs의 호환문제가 아닐까 싶어 이를 위주로 디버깅해본 결과 나의 예상이 맞아 떨어지었다.
결론
최종적인 문제는 `createAccessToken` 메소드에서 사용되는 JWT는 "Auth0"의 라이브러리를 사용중이며, `getExprienceAccessToken` 메소드에서는 Spring Security 라이브러리인 io.jsonwebtoken을 사용하기 때문에 앞서말한 서명의 생성방식과 검증방식이 다르게 동작하여 일어난 에러였다.
내가 현재 사용하는 Auth0 라이브러리는 자체적으로 JWT 서명을 검증하는 기능을 제공하며, JWT 검증 결과를 직접 반환하는 반면, Spring Security의 io.jsonwebtoken 라이브러리는 JWT 서명을 검증하기 위해 내부적으로 객체를 생성 후 이를 통해 검증 후 반환하기 때문에 검증 결과는 당연히 다르게 나온다.
또한 Auth0 라이브러리는 디폴트로 HS256 알고리즘을 사용하며, io.jsonwebtoken 라이브러리는 디폴트로 HS512 알고리즘을 사용하기 때문에 검증방식과 암호화방식의 차이로 유효한 토큰이여도 Parsing 작업이 원활하지 못하였다.
이 에러를 통해 객체의 내부 원리를 파악 후 하나의 라이브러리로 통일하거나 규격을 맞추어 사용하는 것을 지향하여야한다는 점을 알 수 있었다
'Spring-boot' 카테고리의 다른 글
JPA 1차 캐시와 2차 캐시 (1) | 2023.02.26 |
---|---|
SpringBoot 외부 Rest API 호출 방식(JAVA) - RestTemplate (0) | 2023.02.25 |
N+1 이슈 (0) | 2022.12.06 |
@Getter와 @Setter는 왜 지양되어야 하는가? (0) | 2022.11.29 |
[Spring] 패키지 구조 (0) | 2022.10.21 |