JWT란
JSON Web Token의 약자로, 두 개체에서 JSON 객체를 사용하여 가볍고 자가 수용적인(self-contained) 방식으로 정보를 안전성 있게 전달해준다. 자가수용적이라는 것은 필요한 모든 정보를 자체적으로 지니고 있다는 뜻이다. JWT 시스템에서 발급된 토큰은 토큰에 대한 기본정보, 전달할 정보 (로그인 시스템에서는 유저 정보) 그리고 토큰이 검증됐다는 것을 증명해주는 signature를 포함한다. JWT를 만들때는 JWT를 담당하는 라이브러리가 자동으로 인코딩 및 해싱 작업을 해준다.
- 다양한 프로그래밍 언어에서 지원
- 자가 수용적(self-contained). 사용자 인증에 필요한 모든 정보를 토큰 자체에 포함하기 때문에 별도의 인증 저장소가 필요없음.
- 쉽게 전달 될 수 있음. 두 개체 사이에서 손쉽게 전달 될 수 있다. 웹서버의 경우 HTTP의 헤더에 넣어서 전달할 수도 있고, URL 파라미터로 전달할 수도 있다.
‘인증(Authentication)‘이란, 등록된 사용자인지 확인하는 과정이고, ‘인가(Authorization)‘는 권한이 있는 사용자인지 확인하는 과정이다. http 특성 때문에 인증, 인가 과정은 꼭 필요하다. http는 요청(request)이 들어오면 응답(response)하는 구조인데, 응답 이후 상태가 저장되지 않기 때문에(stateless) 요청을 보낸 사용자가 등록된 사용자인지, 요청한 작업 권한을 가지고 있는지 검증해야한다.
서버 기반 인증 vs. 토큰 기반 인증
JWT와 같은 토큰으로 사용자를 인증/인가하기 전에는 서버 기반으로 사용자를 인증했다. 인증에 필요한 정보를 모두 서버에 담고 있다가 클라이언트에서 요청이 왔을 때 응답하는 것.
서버 기반 인증 방식은 서버를 확장하기 어렵다는 큰 단점이 있다. 세션을 서버에 저장하고 있고, 서버를 여러 대 사용하여 요청을 분산한 상황이라고 가정해보자. 어떤 사용자가 로그인 했을 때 그 사용자는 처음 로그인 했던 그 서버, 즉 그 사용자 정보가 담긴 서버에만 요청을 보내도록 설정을 해야한다. 그렇지 않으면 사용자 정보를 불러올 수 없기 때문이다.
반면 토큰 인증 방식은 어떤 서버로 요청이 들어가든 상관없다. 서버에 세션이 존재하지 않으니 사용자의 로그인 상태를 신경쓰지 않아도 되며 이는 곧 서버를 쉽게 확장할 수 있다는 이야기가 된다.
JWT를 이용한 인가 방식
Refresh token과 access token 두 가지를 사용한다는 전제 하에 JWT는 다음과 같이 동작한다.
- 클라이언트에서 로그인 요청(post)
- 서버에서, 요청이 들어온 로그인 정보와 DB에 저장된 유저 정보와 비교
- Access token을 발급하여 클라이언트에 JWT를 반환
- 클라이언트에서 Access Token을 세션에 갖고있다가 인가가 필요한 요청(댓글 쓰기, 회원 정보 수정 등)을 할 때 Header에 담아 전달
- Access Token이 만료됐다면 서버에 있는 Refresh Token이 유효한지 확인 후 Access Token 재발급
- 재발급한 Access Token을 헤더에 담아 다시 서버에 요청
- 서버에서 토큰과 맞는 유저 정보가 있는지 확인 (디코딩 과정)
- 찾은 유저 정보와 요청한 유저가 일치하는지 확인.
- 일치하면 Request에 맞는 Response를 반환하고, 만료된 토큰이거나 잘못된 토큰이면(일치하는 유저가 없으면) 401 Unauthorized 리턴
JWT 구조
- 헤더(header) : JWT의 헤더는 타입과 알고리즘을 지정하고, BASE64 인코딩 되어 가장 맨 앞에 위치한다. 토큰 타입과 해시 알고리즘 종류 정보가 들어간다.
- 내용(payload) : JWT의 페이로드는 공개 클레임(claim. 정보의 한 단위, key/value 한 쌍을 의미)과 비공개 클레임을 작성한 뒤 BASE64 인코딩하여 두 번째 요소에 위치시킨다. 지금 로그인한 사람이 누구인지 정보가 들어가는데, 쉽게 노출되지 않도록 pk키 등 쉽게 식별할 수 없는 정보가 들어가야 한다. (유저 아이디 절대 안 됨) 토큰 유효기간은 claim set이라고도 불리는 페이로드에 저장할 수 있다.
참고) 공개된 클레임 이름은 토큰에서 사용하기 위해서 정의했지만, 충돌을 방지하기 위해서 공개한 이름이고 비밀 클레임이름은 서버와 클라이언트가 협의로 사용하는 이름을 의미
- 서명(signature) : JWT의 시그니처는 인코딩된 헤더와 페이로드를 합쳐 별도 지정된 secret key를 이용해 헤더에 지정된 알고리즘으로 암호화하여 생성한다. 프론트엔드에서 JWT를 백엔드 API 서버로 전송하면 서버에서는 전송받은 JWT의 서명 부분을 복호화하여 서버에서 생성한 JWT가 맞는지 확인한다. 계약서 위변조를 막기 위해 서로 사인하는 것과 같다.
JWT 사용 시 주의사항
JWT 페이로드 부분에는 사용자를 특정할 수 있는 정보가 들어가면 안 된다. 페이로드는 사용자를 식별할 수 있는 정보를 담고 있는데, 암호화가 아닌 단순 BASE64 인코딩으로 만든다. 누구나 쉽게 디코딩할 수 있기 때문에 아이디나 이메일 등 사용자 개인정보를 페이로드에 담아 인코딩한다면 개인정보가 유출될 우려가 있다. 따라서 DB에 저장된 유저의 PK값 같이 디코딩된 정보만 봐서는 어떤 사용자인지 알 수 없는 정보를 담아야 한다.
참고) payload에 들어가는 클레임 설명 (필수 아님. 모두 선택사항)
iss
: 토큰 발급자(issuer)sub
: 토큰 제목(subject). 토큰이 갖는 문맥aud
: 토큰 대상자(audience). 토큰을 사용할 수신자exp
: 토큰의 만료시간(expiraton), 시간은 NumericDate 형식으로 되어있어야 하며 (예: 1480849147370) 언제나 현재 시간보다 이후로 설정해야 함nbf
: Not Before를 의미하며, 토큰의 활성 날짜와 비슷한 개념. 여기에도 NumericDate 형식으로 날짜를 지정하며, 이 날짜 이전에는 토큰을 처리하지 않아야 함을 의미iat
: 토큰이 발급된 시간(issued at), 이 값으로 토큰의 age가 얼마나 되었는지 판단jti
: JWT의 고유 식별자로, 중복 처리를 방지하기 위하여 사용. 일회용 토큰에 사용하면 유용
JWT 유효기간 설정
from datetime import datetime, timedelta
token = jwt.encode(
{
'login_id' : data['login_id'],
# 토큰 만료시간 3일 뒤로 설정
'exp' : datetime.utcnow() + timedelta(days=3)
}, SECRET_KEY, algorithm = 'HS256')
datetime.utcnow()
현재 시간 불러오는 메서드timedelta
특정 시간을 더하고 뺄 때 사용하는 메서드timedelta
는 인자로seconds
,hours
,days
,weeks
를 받고,=
를 넣어서 시간을 표현
month
, year
는 다른 방법으로 설정해야한다.
from dateutil.relativedelta import relativedelta
exp = datetime.utcnow() - relativedelta(months=3)
Access token은 탈취될 가능성이 높으므로 보통 30분~2시간, refresh token은 아예 유효기간을 두지 않는 경우도 있고 한 달~3개월로 두는 곳도 있다.
JWT 변조 공격 대처
대표적 JWT 변조 공격은 Signature Stripping인데 헤더의 alg
클레임을 None
으로 변조하는 공격으로, 일부 JWT 라이브러리들이 alg
가 None
인 토큰을
유효한 토큰으로 인식하는 문제가 있다. 또는 웹 게시판 등에 사용자가 입력한 값이 DB에 저장되고, 프론트엔드단에 출력하는 구조를 가진 페이지에서 공격자가 <script>
태그를 입력하면 공격이 성립할 수 있다. 이때 일반 쿠키 또는 세션스토리지에 저장한 토큰이 탈취당할 수 있다.
이를 보완하는 방법 첫 번째는 Refresh Token입니다. 사용자가 로그인할 때 Access Token과 함께 Refresh Token을 발급하는 것입니다.
- Access Token : 짧은 시간 내 만료되는 토큰. 사용자의 인증, 인가에 사용되는 토큰. 주로 세션에 저장
- Refresh Token : Access Token에 비해 긴 만료시간(하루 ~ 일주일 등)을 갖는 토큰으로 Access Token 재발급용 토큰. 노출되면 안 되므로 데이터베이스에 저장
클라이언트는 Access Token이 만료되었다는 오류를 받으면 따로 저장해두었던 Refresh Token을 이용하여 Access Token의 재발급을 요청한다. 서버는 유효한 Refresh Token으로 요청이 들어오면 새로운 Access Token을 발급하고, 만료된 Refresh Token으로 요청이 들어오면 오류를 반환, 사용자에게 로그인을 요구한다.
Access Token은 서버에 따로 저장해 둘 필요가 없지만, Refresh Token은 서버에 저장해 Access Token 재발행 시 검증에 활용해야 한다. 그러므로 Refresh Token을 이용한다는 것은 추가적인 I/O(Input/Output) 작업이 필요하다는 의미이며, 이는 I/O 작업이 필요없는 빠른 인증 처리를 장점으로 내세우는 JWT의 스펙에 포함되지 않는 부가적인 기술이다.
그 외에도 토큰 자체를 암호화 하기, HTTPS 통신에서만 쿠키를 전송할 수 있게 하기, 기기 로그인 IP 등을 토큰에 넣는 것도 방법이 될 수 있다.
참고 자료
토큰(Token) 기반 인증에 대한 소개
JWT 토큰 유효시간 설정
REST JWT(JSON Web Token)소개
JWT의 보안적 고려사항