이전 포스트에서는 회원가입과 로그인 API를 만들었다.
이제 유저가 로그인이 필요한 API를 요청할 때에, 해당 유저가 로그인이 되었는지 안 되었는지를 확인하는 기능이 필요하다.
우선 다루고 있는 것이 인증 과 인가 에 대한 부분이니만큼 둘의 차이에 대해 인지하고 있어야한다.
회원가입 / 로그인은 인증에 관한 기능을 한다. 유저는 회원가입 / 로그인을 통해 자신이 이 서버의 회원임을 인증할 수 있다.
하지만 유저가 권한이 필요한 기능에 접근하려고 할 때마다 로그인을 통해 인증을 해야한다면 UX는 저하된다. 생각해보라, 보통 로그인을 한 번 하고 나면 권한이 필요한 기능에 접근할 때에 다시 로그인을 하지 않아도 접근이 가능했지 않은가? 반면 매번 로그인을 해야한다면 상당히 불편할 것이다.
이것이 어떻게 가능할까? 그것은 클라이언트든 서버든 기능에 접근하려는 유저가 권한을 이미 가지고 있음을 알고 있기 때문이다. 즉, 이 유저가 인가 되었음을 알고 있기 때문이다.
그리고 JWT는 인가에 관한 기능을 한다.
HTTP 통신은 Connectionless이며 Stateless다. 무슨 말인고 하면 서버는 어떤 요청에 대해 응답하고 나면 그걸로 연결이 종료되며, 요청을 보내온 상대를 식별할 수 없다.
식별을 하기 위해서는 클라이언트가 인증을 하면 서버에서 해당 클라이언트를 식별할 수 있는 일종의 ID를 주고, 클라이언트에서는 인가가 필요한 API에 접근할 때에 해당 ID를 서버에 전송함으로써 인가된 클라이언트임을 서버에 알리는 것이 현재의 보편화된 인가 방식이다.
과거에는 인가를 위해서 세션을 보편적으로 사용했었다.
잠시 세션은 어떤 원리로 인가를 하는지 가볍게 살펴보자.
이후에 인가를 취소하는 요청(예를 들면 로그아웃)이 들어오면 해당 Session ID를 지운다. Session ID가 저장된 메모리에는 현재 인가된 상태의 유저들의 Session ID가 저장되어 있는 것이다. 그래서 실시간으로 인가된 유저들을 관리하기에 용이하다.
허나 Session ID를 서버 메모리에 저장한다는 것이 치명적인 단점이다.
이런 단점들을 해결하기 위해 나온 것이, 바로 JWT다.
큰 흐름은 세션과 같다.
세션 방식과 다른 점은 서버에 뭔가 저장하지 않는다는 것이다. 이것이 어떻게 가능한지 알아보자.
JWT는 Header, Paylaod, Signature로 구성되어있고, 각 요소는 . 으로 구분된다.
실제 JWT의 모습이다.
Signature를 제외한 각 요소는 JSON 객체이다. 이렇게 문자열의 형태를 띄고 있는 것은 base64 방식으로 인코딩되었기 때문이다.
이 문자열의 인코딩 되기 이전의 모습은 이렇다.
이제 각 구성요소가 갖는 의미에 대해 알아보자.
JWT의 Header에는 해싱 알고리즘 방식을 명시하는 alg와 토큰 타입을 명시하는 typ가 들어있어야 한다.
Payload는 data를 담는 곳이다. 그리고 이 부분이 JWT가 서버에 정보를 따로 저장하지 않고 사용자를 식별하고 HTTP 통신 환경을 개인화 할 수 있는 핵심적인 부분이다.
Payload에 담기는 Key-Value 한 쌍을 Claim이라고 부른다.
그리고 Claim에는 세 종류가 있다.
Signature는 Header와 Payload를 각각 base64 방식으로 인코딩 하고 하나의 문자열로 합친 다음 임의의 Sceret key를 사용해 HMAC SHA-256 방식으로 암호화한 값이다.
그래서 Payload의 내용이 한 끗만 바뀌어도 Signature값은 송두리째 바뀐다.
JWT는 Token을 검증할 때에 Header와 Payload를 해당 Token을 발급할 때 사용했던 Secret key를 이용해 Signature 값을 만들고, 이 값과 해당 Token의 Signature 값이 일치하지 않는다면 검증은 실패한다.(나는 처음에 이 과정을 잘 이해 못 했었는데, 어떻게 잘 설명이 되었길 바란다.)
그러니 악의적인 유저가 Token의 Payload를 decode해서 임의로 값을 바꾼 다음 encode하는 둥의 조작을 가해도 Secret key를 모른다면 Signature 값을 일치시킬 수 없어 검증을 통과할 수 없다.
이렇듯 JWT는 Token 자체에 데이터가 모두 담겨있기 때문에, 서버에 따로 정보를 저장해두지 않아도 요청을 보낸 사용자에 대해 어느정도 식별이 가능하다. 이것이 세션 방식과의 차이점이다.
클라이언트에서 서버로 Token을 전달 할 때에 사용할 수 있는 경로가 3개 정도 있다.
보통 Cookie 혹은 Authorization header에 Token을 담아 요청을 보낸다.(항상 그렇지만 내가 모르는 방법이 있을 수도 있다. Authorization header의 존재도 이 포스트를 쓰다가 알았다.)
JWT를 사용할 때에 Access token과 Refresh token을 발급하고 Access Token의 만료기간을 1시간 혹은 그보다 더 짧게 설정하고 Refresh token의 만료기간은 1주 혹은 2주 정도로 설정한다.
그리고 서버에서는 Access token이 만료되었으나 Refresh token이 유효하다면 Access token을 다시 발급해주는 로직을 짜놓는다.
이렇게 하면 Access token이 탈취당해도 만료기간이 짧으므로 해킹으로 인한 피해를 줄일 수 있다는 이야기이다. 물론 Refresh token이 탈취당하면 그게 그거 아닌가 싶긴 하지만..
아무튼 갑자기 Access token과 Refresh token을 설명하는 이유는 두 Token의 저장 위치를 달리하는 방법을 설명하고 싶어서다. Access token은 Authorization header에, Refresh token은 Cookie에 저장하는 방법이다. 자세한 시나리오를 보자.
서버에서 인증된 유저에게 응답할 때에 Access token은 request.body로, Refresh token은 Cookie로 발급한다. 클라이언트는 Access token를 받으면 인가가 필요한 API 요청의 Authorization header에 즉시 삽입한다. Refresh token은 Cookie에 저장되어 있으니 자동으로 요청에 포함된다. 그러므로 Access token이 만료되거나 사용자가 브라우저를 껐다 키거나 새로고침 해서 Access token이 사라진다고 해도 Refresh token에 의해 Access token이 재발급 된다.
처음에는 "로컬 변수에 Token을 저장하면 JS코드로 접근 가능한 거 아닌가?" 싶었는데, 이런 보안에 민감한 부분을 다루는 웹을 순수 JS로 만들 리는 없고, 순수 JS를 사용한다 하더라도 클로저로 변수를 보호하면 되고, React나 Vue같은 프레임워크를 사용한다면 JS코드로 변수에 접근할 수 없으니 그 부분은 안전하다고 생각한다.
보안에 관한 부분은 여기서 다루자면 끝이 없을 것 같다. JWT 사용하기 편에서 어떤 보안책을 적용했고 왜 그랬는지 이야기를 해 볼 생각이다.
솔직히 이렇게 길어질 줄 몰랐다! 코드는 보안에 관련되기만 하면 배워야 할 게 너무 많아진다.
이 포스트를 쓰기전에는
여기까지 구현하고 끝인줄 알았다. 근데 포스팅하려고 알아보기 시작하니 너무 빙산의 일각만을 알고 있었다.
다음 포스트에선 실제로 서버에 JWT를 적용시키는 부분을 다룰 예정이다.
이틀 전에 면접을 봤다. 처음 보는 면접이라 긴장을 했는지 잘못된 대답을 두 어개 정도 했다... 면접 내용을 복기하고 있으려니 기술적인 질문도 그렇고 인적인 질문에도 내가 이상하게 대답을 했었다는 걸 깨달아서 내가 긴장을 하긴 했구나 싶었고, 다음에 면접을 볼 때는 정신을 더욱 집중해야겠다 느꼈다.
결과는 어땠냐고? 떨어졌다! 자가진단을 하기로는 아무래도 포트폴리오에 문제가 있다고 생각한다. 포트폴리오 하나하나의 개발기간이 되게 긴데, 왜 길어졌는지 정확히 설명을 하지 못 하기도 했고 나 자신을 모두 포트폴리오에 녹이지 못 했다고 생각한다. 질문에 이상하게 답을 한 것도 원인이라고 생각한다.
아쉽지만 어쩌겠는가! 그럼에도 앞으로 나아가야한다. 하지만 구직기간이 너무 길어져서 단기 아르바이트라도 병행해야겠다 싶다.