앞서 쿠키/세션과 관련된 포스트에서 HTTP 프로토콜에서 사용되는 쿠키와 세션의 개념에 대해 확인했다. 그렇다면 이제 사용자의 인증 과정에서 세션을 사용하는 세션 기반 인증(Session-based authentication)과 세션이 아닌 '토큰'을 사용하는 토큰 기반 인증(Token-based authentication), 그리고 가장 대표적인 토큰 기반 인증 방식인 JWT(JSON Web Token)에 대해 알아보자.
1. 세션 기반 인증(Session-based authetication)
세션 기반 인증 동작 방식
세션 기반 인증은 사용자의 인증 정보가 서버의 세션 저장소에 저장되는 방식이다. 클라이언트에서 사용자가 로그인을 하면, 서버는 로그인 정보가 유효한지 확인한 뒤 해당 인증 정보를 서버의 세선 저장소에 저장하고, 클라이언트에는 저장된 세션 정보의 식별을 위한 정보인 Session ID를 발급하여 쿠키 형태로 실어 보낸다.
그리고 사용자의 브라우저는 이렇게 인증 절차를 마친 후 요청마다 발급받은 Session ID를 함께 서버로 전송하고, 서버는 전달받은 요청에서 Session ID를 확인한 뒤 해당 ID에 해당하는 정보가 세션 저장소에 존재한다면 해당 클라이언트를 인증된 클라이언트로 판단한다.
세션 기반 인증의 문제점
세션 기반 인증 방식은 개발의 입장에서는 상당히 간편하지만 몇 가지 단점이 존재한다. 그중 하나가 보안 문제인데, CSRF(Cross-Site Request Forgery)에 대한 위험성을 안고 있다는 점이다.
CSRF는 '사이트 간 요청 위조'를 말하는 것으로 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(데이터의 등록, 수정, 삭제 등)를 특정 웹사이트에 요청하도록 하는 공격이다. 이 과정을 간단하게 요약하면 다음과 같이 정리할 수 있다.
- 사용자가 보안에 취약한 웹사이트에 접속한다.
- 서버에 저장된 세션 정보에 접근할 수 있는 세션 ID가 사용자의 브라우저로 전송되어 저장된다.
- 공격자는 사용자가 악성 스크립트 페이지를 누르도록 유도한다.
- 게시판에 악성 스크립트를 게시글로 작성하여 게시글을 누르도록 유도
- 메일 등으로 악성 스크립트를 직접 전달하거나 악성 스크립트가 적힌 페이지의 링크를 전달
- 클라이언트가 악성 스크립트 페이지에 접근하는 순간 브라우저는 쿠키에 저장된 세션 ID가 서버로 요청된다.
- 악성 스크립트를 거쳐 위조된 요청이 세션 ID와 함께 서버에 요청되고, 서버는 이렇게 위조된 요청을 본래 사용자가 의도한 요청으로 이해하고 처리한다.
다만 CSRF는 현대의 개발 프레임워크를 사용한다면 그 위험성이 극히 줄어들므로 일반적인 사용 범위 내에서는 큰 지장이 없다.
오히려 세션 방식 인증에서 가장 큰 문제가 되는 것은 서버의 메모리에 대한 부하 문제이다. 세션 정보가 서버의 메모리에 저장되므로, 트래픽이 커질수록 서버가 감당해야 할 메모리 부담이 커지게 된다.
이러한 문제를 해결하기 위해 등장한 대안이 바로 토큰 기반 인증 시스템이다.
2. 토큰 기반 인증(Token-based authetication)
토큰 기반 인증 동작 방식
기본적인 방식은 세션 기반 인증의 동작 방식과 유사하다(JWT 기준).
- 클라이언트가 로그인 정보를 서버에 전송한다.
- 서버는 로그인 정보를 확인한 뒤 검증이 완료된다면 signed 토큰(해당 토큰이 서버에서 정상적으로 발급된 토큰임을 증명하는 signature를 가진 토큰)을 생성하고, 이 토큰 정보를 클라이언트에 쿠키의 형태로 전송, 사용자의 로컬 환경에 저장한다.
- 클라이언트는 이후 요청에 토큰을 실어 전송한다.
- 서버는 전송받은 토큰을 DB나 세션에 저장하지 않고, 프로그래밍적으로 토큰의 서명(signature)을 인증한 뒤 유저의 요청에 응답한다.
토큰 기반 인증 방식의 장점
- 무상태성(Stateless)과 확장성(Scalability)
토큰은 서버가 아닌 클라이언트에 저장되므로 서버는 stateless 상태를 유지할 수 있다. 또한 클라이언트와 서버의 연결성이 없으므로 서버를 확장하기에도 매우 용이하다. 만약 클라이언트의 정보가 서버에 저장된 경우, 서버를 확장하여 분산 처리하려면 해당 클라이언트는 최초 로그인을 했던 서버에만 요청을 하도록 설정되어야 한다. 그러나 토큰을 사용할 경우 어떠한 서버로 요청이 와도 상관이 없게 된다.
- 보안성
클라이언트가 서버로 요청을 보낼 때 쿠키를 전달하지 않고 암호화된 토큰을 보내므로 쿠키 사용에 의한 보안 취약점이 사라진다(물론 토큰 환경에 대한 취약점에 대한 대비는 필요할 것이다).
- 확장성(Extensibility)
서버의 확장성을 의미하는 Scalability와 달리, Extensibility는 로그인 정보가 사용되는 분야의 확장을 의미한다. 토큰 기반 인증 시스템에서는 토큰에 선택적인 권한만을 부여하여 발급하는 것이 가능하며, OAuth의 경우 소셜 계정을 활용하여 다른 웹 서비스에서 로그인하는 것도 가능하다.
- 여러 플랫폼 및 도메인
서버 기반 인증 시스템의 문제점 중 하나인 CORS를 해결할 수 있다. 토큰을 사용할 경우 어떤 디바이스 혹은 어떤 도메인에서든 토큰의 유효성 검사를 진행한 후 요청을 처리할 수 있게 되며, 이런 구조를 통해 assests 파일(html, image, css, js 등)은 모두 CDN에서 제공하고, 서버 측에서는 API만 다루도록 설계하는 것이 가능해진다.
※ CORS에 대해서는 별도로 알아보는 시간이 필요하며, 일단은 CSRF와 관련된 설정 정도로 이해해 둔다.
토큰 기반 인증 방식의 문제점
토큰 기반 인증 방식 역시 장단점이 존재한다. 가장 대표적인 단점은 다음과 같다.
1. 세션 기반 인증 방식에 비해 훨씬 더 많은 네트워크 트래픽을 사용한다.
2. 토큰이 탈취될 경우 해당 토큰이 만료되기 전까지는 피해를 막을 방법이 없다(세션은 서버에서 해당 세션을 무효 처리하면 된다).
따라서 server-to-server 인증 등 특정한 경우라면 토큰 인증 방식이 상당히 탁월한 선택이 되겠지만, 대부분의 사용자가 일반적으로 접하는 웹 사이트의 경우 토큰 기반 인증 방식은 트래픽 부담은 물론 컴퓨팅 성능도 더 많이 필요로 하므로 세션 기반 인증 방식이 더 효율적일 수도 있다.
따라서 어떠한 인증 방식을 사용할 것인가는 해당 사이트가 어떠한 서비스를 제공하느냐에 따라 더 적합한 방식을 선택해야 할 것이다.
3. JWT(JSON Web Token)
JWT의 개념
JWT(JSON Web-Token)이란 이름에서 알 수 있듯 JSON 포맥을 이용하여 사용자의 정보를 저장하는 Claim 기반의 Web-Token이다. 주로 회원 인증이나 정보 전달에 사용되며, 토큰 자체를 정보로 사용하는 Self-Contained 방식으로 정보를 안전하게 전달한다. 기본적인 동작 방식은 다음과 같다.
- 클라이언트가 서버에 접속하여 ID와 PW를 이용해 로그인한다.
- 서버는 요청을 확인하고 Access token을 발급하여 클라이언트에 JWT를 전달한다.
- 사용자는 서버에 요청과 함께 발급받았던 JWT를 전송한다.
- 서버는 JWT의 서명을 체크하고, JWT로부터 사용자 정보를 확인한다.
- 서명이 유효하다면 서버는 클라이언트에 요청에 대한 응답을 전달한다.
JWT의 구조
JWT는 Header, Payload, Signature의 3개 부분으로 구성되며, 각 부분은 JSON 형태로서 Base64Url로 인코딩되어 표현된다. 그리고 '.'을 이용하여 각 부분을 구분한다.
※ Base64Url은 암호화 함수가 아니라 인코더이므로 같은 문자열에 대해 항상 같은 형태로 인코딩된 문자열을 반환한다.
- 헤더(Header)
토큰의 헤더는 typ과 alg 두 가지 정보로 구성된다. typ는 해당 토큰이 어떤 타입인지를 지정하고, alg는 Signature를 해싱하기 위한 알고리즘을 지정한다.
{
"alg": "HS256"
"typ": JWT
}
- 페이로드(Payload)
토큰에서 사용할 정보의 조각들, 즉 클레임(Claim)이 담겨 있다. 클레임은 크게 3가지로 구분되며, JSON 형태로서 다수의 정보를 입력할 수 있다.
- 등록된 클레임(Registered claim) : 토큰 정보를 표현하기 위해 약속된 종류의 데이터로, 선택적으로 작성이 가능하나 사용할 것이 권장된다.
- iss: 토큰 발급자(issuer)
- sub: 토큰 제목(subject) → 주로 email이 사용된다.
- aud: 토큰 대상자(audience)
- exp: 토큰 만료 시간(expiration) → NumericDate 형식으로 되어 있어야 한다(예: 1480849147370)
- nbf: 토큰 활성 날짜(not before) → 이 날짜 이전에는 토큰이 활성화되지 않는다.
- iat: 토큰 발급 시간(issued at) → 토큰 발급 이후의 경과 시간을 확인할 수 있다.
- jti: JWT 토큰 식별자(JWT ID) → 중복 방지를 위해 사용하며, 일회용 토큰(Access token) 등에 사용된다.
- 공개 클레임(Public claim) : 사용자 정의 클레임으로, 공개용 정보를 위해 사용된다. 중돌을 방지하기 위해 URL 포맷을 사용한다.
{
"https://velog.io/@mino0121": true
}
- 비공개 클레임(Private claim) : 사용자 정의 클레임이며, 서버와 클라이언트 사이에서 임의로 지정한 정보를 저장한다.
{
"token_type": access
}
- 서명(Signature)
서명은 토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드로, 위에서 만든 헤더와 페이로드의 값을 각각 BASE64Url로 인코딩한 뒤, 인코딩한 값을 비밀 키를 이용해 헤더에서 정의한 알고리즘으로 해싱하고 이 값을 다시 BASE64Url로 인코딩하여 생성한다.
이렇게 생성된 토큰은 HTTP 통신 시 Authorization이라는 key의 value로 사용되며, 일반적으로 앞에 Bearer를 붙여 사용한다.
{
"Authorization": "Bearer {생성된 토큰 값}"
}
JWT의 단점 및 고려 사항
- Self-contained : 토큰 자체에 정보를 담고 있으므로 토큰 탈취 시 정보 보안 문제가 발생할 수 있다.
- 토큰 길이 : 토큰의 페이로드에 3종류의 클레임을 저장하므로 정보가 많아질수록 토큰의 길이가 길어져 네트워크에 부하를 줄 수 있다.
- 페이로드 인코딩 : 페이로드 자체가 암호화된 것이 아니라 단순히 인코딩된 것이므로 디코딩이 가능하며, 따라서 중요한 정보를 넣을 수 없다.
- 무연결성 : JWT는 사용자의 상태를 저장하지 않으므로 일단 작성되면 이후 제어가 불가능하다. 즉, 서버 측에서 토큰을 삭제하는 것이 불가능하므로 토큰 만료 시간을 반드시 넣어야 한다.
참고 자료