쿠키는 Key-Value 형식의 문자열 덩어리로, 클라이언트가 어떤 웹사이트에 접속할 때 서버가 해당 클라이언트의 웹 브라우저에 작은 기록 정보 파일을 설치합니다. 각 사용자마다 브라우저에 정보를 저장하니 고유 정보 식별이 가능합니다.
먼저 클라이언트가 서버에 요청을 보내면 서버는 클라이언트의 요청에 응답할 때, 클라이언트 측에 저장하고 싶은 정보를 응답 헤더의 Set-Cookie에 담아 보냅니다. 이후 클라이언트는 서버에 요청할 때 마다 저장된 쿠키를 요청 헤더에 담아 보냅니다.
보안
가장 큰 단점으로 보안에 취약합니다. 쿠키의 값을 그대로 보내기 때문에 유출 및 조작될 위험이 있습니다.
용량 제한 및 네트워크 부하
쿠키에는 용량 제한이 있어 많은 정보를 담을 수 없으며 , 쿠키의 크기가 커지면 네트워크에 부하가 심해집니다.
브라우저 공유
웹 브라우저마다 쿠키의 지원 형태가 다르기 때문에 브라우저간 공유가 불가능합니다.
세션 기반의 인증 시스템은 서버측에 사용자들의 정보를 기억하는 방식입니다. 사용자의 정보를 기억하기 위해 메모리나 데이터베이스를 통해 세션을 유지하며, 서비스를 제공할 때 사용합니다. 이러한 인증 시스템은 서버가 클라이언트의 요청을 받으면, 서버가 클라이언트의 상태를 유지해야 하는데 이러한 서버를 StateFul 서버
라고 합니다.
위의 그림처럼 클라이언트가 로그인을 하면, 서버는 세션에 사용자 정보를 저장해두고 세션을 식별하기 위해 Session Id를 클라이언트 쿠키에 저장합니다.
클라이언트는 서버에 요청할 때 마다 Session Id 를 보내 유효성 검증을 한 후 응답을 받습니다. 하지만 이러한 인증 시스템은 하단과 같은 문제점이 있습니다.
세션
사용자가 인증을 할 때, 서버는 인증한 정보를 저장하는 세션을 유지
해야 합니다. 대부분 메모리에 저장되는데 로그인 중인 사용자가 늘어날 경우 서버의 메모리에 부하가 걸리게 됩니다. 이를 피하기 위해 Database에 저장하는 방법도 있는데 이러한 방식 역시 Database 에 무리를 줍니다.
확장성
사용자가 늘어나게 되면 더 많은 트래픽을 처리하기 위해 서버를 확장해야 합니다. 세션을 사용한다면 세션을 분산시키는 시스템을 설계해야 하는데 이러한 과정은 복잡하고 어렵습니다.
CORS
웹 어플리케이션에서 세션을 관리할 때 자주 사용되는 쿠키는 단일 도메인 및 서브 도메인에서만 작동하도록 설계되어 있습니다. 따라서 여러 도메인에서 쿠키를 관리하기가 어렵습니다.
보안
쿠키를 포함한 요청이 외부에 노출되더라도 유의미한 정보를 담고 있지 않지만, 세션 ID 를 탈취해서 클라이언트인척 위장할 수 있는 한계가 있습니다.
토큰 기반의 인증 시스템은 인증받은 사용자들에게 토큰을 발급하고, 서버에 요청을 할 때 헤더에 토큰을 함께 보내 유효성 검사를 합니다. 이러한 시스템은 사용자의 인증 정보를 서버나 세션에 유지하지 않고 클라이언트에 저장합니다. 따라서 상태를 유지하지 않으므로 Stateless한 구조
를 갖습니다.
위의 그림처럼 사용자가 로그인을 할 때 서버쪽에서 정보를 검증하고 사용자에게 Signed 토큰을 발급합니다. ( Signed는 해당 토큰이 서버에서 정상적으로 발급된 토큰임을 증명하는 Signature 를 가지고 있다는 것 ) 이후 클라이언트는 전달 받은 토큰을 저장해두고 서버에 요청을 할 때마다 해당 토큰을 HTTP 헤더에 담아 함께 서버에 전달합니다. 서버는 토큰을 검증하고 , 요청에 응답합니다.
무상태성 ( Stateless ) & 확장성 ( Scalability )
토큰은 클라이언트 측에 저장되기 때문에 서버는 완전히 Stateless 하며, 클라이언트와 서버가 연결되어 있지 않기 때문에 확장하기에 적합합니다. 만약 사용자 정보가 서버쪽 세션에 저장된 경우에 서버를 확장하여 분산처리 한다면, 해당 사용자는 처음 로그인 했었던 서버에서만 요청을 받게 해야합니다. 하지만 토큰을 사용한다면 어떤 서버로 요청이 와도 상관이 없습니다.
보안성
클라이언트가 서버로 요청을 보낼 때 더 이상 쿠키를 사용하지 않으므로,
쿠키 사용에 대한 취약점이 사라집니다. 하지만 토큰 환경에서의 취약점이 있으므로 이에 대비해야 합니다.
확장성 ( Extensibility )
시스템의 확장성을 의미하는 Scalability 와 달리 Extensibility 는 로그인 정보가 사용되는 분야의 확장을 의미합니다. 토큰 기반의 인증 시스템에서는 토큰에 권한을 부여하여 발급할 수 있으며 OAuth 의 경우 소셜 계정을 이용하여 다른 웹 서비스에서도 로그인을 할 수 있습니다.
여러 플랫폼 및 도메인
애플리케이션의 서비스 규모가 커지면 여러 디바이스를 호환시키고 더 많은 서비스를 제공하게 됩니다. 이때 토큰을 사용한다면 여러 디바이스나 여러 도메인에서도 토큰의 유효성을 검사한 후에 요청을 처리할 수 있습니다.
웹에서는 쿠키와 세션이 있지만 앱에서는 없기 때문에 토큰 기반의 인증 시스템을 많이 사용합니다.
JWT 는 Json 포멧을 이용하여 사용자에 대한 정보를 저장하는 Claim 기반의 Web Token 입니다. JWT는 토큰 자체를 정보로 사용하는 Self-Contained 방식
으로 정보를 안전하게 전달하며 , Json 데이터를 Base64 URL 를 통해 인코딩합니다. 또한 토큰 내부에는 위변조 방지를 위해 개인키를 통한 전자서명
도 들어가있습니다.
JWT는 static 변수와 로컬 스토리지에 저장됩니다. static 변수에 저장하는 이유는 HTTP 통신을 할 때마다 JWT를 HTTP 헤더에 담아 보내줘야 하는데, 이를 로컬 스토리지에서 계속 불러오면 오버헤드가 발생하기 때문입니다. 또한 로그아웃을 할 때 로컬 스토리지에 저장된 JWT 데이터를 제거해야 합니다. ( 실제 서비스에 로그아웃할 때, 사용했던 토큰을 Blacklist 라는 DB 테이블에 넣어 해당 토큰의 접근을 막습니다. )
JWT는 Header , Payload , Signature의 3 부분으로 이뤄지며, Json 형태인 각 부분을 Base64 URL
로 인코딩 되어 표현됩니다. 또한 각각의 구분을 이어 주기 위해 . 구분자를 사용하여 구분합니다.
토큰의 헤더는 typ 과 alg 두 가지 정보로 구성됩니다.
{ "alg": "HS256", "typ": JWT }
토큰에서 사용할 정보의 조각들인 클레임(Claim)이 담겨 있습니다.
즉, 서버와 클라이언트가 주고받는 실제로 사용될 데이터를 담고 있습니다.
클레임은 총 3가지로 나뉘며 , Json 형태로 다수의 정보를 담을 수 있습니다.
토큰의 정보를 표현하기 위한 데이터들로 , 모두 선택적으로 작성이 가능하며 사용할 것을 권장합니다. 또한 JWT를 간결하게 하기 위해 key 는 모두 3자리 String 으로 이루어집니다.
사용자 정의 클레임으로 공개용 정보를 위해 사용되며 , 충돌 방지를 위해 URI 포맷을 이용합니다.
{ "https://www.naver.com": true }
사용자 정의 클레임으로, 당사자들 간에 정보를 공유하기 위해 만들어지며 해당 유저를 특정할 수 있는 정보를 담습니다.
토큰을 인코딩하거나 유효성 검증을 할 때 사용되는 고유한 암호화 코드입니다. 서명은 위에서 만든 헤더 (Header) 와 페이로드 (Payload)의 값을 각각 Base64URL 로 인코딩하고, 인코딩한 값을 비밀 키와 합쳐 헤더 ( Header ) 에서 정의한 알고리즘으로 암호화 합니다.
Header 와 Payload 는 단순히 인코딩한 값이기 때문에 제 3자가 조작할 수 있지만 , Signature는 서버에서 관리하는 비밀키가 유출되지 않는 한 복호화를 할 수 없습니다. 따라서 Signature는 토큰의 위변조 여부를 확인하는데 사용됩니다.
서버는 토큰 안에 들어있는 정보보다 해당 토큰이 유효한 토큰인지 확인하는 것이 더 중요하기 때문에, 클라이언트로 부터 받은 JWT 의 헤더 , 페이로드를 서버의 key값을 이용해 시그니처를 다시 만들고 이를 비교하여 일치했을 경우 인증을 통과합니다.
다만 JWT도 제 3자에게 토큰 탈취의 위험성이 있기 때문에 그대로 사용하는 것이 아닌 AccessToken 과 RefreshToken 으로 나누어 인증을 합니다.
AccessToken과 RefreshToken은 둘다 JWT이며 어떻게 사용되는지에 따라 차이가 있습니다.
Access Token 만 사용했을 경우 발생하는 문제는 제 3자에게 탈취당했을 경우 보안에 취약하다는 것 입니다.
Access Token은 발급한 이후에 서버에 저장되지 않고 토큰 자체로 검증을 하여 사용자 권한을 인증하기 때문에 , Access Token이 탈취되면 토큰이 만료되기 전까지 토큰을 획득한 사람이라면 언제나 권한 접근이 가능해지기 때문입니다.
JWT는 발급한 이후에 삭제가 불가능 하기 때문에 토큰에 유효 시간을 부여하여 탈취 문제를 대응했습니다.
유효 시간을 짧게 설정하면 토큰 남용을 방지할 수 있지만, 유효시간이 너무 짧을 경우 사용자가 로그인을 자주 해서 Token 을 재발급 받아야 하며, 반대로 유효 시간을 길게 설정하면 토큰을 탈취 당했을 때 보안에 더 취약해집니다.
따라서 유효 시간을 짧게 하면서 좋은 방법
이 바로 Refresh Token을 사용하는 것 입니다.
이름이 다르지만 Refresh Token 과 Access Token 은 똑같은 JWT 입니다. 다만 Access Token은 접근에 관여하는 토큰이고 , Refresh Token 은 재발급에 관여하는 토큰으로 역할이 다릅니다.
이 Refresh Token 은 긴 유효 시간을 가지면서, Access Token이 만료되었을 때 새로 재발급 해주는 열쇠가 됩니다. 따라서 만료된 Access Token을 서버에 보내면 , 서버는 같이 보내진 Refresh Token 을 DB에 있는 것과 비교해서 일치하면 새로운 Access Token 을 발급합니다. 그리고 사용자가 로그 아웃을 하면 저장소에 있는 Refresh Token을 삭제합니다.
Refresh Token 역시 탈취 당할 가능성이 있기 때문에 적절한 유효기간을 설정해주어야 합니다. ( Refresh Token은 평균 2주 , Access Token은 평균 30분 )
참고 블로그 1 : https://mangkyu.tistory.com/55
참고 블로그 2 : https://mangkyu.tistory.com/56
참고 블로그 3 : https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-JWTjson-web-token-%EB%9E%80-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC#jwt%EC%9D%98_access_token_/_refresh_token
참고 블로그 4 : https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-Access-Token-Refresh-Token-%EC%9B%90%EB%A6%AC-feat-JWT