창업 동아리 백엔드 개발을 처음 시작하면서 마주친 첫번째 고비였다. 바로 로그인 구현이였다. 처음이라 백지 상태였어서 도대체 어떻게 해야할지 모르겠어서 구글링을 통해 수많은 래퍼런스를 찾아본 결과 JWT 토큰을 활용하여 구현할 수 있다는 것을 깨달았다.
그때 로그인을 구현하기 위해 내가 공부했던 JWT 토큰과 내가 짠 로직에 대해 간단히 정리해보고자 한다.
그래서 JWT 토큰이란 무엇인가?
JWT는 Json Web Token의 약어로 Json 객체에 인증에 필요한 정보들을 담은 후 비밀키로 서명한 토큰으로, 인터넷 표준 인증 방식이다. 공식적으로 인증(Authentication) & 권한허가(Authorization) 방식으로 사용된다.
JWT를 사용하면 RESTful과 같은 무상태(Stateless)인 환경에서 사용자 데이터를 주고받을 수 있게 된다. 세션(Session)을 사용하게 될 경우에는 쿠키 등을 통해 사용자를 식별하고 서버에 세션을 저장했지만, JWT의 경우에는 토큰을 클라이언트에 저장하고 요청시 HTTP 헤더에 토큰을 첨부하는 것만으로도 단순하게 데이터를 요청하고 응답을 받아올 수 있다.
토큰은 크게 두가지로 구분된다.
1. 엑세스 토큰(Access Token) 말그대로 엑세스, 인가(Authorization)를 하기 위한 토큰으로 대체로 유효기간이 짧다. 검증된 사용자가 어느 정도의 시간동안 재인증을 하지 않고 로그인 상태가 유지될 수 있도록 한다.
전체적인 플로우를 보면,
- 사용자가 로그인을 시도한다.
- DB와 일치하는지 사용자 확인을 한 후 일치하는 회원이 있는지 확인한다.
- 유효한 유저인것이 확인되면 엑세스 토큰을 발급한다.
- 유저에게 응답(Response)으로 엑세스 토큰을 넘겨준다.
- 클라이언트 단에서 엑세스 토큰을 로컬 저장소 등에 저장하고 있다가 인가가 필요한 데이터 요청(Request)을 보낼때 헤더에 담아 서버로 보낸다.
- 서버에서 헤더에서 엑세스 토큰을 검증한다.
- 유효한 엑세스 토큰이라면 요청한 데이터를 응답으로 보낸다.
2. 리프레시 토큰(Refresh Token) 리프레시 토큰은 필수는 아니다. 위에서 설명한 바와 같이 엑세스 토큰만으로도 충분히 Authorization을 구현이 가능한 것을 볼 수 있다. 그렇다면 이 토큰의 존재의 이유는 바로 탈취에 대비하는 것이다.
엑세스 토큰만을 이용한 인증 방식의 문제는 제 3자에게 토큰을 탈취당하게 되면, 토큰의 유효기간이 만료되기 전까지는 막을 방법이 없기 때문이다. 그래서 엑세스 토큰의 유효기간을 짧게 가져가는 것이고 그렇게 되면 로그인을 자주해야하는 단점이 있어 사용자가 불편을 겪게 된다.
이를 해결하기 위해 탈취에 대비하여 엑세스 토큰의 유효기간을 짧게 가져감과 동시에 유저가 굳이 유효시간이 만료될때마다 로그인을 해줄 필요가 없도록 리프레시 토큰(Refresh Token)방식을 사용한다.
바로, 엑세스 토큰이 만료됐을 때 엑세스 토큰을 재발급하기 위한 토큰으로 대체로 유효기간이 길다.
엑세스 토큰의 플로우에서 몇가지가 추가된다. 유저가 로그인을 요청하고 서버에서 엑세스 토큰을 클라이언트로 반환할때 리프레시 토큰을 함께 반환한다. 클라이언트는 해당 엑세스 토큰과 리프레시 토큰을 모두 저장하고 위에서와 같이 인가가 필요한 요청일때 엑세스 토큰을 헤더에 담아 보낸다. 이때, 서버에서 헤더의 엑세스 토큰을 검증했을 때 토큰이 만료됐다고 가정해보자.
토큰이 만료된 경우 서버는 클라이언트에게 만료됐다는 응답을 보내고 클라이언트는 만료된 토큰을 재발급 하기위해 만료된 엑세스 토큰과 리프레시 토큰을 헤더이 실어 서버에게 새로운 토큰 발급을 요청한다. 서버는 엑세스 토큰과 리프레시 토큰을 모두 검증하여 만료되지 않는 리프레시 토큰이라면 새로운 엑세스 토큰을 발급하여 클라이언트에게 반환한다.
JWT 는 .
을 구분자로 3가지의 문자열로 되어있다.
alg
와 typ
이다.alg
: Signature에서 사용되는 해싱 알고리즘 지정. 대표적으로 RS256 또는 HS256이 쓰인다.typ
: 토큰의 타입 지정한다. {
"alg": "HS256",
"typ": "JWT"
}
클레임(claim)
이라고 부르고, 이는 name과 value 의 한 쌍으로 이뤄졌다. 하나의 토큰에는 여러개의 클레임 들을 넣을 수 있다.iss: 토큰 발급자 (issuer)
sub: 토큰 제목 (subject)
aud: 토큰 대상자 (audience)
exp: 토큰의 만료시간 (expiraton), 시간은 NumericDate 형식으로 되어있어야 하며 (예: 1480849147370) 언제나 현재 시간보다 이후로 설정되어있어야 한다.
nbf: Not Before 를 의미하며, 토큰의 활성 날짜와 비슷한 개념입니다. 여기에도 NumericDate 형식으로 날짜를 지정하며, 이 날짜가 지나기 전까지는 토큰이 처리되지 않는다.
iat: 토큰이 발급된 시간 (issued at), 이 값을 사용하여 토큰의 age 가 얼마나 되었는지 판단 할 수 있다.
jti: JWT의 고유 식별자로서, 주로 중복적인 처리를 방지하기 위하여 사용됩니다. 일회용 토큰에 사용하면 유용하다.
{
"id": "dlwntmd5847",
"nick_name": "JuseungL"
}
Base64url
로 인코딩 되어있기때문에 쉽게 복호화할 수 있지만, 시그니처는 키가 없으면 복호화할 수 없기 때문에 보안상 안전하다.Base64url
로 인코딩한 헤더에 Base64url
로 인코딩한 페이로드 거기에다가 또 추가로 process.env.SECRET
을 추가하여 해싱하여 만들었다.https://jwt.io/ 이 사이트를 통해 만든 JWT 토큰을 직접 디코딩해 볼 수 있습니다.
//간단한 인증 후 JWT 토큰 생성
const [queryResult] = await conn.execute(
"SELECT * FROM user_info WHERE id = ?",
[body.id]
);
const userSelectResult = queryResult[0];
if (userSelectResult.usr_pwd === body.usr_pwd) {
const token = jwt.sign(
{
id: userSelectResult.usr_id,
nick_name: userSelectResult.nickname,
},
process.env.SECRET,
{
issuer: "@juseung",
}
);
이 코드를 통해서 JWT 토큰을 생성하였고 Postman을 통해서 요청을 보내서 실제로 토큰을 생성했다. 생성한 토큰을 사이트에 입력하였더니
이렇게 디코딩 결과가 나왔다.