Post

real-world프로젝트 Spring 시작하기(6) - Jwt Filter 만들기 전 JWT,쿠키,세션 설명

real-world project 구현 과정 카테고리 보기

개요

회원가입 하나 만들기 위해서 해야될 게 참 많다.

사용자가 비밀번호를 입력해서 로그인을 한다고 치자.

HTTP 프로토콜은 stateless의 특성을 가지고 있어서 서버와의 통신이 끝나면 연결이 끊어져버린다.

HTTP1.1 프로토콜은 keep-alive를 통해서 어느정도 요청시간을 유지할 수 있지만 무한정 연결을 걸어놓고 있을수도 없는것이다.

그래서 클라이언트나 서버에 사용자에 대한 인증 정보를 담아놓는데, 이는 쿠키세션으로 가능하다

이 프로젝트에서는 당연하게도 Jwt토큰을 쓰라길래 썼지만 왜 쓰는지는 알아둬야 하는것이다.

쿠키와 세션이 있는데 jwt를 쓰는 이유는 당연히 저 둘과 다른 차별점과 얻는 이점이 있기 때문이다. 그 전에 쿠키와 세션에 대해 간단히 복기겸 정리를 해보았다.

쿠키

쿠키란 HashMap처럼 키-값을 가지는 정보이다.

1
2
키             값
test_cookie = 1234

쿠키를 확인하는 방법은 간단하다. 브라우저에서 개발자도구를 켜서 상단 메뉴탭에 Application -> Storage -> Cookies에 들어가면 된다.

이를 통해서 사용자 인증을 할 수가 있다.

로그인을 성공하면, 클라이언트에 응답을 줄 때 서버에서는 고유한 쿠키를 만들고 헤더에 포함시킨다.

그 이후 클라이언트는 이 쿠키값을 브라우저에 저장하고 요청때 request에 포함시켜 서버에서는 이를 통해 클라이언트를 식별한다.

이러한 방식은 쿠키의 값을 암호화하지 않고 보낼 수도 있기에 보안에 취약하고, 브라우저에 저장하기에 네트워크 부하도 있다. 그리고 브라우저간 쿠키에 대한 지원 형태가 달라서 브라우저간 공유가 안되게 된다.

세션

위 그림을 보는게 편하다. 순서대로

  1. 사용자가 로그인을 시도하면
  2. 서버에서 요청을 받고 DB에서 비교 (비밀번호를 확인한다던지)
  3. 사용자 식별이 되었으면 세션저장소에서 회원정보가 포함된 SessionID를 발급.
  4. 클라이언트에 SessionID를 발급하면서 로그인 성공을 알림.
  5. 클라이언트는 SessionID를 쿠키에 저장하고, 매 요청(request)마다 해당 쿠키를 헤더에 포함해서 요청을 보냄.
  6. 서버에서는 요청을 받고 쿠키값을 뜯어봐서 있는 SessionID를 검증. 즉, 서버에는 세션을 관리하는 세션저장소가 있어야함.
  7. 검증이 성공하면 클라이언트에 응답을 보내줌.

쿠키와 마찬가지로 세션의 단점은 세션을 관리해야하는 저장소가 필요한 것이다. 사용자 수가 많으면 많을수록 부하가 심해질것은 당연하고, 해커가 중간에 가로챌 수도 있다.

jwt

토큰 기반 인증 방식이라고 불리는 jwt는 다르다. 일단 서버에 이를 관리하는 저장소가 필요없다. 일반 jwt단점을 보완시킨 refreshToken을 쓴다면 서버에 저장소가 필요하긴 하다.

일단 jwt는 JWT header, JWT payload, signature라는 세개가 포함된다.

1
xxxxx.yyyyy.zzzzz

x는 header, y는 payload, z는 signature라고 생각만하자. 왜냐하면 저 값들은 나중에 인코딩 되기 때문에 형식을 지금 딱 정할 수는 없는 값이기 때문이다.

  • header

토큰 타입과 사용되는 알고리즘에 대한 정보를 담는다.

1
2
3
4
{
  "alg": "HS256",
  "typ": "JWT"
}
  • payload

claims이라는 것을 포함하는데, Claims이란 user같은 객체와 추가적인 데이터를 포함한 키-값 데이터라고 할 수 있다. 이렇게 하면 뭔 소린지 모를것이다.

Claims에는 3종류가 있다. Registered Claim, Public Claim, Private Claim.

Registered Claim은 **토큰 정보를 표현하기 위한 이미 정해진 종류의 데이터다.

  • iss: 토큰 발급자(issuer)
  • sub: 토큰 제목(subject)
  • aud: 토큰 대상자(audience)
  • exp: 토큰 만료 시간(expiration), NumericDate 형식으로 되어 있어야 함 ex) 1480849147370
  • nbf: 토큰 활성 날짜(not before), 이 날이 지나기 전의 토큰은 활성화되지 않음
  • iat: 토큰 발급 시간(issued at), 토큰 발급 이후의 경과 시간을 알 수 있음
  • jti: JWT 토큰 식별자(JWT ID), 중복 방지를 위해 사용하며, 일회용 토큰(Access Token) 등에 사용

내가 써본것은 iss, sub, exp 정도이다.

그리고 이것들의 공통점은 3글자라는 것이다.

Public Claim은 사용자가 정의한 Claim의 종류로 충돌 방지를 위한 URL형식을 사용하라고 한다.

1
2
3
{
    "kkminseok.github.io" : true
}

Prviate Claim은 사용자 정의 Claim이지만 서버와 클라이언트 사이에 임의로 지정한 정보를 저장한다.

1
2
3
{
    "token_type": access 
}

이러면

1
2
3
4
5
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

이렇게 생긴건 어느정도 구분이 될 것이다.

  • Signature(서명)

이건 앞에서 설명한 header와 payload를 BASE64URL로 인코딩 한 뒤, 이를 사용자가 정의한 비밀 키를 이용하여 헤더에 정의한 알고리즘으로 해싱하고 사용자가 설정한 암호화 방식으로 암호화해버린다.

만약 HMAC SHA256 방식으로 암호화할 것이라면 Signature는 다음과 같이 생성될 것이다.

1
2
3
4
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

이를 통해 실제 토큰을 만들면

‘eyJhbGciOiJIUzM4NCJ9.eyJlbWFpbCI6InRlc3RAbmF2ZXIuY29tIiwiaWF0IjoxNjY1OTMwOTgxLCJleHAiOjE2Njg5MzA5ODF9.CnTX7jJ0gG9R7AhJ14YRSYN9k8jBqn8V9rtVJDAsQRQj0hOCgicuG3qBGI3Ovfha’

이런 값이 랜덤으로 탄생할 것이다.

Header부분과 Payload부분은 단순히 인코딩된 값이기에 3자가 복호화 및 조작이 가능하지만, Signature 부분은 비밀 키만 누출되지 않는다면 복호화가 불가능하다는 특징을 가지고 있다.

그래서 Payload 부분에는 유저에대한 중요한 정보를 넣지 않는게 중요하다.

https://jwt.io/#debugger-io에 들어가서 위에 적힌 토큰을 그대로 넣어 디코드 해보면…

이렇게 email이라는 고객정보가 떠있다.

이렇게 생성된 token을 이용하여

1
Authorization : Bearer <token>

와 같은 형식을 헤더에 포함해서 로그인 성공할때 등 토큰을 서버에서 클라이언트로 보낸다.

클라이언트에서는 이 토큰을 쿠키에 저장하던지, 로컬 스토리지에 저장하던지 저장하고 가지고 다녀서 서버에 요청할 때 Authorization 헤더를 통해 보낸다. 서버는 이 토큰을 복호화하여 유효기간, 사용자 정보를 매칭하고 매칭되었다면 요청에 응답한다.

이러한 토큰을 통해서 서버에 인증관련 저장소가 필요가 없어졌고, Signature를 통해 데이터 위변조를 막을 수 있게 되었다.

하지만 이러한 토큰도 결국 탈취되어버리면 대처가 어려워진다.

보통 토큰의 유효기간을 짧게는 가져가지 않기에 해커가 활동할만한 시간을 주어준다.

심지어 이 토큰은 폐기가 불가능하다.

이러한 단점을 제거하기 위해 Refresh Token이 생겨났다.

이 프로젝트에서는 Refresh Token을 사용하지 않기에 자세한 설명을 적지는 않겠다.

Refresh Token는 기존 Token의 유효시간을 짧게 가져가고 유효시간이 길고 서버에서 관리하는 Refresh Token을 둔다.

문제는 이 Refresh Token도 탈취 당할 수 있다는 것이다.

이는 RTR 기법이 사용된다고 한다.

Refresh Token을 한 번 쓰면 다음 refresh Token을 발급하여 탈취되어 다시 한 번사용되면 탈취된 것으로 간주하고 탈취된 것으로 간주된 모든 Refresh Token을 폐기한다.

이를 감지하기 위해선 Token Chain으로 연결하고 관리한다고 한다.

결론

해당 프로젝트에서는 JWT Token을 이용하여 사용자를 인증, 인가를 하므로 더 깊이까지는 정리하지 않았다.

하지만 refreshToken에 대해서는 알아두는 게 좋다.

Reference

현재 이 프로젝트는 vue.js + vite + vuex + Spring boot, Spring Data JPA + Spring Security 를 이용하여 real-world 데모버전을 제작완료하였습니다. https://github.com/kkminseok/real-world-springboot

This post is licensed under CC BY 4.0 by the author.