인증 : Authentication. 쉽게 말해 로그인이라 생각하면된다.
인가 : Authorization. 인증을 받은 사용자가 서비스 안에 돌아다닐 때 허가를 해주는 것이다.
사용자가 로그인에 성공하면 세션
을 붙여준다. 영화관 티켓을 생각하면 되는데, 반은 서버 본인이 가지고 있고, 반은 사용자에게 준다.
브라우저는 이 표를 Session ID
란 이름의 쿠키로 저장하고, 이 브라우저는 다음 사이트에 요청을 보낼 때마다 이 표딱지를 실어보낸다.
서버에 로그인이 되어있음이 지속되는 이 상태를 '세션'이라고 한다.
서버 입장에서는 세션을 메모리에 들고 있을 수도 있고, 하드디스크나 DB에 놔둘 수도 있다. 하지만 메모리에 놔두면 만약 문제가 생겨서 서버가 재부팅되어야한다면 메모리가 다 날라가서 사용자들이 로그인이 다 튕긴다. 하지만 메모리에 있는게 빠르긴하다.
서버가 여러 대인 경우에도 복잡한데, 만약 로그인은 1번 서버에 연결해서 하고, 그 다음 이메일 페이지로 가는 요청은 3번에게 하면은 세션 유지가 잘 안된다. 그렇다고 한 유저가 특정 한 서버에만 접속하게 하는 것도 까다롭다.
그래서 공용 창고인 데이터베이스 서버에 넣어두거나 (많이 느리다), 더 흔히는 레디스나 MemCached 같은 메모리형 데이터베이스 서버에서 같이 쓰기도 한다.
이러한 부담 없이 인가를 구현하기 위해 설계된 것이 토근방식
JWT이다.
Json Web Token
사용자가 로그인을 하면 찢어서 주지 않고 그냥 준다. 즉 서버가 무엇을 기억하지 않는다. 인코딩된 데이터 3개를 붙인 것이고 ~.~.~
의 형식이다.
각각 header
, payload
, veryfiy signature
로 구분된다.
이걸 Base64로 디코딩해보면 JSON 형식으로 여러 정보들이 들어있다. 이 토큰을 누가 누구에게 발급했는지, 이 토큰이 언제까지 유효한지, 그리고 서비스가 사용자에게 이 토큰을 통해 공개하기 원하는 내용 (예를 들어 사용자의 닉네임이나 서비스상의 레벨, 관리자 여부)등을 서비스 측에서 원하는 대로 담을 수 있다. 이렇게 토근에 담긴 사용자 정보 등의 데이터를 Claim이라고 한다.
정리하면 사용자가 로그인을 하고 나서 받는 토큰에 이 정보들이 클레임이라는 것으로 실려온다. 그게 이 후 요청마다 이번에는 사용자로부터 서버한테 보내진다. 사용자가 받아서 갖고 있는 토큰 자체에 이런 정보들이 들어있으면 서버가 요청마다 일일이 데이터베이스에서 찾아야하는 것이 준다.
근데 base64로만 인코딩되어있으면 사용자가 이것을 악용할 수 있다. 그래서 header
와 verify signature
가 있다.
처음 서버에서 토큰을 발급할 때 나중에 DB를 안찔러도 되게 필요한 정보들을 payload에 담아서 보낸다. 근데 이 payload를 파싱하는 것은 secretkey가 없어도 할 수 있다. 따라서 프론트에서도 이것을 파싱해서 데이터를 볼 수 있다!
두 가지 정보가 담겨있다.
type
여기에는 언제나 JWT
가 들어간다. 고정값이다.
alg
알고리즘의 약자인데 3번 서명 값을 만드는데 사용될 알고리즘이 지정된다. HS256
등 여러 암호화 방식 중 하나를 지정할 수 있다.
1번 헤더와 2번 페이로드, 그리고 "서버에 감춰놓은 비밀 값" 이 셋을 암호화 알고리즘에 넣고 돌리면 3번 서명 값이 나온다.
토큰들을 탈취해도 서버의 "비밀값"을 알지 못하면 무용지물이다.
서버는 요청이 들어오면 header와 payload의 값을 "비밀 키"와 함께 돌려서 계산된 값이 verify signature와 맞는지 확인한다.
이렇게 되면 서버는 사용자들의 상태를 어디다가 따로 기억해둘 필요 없이, 이 비밀 값만 손에 쥐고 있으면은 요청들 들어올 때마다 토큰 삑삑 스캔해서 사용자들을 거러낸다.
이처럼 시간에 따라 바뀌는 어떤 상태값을 안 갖는 것을 Stateless
라고 한다. 세션은 반대로 stateful
이다.
그럼 세션보다 JWT가 우월한 것일까? 그렇지 않다. JWT의 단점이 존재한다.
세션처럼 stateful해서 모든 사용자들의 상태를 기억하고 있다는 건 구현하기 부담되고 고려사항도 많지만, 되기만 하면 기억하는 대상들의 상태들을 언제든지 제어 가능하다.
예를 들어 PC에서 로그인한 상태의 어떤 사용자가 핸드폰에서 또 로그인하면 PC에서는 로그아웃하도록 기존 세션을 종료할 수 있다. 세션에서는 서버에 있는 표딱지를 버리면 되는데, JWT에서는 불가능하다. 이미 줘버린 토큰을 뺏을 수도 없고, 그 토큰의 발급 내역이나 정보를 서버가 어디 기록해서 추적하고 있는 것이 아니기 때문이다.
이 한계를 부분적으로 보완하는 것이 로그인하면 두 토큰을 주는 것이다. 수명이 몇 시간이나 몇 분이하로 짧은 access
토큰이랑, 꽤 길게 보통 2주 정도로 잡혀있는 refresh
토큰이다. access
토큰과 refresh
토큰을 발급하고 클라이언트에게 보내고 나서, refresh
토큰은 상응 값을 데이터베이스에도 저장한다. 손님은 access
토근의 수명이 다하면 refresh
토큰을 보낸다. 서버는 그걸 데이터베이스에 저장된 값과 대조해보고, 맞다면 새 access
토큰을 발급해준다. 이제 이 refresh
토큰만 안전하게 관리된다면 access
토큰이 만료될 때마다 다시 로그인할 필요가 없다. 이러면 중간에 access
토큰이 탈취당해도, 오래 쓰지 못한다. 누구를 강제 로그아웃 시키려면 refresh
토큰을 DB에서 지우면 된다. 이렇게 되면 access
토큰을 짧게 밖에 쓰지 못한다.
url에서 파싱을 한 후 base64라는 인코더를 이용해서 인코딩 한 후 요청 헤더에 넣어서 보내준다.
지금과 같은 상황이면 매번 인증을 해야한다는 문제가 있다.
정확히는 browser의 storage를 사용하는 것인데, 스토리지는 로컬 스토리지, 세션 스토리지, 쿠키 스토리지가 될 수 있는데, 여기서는 쿠키를 생각해보자.
쿠키에 사용자 정보를 넣어놓고 인증이 필요할 때 쿠키를 보내는 방법이 있다. 하지만 이렇게되면 보안에 취약하다. 클라이언트가 raw한 데이터를 가지고 있는데, 클라이언트는 서버보다 보안에 취약하다.
세션을 인증된 사용자의 식별자와 랜덤한 문자열로 session id를 만든다. 이것을 응답의 header로 넘겨주고 클라이언트가 이를 저장한다. 사용자가 raw한 데이터를 가지고 있지 않기 때문에 보안이 낫다. 또한 세션의 만료 기간을 설정할 수 있다는 것도 장점이다.
서버가 여러개가 되면 다른 서버에서 세션 인증을 못받을 수도 있다. 이런 경우
세션 스토리지
를 둬서 해결을 한다. 즉 세션들을 한 곳에서 관리하는 것이다.
하지만 이것 역시 클라이언트가 많아지면 터진다.
Stateful
HTTP와 서버가 지향하는 REST API는 무상태성을 기초로하는데, 인증과 인가에는 상태를 가지고 있으므로 두 패러다임이 충돌하고 있다.
서버는 클라이언트로부터 온 토큰 유효성을 본인이 가진 시크릿키로 검사를 한다.
그럼에도 중요한 정보는 담으면 안된다. 디코딩하기가 쉽다.
장점은 세션 디비가 없어도 각자의 서버에서 해결이 가능하다. 즉 서버가 확장되어도 기존의 방식대로 하면 된다는 장점이 있다.
토큰이 탈취당하면 해커도 똑같은 지위를 가지게 된다.
만료기한이 지나면 해커도, 사용자도 사용하지 못하게 된다.
처음 요청을 받으면 access token과 refresh token을 한번에 만든다. 시간이 지나서 access 토큰이 만료되면, refresh token을 참조해서 새로운 access token을 보낸다.
토큰으로 상태관리를 하기에 따로 세션을 둘 필요가 없다. 하지만 토큰도 관리를 해야한다. 결국 토큰도 탈취당할 수 있다.
처음 서버에서 토큰을 발급할 때 나중에 DB를 안찔러도 되게 필요한 정보들을 payload에 담아서 보낸다. 근데 이 payload를 파싱하는 것은 secretkey가 없어도 할 수 있다. 따라서 프론트에서도 이것을 파싱해서 데이터를 볼 수 있다!