현대 웹 인증에서 빼놓을 수 없는 개념이 바로 쿠키와 세션이다. 많이 들어본 개념이지만 정확히 어떤점이 다르고 어떤 상황에서 쓰게 되는 것일까?
쿠키와 세션은 HTTP 프로토콜의 환경을 보완하기 위해서 사용된다. 기본적으로 HTTP 프로토콜은 다음과 같은 특징이 있다.
stateless(무상태) : 서버는 기본적으로 클라이언트의 이전 상태를 보존하지 않는다.
connectionless(비연결성) : 클라이언트가 서버에 요청을 하고 응답을 받으면 바로 연결을 끊어 서버의 자원을 효율적으로 관리한다.
때문에 서버는 클라이언트가 누구인지 매번 확인해야만 한다. 이런 특성의 단점을 보완하기 위해서 쿠키나 세션을 사용하게 되는 것이다.
쿠키는 기본적으로 클라이언트 로컬에 저장되며, 키/값으로 정의되는 데이터 파일이다. 쿠키는 유효기간을 따로 정할 수 있고, 사용자가 따로 요청하지 않아도 브라우저가 Request시, 자동으로 서버에 전송하게 된다.
하지만 브라우저에 직접 저장되기 때문에, 탈취 및 변조가 쉬워 보안이 중요한 정보를 다룰때에는 이용해서는 안된다. 때문에 주로 유출되어도 크게 상관없는 정보의 상태를 저장할 때 쓰이곤 한다.
세션은 일정 시간동안 같은 사용자로부터 오는 요구를 하나의 상태로 판단, 그 상태를 유지시키는 기법이다. 이러한 사용자 정보를 서버측에서 관리한다는 점이 쿠키와는 가장 큰 차이점이다.
서버에서는 클라이언트를 구분하기 위해 세션 ID를 부여하며, 브라우저가 서버에 접속해 브라우저를 종료할 때 까지 인증상태를 유지한다. 서버에서 사용자 정보를 관리하므로, 상대적으로 보안성이 우수하다는 장점이 있다. 때문에 로그인과 같은 작업을 수행할 때 사용된다.
(세션ID 발급및 사용과정, 발급받은 세션ID를 쿠키로 저장한다.)
하지만 세션방식에도 단점은 있다. 사용자의 정보를 서버과 관리하기 때문에, 사용자가 많아질수록 서버에 걸리는 부하가 커지게 된다.
JWT(Json Web Token)는 이러한 상황에서 새롭게 등장한 인터넷 표준 인증 방식이다. 이름 그대로 서버가 인증에 필요한 정보들을 Token에 담아 클라이언트로 보내 클라이언트측에서 관리하게끔 하는 것이다.
기본적인 인증진행구조는 쿠키때와 크게 다르지 않지만, JWT의 토큰은 비대칭키를 이용한 전자서명을 통해 토큰의 정상여부를 판별한다.
JWT 토큰의 구성은 다음과 같다.
.
으로 구분되는 각각의 요소는 Header
, Payload
, Signature
라고 불리운다.
Header
Header에는 JWT에서 사용할 타입과 해시 알고리즘의 종류가 담겨있다.
{
"typ": "JWT", // 타입
"alg": "HS512" // 서명 시 사용하는 알고리즘
}
Payload
Payload에는 서버에서 첨부한 사용자 권한 정보와 데이터가 담겨있다.
{
"sub": "JWT", // 토큰제목
"iss": "HS512", // 토큰 발급자
"exp": 1636989718, // 토큰 만료 시간
"iat": 1636989718 // 토큰 발급 시간
... // nbf(토큰 활성 날짜), jti(JWT 식별자), aud(대상자) 등이 들어갈 수 있음
}
위 내용은 표준 스펙이긴 하나, 언급된 7가지 내용을 전부 적을 필요도 없고, 유저가 원하는 커스텀값을 기입해도 괜찮다. 다만, 중요한 것은 민감한 정보를 기입하지 않도록 하는 것이다. Header와 Payload에는 특별한 암호화가 되는 것이 아니기 때문에 누구나 값을 알 수 있기 때문이다.
Signature
Signature는 서명으로, 앞서 언급된 Header + Payload 한 값을 인코딩한 후 Header에 명시된 해시함수를 적용하여 서버측 개인키로 암호화한다. 이를통해 클라이언트는 서버측의 공개키로 이를 복호화하여 받아온 토큰이 유효한지 확인할 수 있다. (복호화한 값이 header값과 일치한지 등)
정리해서 사용자의 로그인을 통한 JWT의 전달과정을 그림으로 알기쉽게 표현하면 다음과 같은 그림이 된다.
우선 주요한 이점으로 사용자 인증에 필요한 모든 정보는 토큰에 포함되기 떄문에 별도의 인증 저장소가 반드시 필요하지는 않으며, 클라이언트의 상태를 서버가 저장할 필요가 없어지게 되었다. 거기에 비대칭키를 활용하여 보안성도 기존에 비해 증가하게 되었다.
단점은 토큰에 저장되는 값이 커질 수 있는데, 매 요청시마다 토큰을 보내므로 트래픽 크기에 영향을 미칠 수 있다는점이 있다. 또한 인증의 유효기간에 대한 문제가 따르게 된다. 상태를 저장하지 않기 때문에, 한번 만들어지면 제어가 불가능하다는 것이다.
예시로, 로그아웃이나 토큰이 탈취된 경우에 문제가 발생할 수 있다. 토큰의 상태를 관리하는 중앙시스템이 없기 때문에 토큰을 삭제할수가 없는 것이다. 그래서 나온 개념이 있다. 바로 Access Token과 Refresh Token이다.
여태까지 설명됐던 토큰은 일반적으로 클라이언트가 자신을 입증하는 액세스 토큰으로 동작한다. 하지만 JWT는 상태를 저장하지 않기 때문에(Stateless) 토큰을 가지고 있는 클라이언트가 정말 본인이 맞는지 확인할 수가 없다.
이에대한 대책으로 Refresh Token이라는 추가적인 토큰을 도입할 수 있다. 이 토큰은 어디까지나 새로운 액세스 토큰을 생성하는 용도로만 사용되며, 다음과 같이 사용된다.
여태까지 서버와 클라이언트간의 인증에 대해서 알아보았다. 그리고 중점으로 다룬 JWT의 경우에는 클라이언트측에 토큰을 보내고 클라이언트에서 해당 토큰을 관리하고 있다. 그렇다면 클라이언트는 토큰을 어떻게 보관해야 할까?
보관방법중 가장 중요시해야 할 것은 바로 취약점에 대한 고려일 것이다. 대표적인 웹에서의 취약점으로는 XSS와 CSRF가 있다. XSS는 JS코드를 사용자 브라우저에서 실행하여 공격을 하는 방식이고, CSRF는 브라우저에서 어떤 api로의 요청을 강제함으로써 사용자가 의도하지 않은 작업을 수행하게하는 공격방식이다. 이런 공격을 막는것을 상정하고 보관해야만 할 것이다.
크게 두가지 보관장소가 있다. LocalStorage와 쿠키이다. 하지만 결론부터 이야기하자면, 정답은 없다. 각각 장단점이 있고 서비스환경에 따라 고려해야 하는 점들이 있기 때문이다. 하지만 굳이 장단점을 서술하고, 추천되는 장소를 말할 수는 있겠다.
브라우저의 LocalStorage에 저장하는 방식으로, CSRF에 안전하다고 알려져있다. 쿠키는 서버에 요청을 하게되면 자동으로 request에 담기지만, localstorage는 js 코드에 의해 헤더에 담기게 되므로 XSS를 뚫지 않는 이상은 CSRF공격에 대해서는 상대적으로 안전하다고 볼 수 있겠다.
하지만 역으로 XSS에 매우 취약하다는 단점이 있다. localstorage에 접근하는 Js 코드를 입력하기만 하면 손쉽게 내 토큰에 공격자가 접근할 수 있게된다. 더욱이 XSS는 웹 보안의 근간으로, 이것이 뚫리면 CSRF를 아무리 잘 막더라도 의미가 퇴색되기 때문이다.
쿠키에 저장하는 방법은 localstorage에 비해서 XSS공격에 더 안전하다고 볼 수 있다. 쿠키의 옵션중 httpOnly
라는 옵션을 사용하면 Js에서는 쿠키에 접근 자체를 할 수가 없다. 때문에 XSS 공격으로는 쿠키에 담긴 정보를 탈취할 수 없는 것이다.
물론 자동으로 request에 담기는 쿠키의 특성상 완전히 안전하다고는 할 수 없을 것이다.
우선 mdn은 저장소로 쿠키대신 ModernStorage(LocalStorage, SessionStorage)를 추천하고 있다. ModernStorage를 추천하는 사람들의 입장은 - 어차피 쿠키의 httpOnly 옵션으로도 XSS 공격을 완벽히 막을 수 없고, XSS 방어는 필수적으로 진행되는 작업이므로 쿠키만의 장점으로 보기도 어렵다고 한다.
반면 쿠키를 지지하는 사람들의 입장은 - CSRF 공격은 다루기 쉬운 반면, XSS 공격을 막기 위한 작업은 프론트엔드의 크기가 클수록 어려워 지므로 쿠키를 사용하는 것이 더 이득이라는 것이다.
어느쪽을 선택할 지는 개발자 스스로가 정해야 할 문제일 것이다.
참조 : What's the Secure Way to Store JWT?
JWT를 이용한 로그인(로그아웃)
JWT에서 Refresh Token은 왜 필요한가?
JWT, 정확하게 무엇이고 왜 쓰이는 걸까?
WT(Json Web Token) 알아가기
로그인은 어떻게 이루어질까
쿠키(Cookie)와 세션(Session)의 차이