사용자의 인증정보를 관리하는 방식은 크게 두가지로 나뉜다.
첫째는 세션 기반 인증이다. 이는 서버 기반 인증이라고 불린다. 이 인증 시스템은 유저가 로그인을 하게 되면, 서버측에서 유저가 현재 로그인 중이라는 인증정보를 기억하고 있어야 한다. 그리고 이 정보를 세션 이라고 부른다.
서버 기반 인증의 문제점
서버기반 인증의 문제점은 크게 2가지로 있다.
첫째는, 세션을 유지하게 될 때, 로그인중인 유저의 수가 많아진다면 성능에 무리가 가게 된다. 이 정보를 메모리에 넣게 된다면 램이 과부화가 되고, 데이터베이스에 넣게 된다면 데이터베이스의 성능에 무리가 가게 된다.
두번째는, 서버 확장이 어려워진다는 점이다. 여기서 서버 확장은 더 많은 트래픽을 감당하기 위하여 여러 프로세스를 돌리거나 여러 서버 컴퓨터를 추가하여 로드밸런싱을 할 때를 의미한다. 세션을 사용하면서 분산된 시스템을 설계하는 것은 세션의 정보가 분산된 프로세스간에 동기화가 되야하므로 과정이 매우 복잡하다.
토큰 기반 시스템이 제공해주는 것
토큰 기반 시스템은 stateless 하다. 이 말의 의미는 '무상태' 라는 뜻이다. 서버시스템측에서 더 이상 유저의 정보를 유지하지 않고, 유저가 회원 인증을 하게 될 때 토큰 을 발급해줌으로서 유저가 자기 자신임을 인증 할 수 있게 해준다. 발급이 된 토큰은 토큰의 유효기간, 정보를 담고 있다. 해싱 알고리즘을 통해 인증이 되어있어서 서버에서 검증을 통하여 처음 서버가 발급해주었던 정보가 변조되지 않았음을 보장 해 줄 수 있다.
토큰을 사용함으로서, 서버를 확장하게 될 때에 매우 용이해지게 된다. 서버 시스템이 분산이 되어있어도 유저는 같은 토큰으로 서버에 요청을 하면 되고, 서버는 그저 그 토큰이 위조되지 않았는지만 검증을 하고 데이터베이스 조회도 할 필요 없이 바로 유저임을 신뢰하고 처리를 하면 되기 때문이다.
추가적으로, 토큰을 사용하면 플랫폼간 권한을 공유 할 수 있다. 예를 들어 페이스북, 구글 계정을 통한 소셜 로그인이 가능한 이유도 구글과 페이스북에서 토큰기반 인증 시스템을 사용하기 때문입니다. 소셜 로그인 과정에서 구글/페이스북 플랫폼에서 로그인을 하고, 해당 플랫폼이 토큰을 발급을 해주면 우리의 백엔드 서버에서 이를 통하여 회원정보를 가져오고 우리의 서비스에 계정 생성을 하게 된다.
마지막으로, 토큰 기반 시스템은 모바일 어플리케이션에서 사용하기에 편해진다. 만약에 세션 기반 인증을 사용한다면 쿠키를 사용해야 하기 때문에, 쿠키 매니저를 따로 관리해줘야 한다. 토큰을 사용한다면 웹 요청 API 에 헤더에 넣어서 사용해주면 되기때문에 더이상 쿠키 매니저를 사용 할 필요가 없어진다.
토큰 저장 위치
서버가 토큰을 발급해주면, 브라우저에서 사용자 / 서버간에 토큰이 전달되는 방식은 크게 두가지로 나뉜다.
첫번째는, 로그인에 성공하게 되었을 때 서버가 토큰을 응답정보에 토큰을 넣어서 전달하도록 하고, 해당 값을 웹 스토리지 (localStorage 혹은 sessionStorage) 에 넣는다. 그 다음부터 웹 요청을 할 때마다 HTTP 헤더 값에 넣어서 요청하는 방법이 있다.
→ 구현하기 쉽고, 하나의 도메인에 제한되어있지 않다는 장점이 있다. 그렇지만 XSS 해킹 공격을 통하여 해커의 악성스크립트에 노출이 되는 경우 매우 쉽게 토큰이 탈취될 수 있다. 그냥 localStorage 에 접근하면 바로 토큰에 접근 할 수 있기 때문이다.
두 번째 방식은 이 토큰을 쿠키에 넣는 것 입니다. 쿠키를 정보 전송수단으로 사용한다. 이 과정에서, 서버측에서 응답을 하면서 쿠키를 설정 해 줄 때 ***httpOnly 값을 활성화***를 해주면, 네트워크 통신 상에서만 해당 쿠키가 붙게 된다. 따라서, 브라우저상에서는 자바스크립트로 토큰값에 접근하는 것이 불가능해지게 된다.
→ 두번째 대안에 대한 단점은, 쿠키가 한정된 도메인에서만 사용이 된다는 점 입니다. 그러나 이 부분은 토큰이 필요해질 때 현재 쿠키에 있는 토큰을 사용하여 새 토큰을 문자열로 받아올 수 있게 하는 API를 구현하여 해결하면 된다.
CSRF 공격의 위험성이 생긴다는 점 입니다. CSRF은 스크립트를 통하여 사이트의 외부에서 사이트의 API 를 사용하는 것처럼 모방한다. 혹은, 사이트 내부에서 스크립트가 실행되어 원하지 않는 작업이 수행되게 할 수도 있다.
- XSS 공격
- 해커가 클라이언트 브라우저에 Javascript를 삽입해 실행하는 공격이다. 공격자가 <input>을 통해 Javascript를 서버로 전송해 서버에서 스크립트를 실행하거나, url에 Javascript를 적어 클라이언트에서 스크립트 실행이 가능하다면 공격자가 사이트에 스크립트를 삽입해 XSS 공격을 할 수 있다. 이 때 해커는 Javascript를 통해 사이트의 글로벌 변숫값을 가져오거나 그 값을 이용해 사이트인 척 API 콜을 요청할 수도 있다. 다시 말하면 공격자의 코드가 내 사이트의 로직인 척 행동할 수 있다.
- CSRF 공격
- 공격자가 다른 사이트에서 우리 사이트의 API 콜을 요청해 실행하는 공격이다. API 콜을 요청할 수 있는 클라이언트 도메인이 누구인지 서버에서 통제하고 있지 않다면 CSRF가 가능하다. 해커가 클라이언트에 저장된 유저 인증정보를 서버에 보낼 수 있다면, 유저로 로그인한 것처럼 유저의 정보를 변경하거나 유저만 가능한 액션들을 수행할 수 있다. 예를 들어, CSRF에 취약한 은행 사이트가 있다면 로그인한 척 계좌 비밀번호를 바꾸거나 송금을 보낼 수 있다.
→ 웹스토리지에 토큰을 담으면 XSS 에 취약하고, 쿠키에 담으면 CSRF 에 취약해집니다. 하지만, CSRF는 보안에 신경을 쓰면 차단할 수 있습니다.
로그인 방식
1. 세션 id를 이용하는 방식
서버는 특정 유저의 정보를 담은 세션을 생성한다. (1) 유저가 로그인할 때 (2) 서버는 세션을 생성한 후 (3) 그 세션의 id를 클라이언트에 보내주고 (4) 클라이언트는 이 id를 클라이언트에 저장해두었다가 (5) 인증이 필요한 데이터를 가져올 때 서버에 id 값을 보내면 (6) 서버는 그 id를 통해 세션을 불러와 유효한지 확인하는 방식으로 인증한다.
2. JWT를 이용하는 방식 (ft. refreshToken, accessToken)
- 실질적인 인증 정보는 accessToken인데 일정 시간이 지나면 만료하는 구조를 가지고있다. refreshToken을 이용해 로그인을 지속적으로 유지할 수도 있다. refreshToken을 서버에 보내면 그 때마다 새로운 accessToken을 발급해 돌려준다. refreshToken 사용은 옵션이다.
- (5) 이 accessToken을 유저에게만 보여줄 수 있는 정보에 접근할 때 서버에 보내면 (6) 서버는 그 토큰이 유효한지 확인하는 방식으로 인증한다.
- (1) 유저가 로그인할 때 (2) 서버가 인증 정보를 보내주는데, 암호화나 시그니처 추가가 가능한 데이터 패키지안에 인증 정보를 담아 보내준다. (이 패키지가 JSON Web Token 즉 JWT다.) (3) 담기는 정보 중 accessToken과 refreshToken이 이후 유저 인증에 사용되는데 (4) 이 정보를 클라이언트에 저장해둔다.
JWT
JWT(Json web Token)은 토큰 기반 인증 방식으로, 필요한 정보를 토큰 body에 저장해 클라이언트가 가지고 있고 그것을 증명서처럼 사용한다.
JWT의 기본 구성은 .을 기준으로 3가지로 나눠진다.
Header
: JWT인 토큰의 유형이나 HMAC SHA256 또는 RSA와 같이 사용되는 해시 알고리즘이 무엇으로 사용했는지 등 정보가 담깁니다. Base64Url로 인코딩되어 있습니다.
{
"alg": "서명 시 사용하는 알고리즘",
"kid": "서명 시 사용하는 키를 식별하는 값",
"typ": "타입"
}
헤더(Header)에는 JWT를 어떻게 검증하는지에 대한 내용이 들어가 있다. 토큰의 타입, 암호화 알고리즘이 어떤 알고리즘인지에 대한 정보가 들어있다. 해시 알고리즘의 이름을 적어줄 수 있다.
payload
: 클라이언트에 대한 정보나, meta Data같은 내용이 들어있고, Base64Url로 인코딩되어있습니다.
{
"sub": "hyeonsu.jung",
"exp": 1623235123,
"iat": 1532341234
}
토큰에 담아서 우리가 보내고자 하는 데이터가 이곳에 담겨져 있다.이 정보의 조각은 클레임(claim) 이라고 하고, key - value의 한 쌍으로 이루어져 있다.그리고 payload에는 여러 개의 클레임을 담을 수 있고, 클레임을 공개(public) 혹은 비공개(private) 할 것인지 등록(registered)할 것인지 결정할 수 있다.
signature
: header에서 지정한 알고리즘과 secret 키, 서명으로 payload와 header를 담습니다.
시그니처(signature)에는 위의 헤더(header)와 페이로드(Paylaod)를 합친 문자열을 서명한 값이다. 서명은 헤더의 alg에 정의된 알고리즘과 secret key를 이용해 생성하고 Base64 URL-Safe로 인코딩한다.secret key를 포함해서 암호화가 되어있다.
생성된 JWT는 왼쪽의 Encoded된 값처럼 점(.)을 구분자로 헤더, 페이로드, 시그니쳐로 나눠서 서로 전달하게 된다. 서버는 헤더의 alg, kid 속성과 공개 키를 이용해 검증할 수 있다. 검증(이 토큰이 신뢰할 수 있는 토큰인가?)가 성공하면 페이로드의 값으로 접근을 해서 사용할 수 있다.
JWT의 위험성
JWT는 자체 내에 정보를 가지고 있기 때문에 클라이언트가 해독해 정보를 볼 수 있지만 받는 자가 secret 키를 알고 있어야만 수정이 가능합니다. 그리고 JWT 인증 방식을 채용할려면 기한 조절이 중요합니다. 만약 수명이 길면 해커에게 해독될 위험이 있고 수명이 짧다면 토큰이 만료될때마다 로그인을 다시 해줘야됩니다.
장점
- 세션 방식과 다르게 별도의 인증 저장소가 필요 없어서 서버와의 커뮤니케이션을 최소한으로 할 수 있다.
- 트래픽에 대한 부담이 적다.
- 세션과 다르게 독립적인 느낌의 JWT를 활용한다는 것.
단점
- JWT의 크기가 커질수록 거의 모든 요청에 대해 전송되므로 데이터 트래픽 크기에 영향을 미칠 수 있다.
- 토큰은 클라이언트에 저장되기 때문에 DB에서 사용자 정보를 수정하더라도 토큰에 직접 적용할 수 없다.