JWT 찐하게 이해하기 👀 | JWT 구성과 인증 원리
많이 듣고 많이 사용했던 JWT
싹 다 정리해보고 이전 과제때부터 마주했던 JwtUtil 클래스도 좀 살펴보려고 한다.
JWT와 Session을 통한 인증
JWT와 Session 모두 웹 서비스에서 사용자의 인증 상태를 유지하는 방법이다.
익숙한 말로는 로그인을 유지하는 방법이라고도 할 수 있겠다.
HTTP의 stateless한 특성으로 인해 사용자의 인증 정보를 유지하는 기술이 필요하고, 이를 위해 Session 혹은 JWT를 이용할 수 있다.
► Session 인증 방식
세션 인증 방식을 순서 매기자면 다음과 같다.
- 사용자 최초 로그인 시, 서버는 세션을 생성하여 인증 정보를 세션 저장소(별도 서버)에 저장한다.
- 서버는 세션 생성 시 발급한 sessionId를 클라이언트에 Cookie에 포함하여 응답한다.
- 클라이언트는 이후 Cookie에 sessionId를 포함하여 요청을 보낸다.
- 서버는 sessionId로 세션을 확인하여 사용자를 인증하고, 세션 저장소에서 사용자 인증 정보(ex. userId, role)를 가져와 사용한다.
즉, 로그인 후 유지할 상태 데이터가 서버 측에 저장되는 방식이다.
JWT와 Session 인증 방식의 큰 차이점은 바로 이 ``데이터의 저장 위치``이다.
► JWT 인증 방식
인증 방식을 순서 매기자면 다음과 같다.
- 사용자 최초 로그인 시, 서버는 인증 정보를 포함하여 JWT를 발급한다.
- 서버는 클라이언트에 응답 시 발급한 JWT를 전달한다.
- 클라이언트는 이후 ``Authorization`` 헤더에 JWT를 포함하여 요청을 보낸다.
- 서버는 JWT 서명을 검증하여 사용자를 인증하고, JWT에 포함된 사용자 인증 정보((ex. userId, role)를 사용한다.
즉, 서버가 상태 데이터를 저장하지 않는다.
토큰 자체에 유지할 데이터를 함께 포함시킨 후 클라이언트 측에서 토큰을 유지하도록 하는 방식이다.
서버는 토큰의 유효성을 검증하는 알고리즘만 동작시키면 될뿐, 별도로 상태를 저장할 저장소를 유지하지 않아도 된다.
✔️ Session를 통한 로그인 인증 방식
- 인증 : sessionId로 세션 조회, 유효한 세션이 있으면 인증 성공
- 인증 정보 사용 : sessionId로 조회한 세션에 저장된 데이터 사용
✔️ JWT를 통한 로그인 인증 방식
- 인증 : JWT 서명을 검증하여 유효한 정보인지 확인
- 인증 정보 사용 : JWT에 포함된 데이터 사용
JWT의 구성
이해한 내용 짧게만 정리해보자
자세한 내용은 여기 참고
- Header, Payload, Signature로 구성되어 있다.
- 세 파트가 dot(.)으로 이어져 있는 구조이다.
► Header
{
"alg": "HS256",
"typ": "JWT"
}
- JWT인만큼 JSON 형태로, 주로 위 코드에서 보이는 두 필드로 구성된다.
- `alg` : JWT의 세 번째 구성요소인 Signature를 만들 때 사용되는 암호화 알고리즘을 명시한다.
- `typ` : 해당 토큰의 타입을 명시하는 것으로 JWT이니까 JWT라고 명시한다.
해당 JSON 내용이 Base64Uri 인코딩 되어 JWT의 첫 번째 파트를 형성한다.
{"alg": "HS256", "typ": "JWT"} → Base64Uri 인코딩 → eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
► Payload
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
- 역시 JSON 형태로, 사용자 정보와 추가 데이터를 담는 부분이다.
- 여기 담기는 데이터를 Claim이라 부른다.
- 데이터를 누가 정의했는지, 어디에서 사용하는 지에 따라 3가지 종류로 나뉜다.
- 개발자가 임의로 넣는 로그인 유지 데이터는 Public Claim에 해당한다. (ex. userRole, nickname 등등)
해당 JSON 내용이 Base64Uri 인코딩 되어 JWT의 두 번째 파트를 형성한다.
► Signature
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
- 앞서 나온 Header,Payload를 secret key를 사용하여 서명 알고리즘을 돌린 값이다.
- 위 코드는 서명 알고리즘으로 HMAC + SHA256을 사용한 예이고, 이 외에도 HMAC + SHA-384, RSA + SHA-256 등등 다양하게 사용하는 것 같다.
이 Signature, 서명값을 검증해서 JWT가 유효한지를 판별한다.
✔️ JWT의 구성
- ``Header.Payload.Signature``
ex) ``eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.MzXXc7P0HPPzPLlXY21kGLBKTdYzq3bdhz76hn8fBYk``
1. Header : Base64 URL 인코딩됨
2. Payload : Base64 URL 인코딩됨
3. Signature : 암호화 알고리즘으로 서명됨
그래서 뭘 어떻게 검증한다는건데?
로그인 정보가 들어가는 Payload 파트는 Base64 URL로 인코딩이 되어 token에 그대로 들어간다.
즉, 디코딩 시 기존 JSON 형태의 데이터를 바로 확인할 수 있다. 따라서 절대로 민감한 정보를 포함해서는 안 된다.
이처럼 JWT는 로그인 정보를 암호화하지 않는다.
다만 서명을 통해 Payload가 변조되지 않았음을 확인한다.
➕ 서명
- 데이터의 무결성 보장 및 송신자 인증
- 데이터가 변경되지 않았음을 증명
➕ 암호화
- 데이터의 기밀성 보장
- 데이터를 제 3자가 읽을 수 없게 함
👀 JWT(JSON Web Token)는 JWS(JWT + 서명) 또는 JWE(JWT + 암호화)가 될 수 있다.
JWE는 Payload를 암호화하여 보호하는 것으로, 서명이 없다.
그렇지만 일반적으로 JWT는 JWS를 의미한다.
아래에 이해한 내용을 간단히 정리해봤다.
► JWT 생성
- ``Header + Payload``와 비밀키를 사용하여 해싱 함수를 돌리고, 결과값을 얻는다. (=> Signatrue 값)
- 이 Signature 값으로 JWT를 구성한 뒤 클라이언트에 전달한다.
► JWT 검증
- 전달된 JWT의 ``Header + Payload``와 비밀키를 사용하여 해싱 함수를 돌리고, 결과값을 얻는다.
- 해당 값과 전달된 JWT의 Signature 값이 동일한지 확인한다.
- 동일하지 않다면 Header 혹은 Payload의 값이 변조된 것임
- 이후 Payload에 만료 기간 정보 등을 넣어놨다면 비교해보고 invalid 처리를 하는 등의 작업을 수행한다.
JwtUtil 뜯어보기
그렇다면 그동안 사용했던 JwtUtil을 한 번 뜯어보자
► 코드 보기
@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil {
private static final String BEARER_PREFIX = "Bearer ";
private static final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
public String createToken(Long userId, String email, String nickname, UserRole userRole) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("email", email)
.claim("userRole", userRole)
.claim("nickname", nickname)
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date) // 발급일
.signWith(key, signatureAlgorithm) // 암호화 알고리즘
.compact();
}
public String substringToken(String tokenValue) {
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
return tokenValue.substring(7);
}
throw new ServerException("Not Found Token");
}
public Claims extractClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
}
- 서명에 사용하는 알고리즘으로는 HS256(HMAC SHA-256)를 사용한다.
- `createToken`
- JWT를 생성한다.
- claim으로 여러 데이터를 넣고 있다. userId를 포함하여 email, userRole, nickname, expiration 등등
- `signWith`으로 JWT Signature을 만들 때 사용할 서명 알고리즘과 비밀키를 전달한다.
- `extractClaims`
- 클라이언트로부터 전달된 JWT를 검증하고 Claim을 추출한다.
- `setSigningKey`로 서명 검증에 사용할 비밀키를 전달한다.
- ``Q1.`` 왜 서명 알고리즘은 전달하지 않을까?
- ``A1.`` Header에 명시되어 있기 때문
- `parseClaimsJws`
- JWS (JWT + Signature)
- JWT 서명을 검증하고 Payload를 해석한다.
- 검증 실패 시 여러 Exception 발생
- `getBody`
- Payload(Claims)를 추출한다.
► 서명 알고리즘 HS256(HMAC SHA-256)
궁금한걸 어찌하리 !! 짧게 알아보자
메세지를 주고 받을 때 메세지 인증 방법으로 사용할 수 있는 것으로 MDC, MAC이 있다.
🔽 MDC(Modification Detection Code), 변경 감지 코드
해당 메세지가 변경되지 않았음을 보장하기 위해 사용하는 것이다.
전달할 메세지 `M`이 있다면 해싱 함수에 넣어 얻은 결과값인 `H(M)`을 MDC로 사용한다.
- 송신측에서 별도의 안전한 채널로 `H(M)`을 전달한다.
- 수신측에서는 전달받은 메세지로 `H(M)` 값을 얻고, 전달받은 MDC와 비교하여 변경 여부를 확인한다.
해싱 함수는 input 값이 달라지면 아예 다른 값을 얻으므로 이를 통해 메세지가 변경되었는지를 감지하는 것이다.
🔽 MAC(Message Authentication Code), 메시지 인증 코드
MDC를 통해서는 해당 메세지가 수정되지 않았음은 확인할 수 있지만, 그 송신자가 인증된 자인지는 확인할 수가 없다.
메세지 변경 감지에 송신자 인증까지 수행하기 위해 사용하는 것이 MAC이다.
인증을 위해 송신자와 수신자만 아는 비밀키를 생성한다. 이 비밀키를 가진 사람인가? 를 확인해서 인증을 한다고 보면 된다.
전달할 메세지 `M`이 있다면 특정 비밀키 `K`와 함께 넣어 얻은 결과값인 `H(K|M)`을 MAC으로 사용한다.
- 송신측에서 `M`과 함께 `H(K|M)`을 전달한다.
- 수신측에서는 전달받은 `M`과 자신이 가진 `K`로 `H(K|M)` 값을 얻고, 전달받은 MAC과 비교하여 변경 여부를 확인한다.
MDC와 MAC 모두 해시 함수를 기반으로 만들어지는 것이 일반적이지만, 반드시 해시 함수를 사용해야 하는 것은 아니다.
HMAC은 MAC이긴 MAC인데 Hashed MAC이다.
메세지를 b 비트 블록으로 분할하고 키를 b 비트로 패딩하고 어떤 상수와 XOR 연산을 한 후에 해싱함수를 적용해서 중간 HMAC을 만들고 어쩌구저쩌구 복잡하다.
암호학 공부를 하는 중은 아니므로 자세히 알 필요는 없을 것 같고, 해시 함수를 여러 번 적용해서 이를 기반으로 만들어진 MAC이라고 보면 될 듯하다.
어쨌거나 HMAC SHA-256에서 HMAC은 메시지 인증 코드를 생성하는 방식을, SHA-256은 해당 코드 생성시 사용하는 해싱 알고리즘을 뜻한다.
😤 속이 다 시원하다