프로젝트

jwt 토큰 방식을 선택한 이유

ompeom 2022. 10. 13. 23:04

프로젝트를 하면서 로그인 인증 방식을 아무 생각 없이 Spring Security 와 jwt 토큰 방식으로 채택하고 구현했다.

정확히 말하면 다른 사람이 만들어둔 코드를 복붙했다.

 

그리고 jwt 토큰이 뭔데? 구조가 어떤데? 왜 세션을 안쓰고 토큰을 쓰는데? 라는 질문을 들었을 때 아무런 대답을 할 수 없었다. 내가 만드는 코드에 이유를 말하지 못하는 개발 방식과 공부 습관은 정말 위험하다는 생각이 들었다.

 

jwt에 대해서 공부하였고, 내가 왜 이 방식을 선택했는지에 대해서 글을 남기려고 한다. 아직 spring security 까지 공부할 실력과 수준이 아니라고 판단되어 spring security 는 일단 프로젝트에서 걷어내기로 했다.

 

JWT 란?

  • Json Web Token 의 약자이다.
  • JWT 는 인증(Authentication)인가(Authorization) 에 사용된다.
  • 인증 절차를 거쳐 서버에서 JWT 를 발급하면, 클라이언트는 서버에 다시 보내어 인가 받을 수 있다.
  • JWT 는 HMAC 또는 RSA, ECDSA 를 사용한 공개키 방식으로 서명하기 때문에 무결성을 검증할 수 있다.

토큰 방식을 택한 이유

  • 서버가 클라이언트 인증을 확인하는 방식은 세션, 토큰 방식이 있다.
  • 기존 세션 방식은 서버가 유저의 세션 상태를 들고 메모리에 저장했다. 이는 사용자가 많아지면 서버에 무리를 줄 수 있으며, 서버의 확장성을 고려하지 않은 방식이다.
    • scale out 환경에서 세션 유지가 어려움 → sticky session, session clustering 등 사용
  • JWT 는 토큰에 대한 사용자 정보를 가지고 있으며, 검증을 증명하는 서명 등 모든 정보를 자체적으로 가지고 있다.
  • 리프레시 토큰 사용하여 보안성을 높일 수 있다.

Stateless

  • HTTP 는 무상태성을 가진 프로토콜이다. 무상태란 말 그대로 상태가 없다는 뜻이다. 상태를 저장하지 않고, 각 요청을 독립적인 트랜잭션으로 취급한다. 이전의 요청, 현재의 요청, 미래의 요청이 서로 관련이 없음을 뜻한다.
  • HTTP 프로토콜을 통해 클라이언트가 인증을 성공하여도 그 상태는 저장되지 않는다. 이를 해결하기 위해 서버 기반 세션 인증을 사용한다. 이것은 서버의 scalability 에도 좋지 못하다.
  • 토큰 기반 인증은 stateless 하다. 유저의 정보를 서버에 저장하지 않고 토큰을 발급하여 클라이언트에 전달한다.

Scalability

  • 클라이언트는 토큰을 받아 저장하고, 서버에 다시 요청할 때 HTTP Header 에 담아 전송한다. 서버는 이것을 디코딩하여 검증한다. 이와 같이 서버는 발급과 검증 두 가지 역할만 할 뿐 직접적인 정보를 저장하지 않는다. 이것은 scale-out 시 여러 대의 서버가 모두 유저의 정보를 저장할 필요가 없다는 뜻이다.
  • 토큰 기반 인증을 사용하면 db 에 heat 을 하거나, scale 방식에 대해 덜 고민할 수 있다.

JWT 구조

  • JWT 는 (.) dots 로 분리된 Header, Payload, Signature 세 파트로 나뉘어진다.
    • xxxxxxx.yyyyyyy.zzzzzz
    • Header
    • Payload
    • Signature

Header

  • Header는 토큰 타입과 해시 암호화 알고리즘으로 구성되어 있다.
  • HMAC SHA256 또는 RSA 와 같은 알고리즘과 토큰 타입인 JWT 가 JSON 형태로 담겨 있다.
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

  • Payload는 claim 정보를 포함하고 있다.
  • 클레임은 정보의 한 조각이며, key-value 형식으로 이루어진 한 쌍의 정보를 뜻한다.
  • 클레임은 사용자의 정보 또는 데이터 속성 등을 나타낸다. userId, expire, scope 등이 여기 해당한다.
  • 클레임은 등록된 클레임 (Registered Claim), 공개 클레임 (Public Claim), 비공개 클레임 (Private Claim) 세 가지 타입으로 구분할 수 있다.
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

등록된 클레임 (Registered Claim)

  • 이미 정의된 클레임이다. 이 클레임은 의무는 아니지만 상호유용성 때문에 추천하고 있다.
  • iss : Issuer 토큰 발급자
  • sub : Subject 토큰 제목
  • aud : Audience 토큰 대상자
  • exp : Expiration Time 토큰 만료 시각
  • nbf : Not Before 토큰의 활성 시각 → 이 시각 전에는 토큰이 유효하지 않다는 의미이다.
  • iat : Issued At 토큰이 발급된 시각 → 이 값으로 토큰이 발급된지 얼마나 오래됐는지 확인할 수 있다.
  • jti JWT ID. JWT 의 식별자

공개 클레임 (Public Claim)

  • JWT 사용자들에 의해 정의되는 클레임이다.
  • 충돌을 피하기 위해 IANA JSON Web Token Registry 에 직접 클레임을 등록하거나, 충돌 방지 네임스페이스를 포함한 URI 형태로 정의해야 한다.

비공개 클레임 (Private Claim)

  • 서버와 클라이언트 사이에만 협의된 클레임이다.

Signature

  • 시그니처를 만들기 위해 인코딩된 헤더, 인코딩된 페이로드, 서버의 secret 키, 지정된 암호화 알고리즘을 가져와 서명해야 한다.
  • Signature 는 서버의 secret key 를 포함하여 암호화 한다.
  • 시그니처는 서버 측에서 관리하는 비밀키가 유출되지 않는 이상 복호화가 불가능하다. 따라서 메시지가 도중에 변경되지 않았는지 검증하며, 개인키로 서명된 토큰의 경우 발신인이 누군지 검증할 수 있다.
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)