본 내용은 인프런의 이도원님의 SpringCloud 강의를 참고하여 작성되었습니다.
이번 포스팅에서는 클라이언트가 사용자 인증을 마친 뒤 발급받은 JWT 토큰을 어떻게, 왜 활용하는지에 대해 알아보자.
JWT는 유저를 인증하고 식별하기 위한 토큰(Token) 기반 인증이다.
인증 Header 내에서 사용되는 토큰 포맷으로 토큰 자체에 사용자의 권한 정보나 서비스를 사용하기 위한 정보가 포함된다. JWT를 사용하면 RESTful과 같은 무상태(Stateless)인 환경에서 사용자 데이터를 주고받을 수 있게 된다.
세션(Session)의 경우 쿠키 등을 통해 사용자를 식별하고 서버에 세션을 저장했지만, JWT와 같은 토큰을 클라이언트에 저장하고 요청시 HTTP 헤더에 토큰을 첨부하는 것만으로도 단순하게 데이터를 요청하고 응답을 받아올 수 있다.
기존의 Cookie, Session 과 같은 전통적인 인증 시스템은 환경적인 한계가 존재한다. 예를들어 JavaScript 기반의 React에서 인증을 요청하게 되면 Java의 Session은 JSP같은 Java 계열이 아니면 사용이 불가하다.
Token 인증방식을 사용하면 굳이 Java 환경에 종속될 필요가 없다! 서버에서는 전달받은 Token이 유효한지만 검증하면 되니까!
https://jwt.io/
해당 웹사이트에서 발급 받았던 JWT Token을 Decoding 할 수 있다.
우측에서 Encoding된 JWT Token을 Decoding 한 결과를 볼 수 있다. JWT는 Header, Payload, Signature로 구성된다.
{
"alg": "HS256", // HMAC-SHA256 해싱 알고리즘
"typ": "JWT" // 토큰 타입
}
Header 부분은 JWT의 서명 알고리즘을 지정한다. "HS512"는 HMAC-SHA512를 나타내며, 서명 생성 및 검증에 사용되는 해시 알고리즘을 지정하는 부분이다.
{
"sub": "1234567890", // 사용자 식별자
"name": "John Doe", // 사용자 이름
"admin": true, // 권한 여부
"exp": 1682444800 // 만료 시간 (Unix Timestamp)
}
Payload 부분은 클레임 (Claim) 정보를 포함하며, 클레임은 토큰에 대한 추가적인 정보를 제공한다. 회원가입시에 userCode를 UUID를 통해 부여했고, 이를 Payload의 Subject로 사용했다.
Claim 정보는 상황에 따라 다르게 구성된다. (id_token과 AccessToken은 각 구성 Claim이 다름)
sub (Subject): JWT의 주체를 나타내며, 토큰이 관련된 사용자 또는 엔터티를 식별하는 데 사용된다. 주어진 값 "72ba254b-ef59-4508-9c06-4e6f6fce66c0"은 주체의 고유 식별자로 사용된다.
exp (Expiration Time): JWT의 만료 시간을 나타내며, 값 "1698756433"은 UNIX 시간 형식으로, 1970년 1월 1일부터 현재까지의 초 단위로 표현된 시간입니다. 만료 시간 클레임은 JWT의 유효성을 제한하고, 시간이 지난 토큰을 사용하지 못하게 한다.
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret_key
)
서명(Signature) 부분은 Header와 Payload의 내용을 보호하고 JWT의 무결성을 검증하기 위해 사용된다. 이 서명은 HMACSHA512 해시 함수를 사용하여 생성된 것을 볼 수 있다.
<Header>.<Payload>.<Signature>
클라이언트는 인증/인가 이후 전달받은 JWT 형식의 토큰을 서버에 전달하면 다음과 같은 과정을 통해 무결성을 검증한다.
new_signature = HMACSHA512(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
your-256-bit-secret(비밀키)
)
new_signature
과 클라이언트가 전달한 signature
를 비교한다.[AuthenticationFilter.java]
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
String userId = ((User) authResult.getPrincipal()).getUsername();
UserResponseDto userDetails = userService.getUserDetailsByUserId(userId);
// 비밀 키를 Base64로 인코딩
String keyBase64Encoded = Base64.getEncoder().encodeToString(env.getProperty("token.secret").getBytes());
// JWT Token을 생성하는 부분
String token = Jwts.builder()
// Payload 세팅: Subject를 유저의 코드로 설정
.setSubject(userDetails.getUserCode())
// 토큰 만료 시간 설정 (현재 시간 + 지정된 만료 시간)
.setExpiration(new Date(System.currentTimeMillis() + Long.parseLong(env.getProperty("token.expiration_time")))
// Signature(서명) 알고리즘 및 비밀 키를 사용하여 서명 생성
.signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret"))
.compact();
// 생성된 JWT Token을 HTTP 응답 헤더에 추가
response.addHeader("token", token);
// 유저 코드도 헤더에 추가
response.addHeader("userCode", userDetails.getUserCode());
}
이전 포스팅에서 설명했던 successfulAuthentication 코드를 보면 더 쉽게 이해할 수 있다.
JWT 토큰은 인증/인가를 요청한 사용자를 검증하기 위해 필요한 정보들을 담아 Header, Payload, Signature의 조합으로 해싱한 문자열이다.
핵심은 비밀키(secret_key)는 서버측에서 관리되기 때문에 사용자에게 전달했던 JWT 토큰의 변조 여부를 서버에서 검증할 수 있다는 것이다!
참고문헌
Inflearn: Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) 강의자료
https://velog.io/@u-nij/JWT