본문 바로가기

JWT

JWT 소개

# Spring Security란?

 

  • Spring 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 하위 프레임워크.
  • 인증(Authentication)과 권한(Authorization)에 대한 부분을 Filter 흐름에 따라 처리.
  • 기본적으로 세션&쿠키 방식으로 인증되며, 어노테이션으로 간단한 설정이 가능하다

 

# 기본용어

 

  • 인증 (Authentication) : 누구인가?
    사용자 본인이 맞는지 확인하는 절차.(ex. 로그인)
  • 인가 (Authoriation) : 어떤것을 할 수 있는가?
    인증된 사용자가 요청한 자원에 접근이 가능한지 확인하는 절차. (ex. 로그인한 유저가 게시글을 쓸수있는 권한이 있는지)
  • Principal(접근 주체) : 보호받는 자원(리소스)에 접근하는 유저
  • Credential(비밀번호) : 자원(리소스)에 접근하는 대상의 비밀번호

 

# JWT

 

  • Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 claim 기반의 web token.

 

JWT는 Header, Payload, Signature 이 세가지로 이뤄져있으며, 각 부분은 Base64로 인코딩 되어 표현된다.

 

  • header
    signature를 해싱하기 위한 알고리즘 정보가 담겨져있다.
{ 
  	"alg": "HS256", // 알고리즘 방식 지정. HS256(SHA256) 또는 RSA
 	"typ": JWT // 토큰 타입 지정
}

 

  • Payload
    시스템에서 실제로 사용될 정보. 토큰에서 사용할 정보의 조각들인 클레임(claim)이 담겨있다.클레임은 총 3가지로 나누어지며, Json(Key/Value) 형태로 다수의 정보를 넣을 수 있다. 페이로드에 인증에 필요한 정보와 유효기간을 담을 수 있으며, 이렇게 key-value 형식으로 이뤄진 정보들을 claim이라고 한다. 특정 해싱 알고리즘을 선택하여 JWT를 암호화할 수 있으며, 이 때 개발자가 임의로 정한 비밀키를 사용한다.

 

 

  • Signature
    토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드이다.
    서명(Signature)은 위에서 만든 헤더(Header)와 페이로드(Payload)의 값을 각각 BASE64로 인코딩하고, 인코딩한 값을 비밀 키를 이용해 헤더(Header)에서 정의한 알고리즘으로 해싱을 하고, 이 값을 다시 BASE64로 인코딩하여 생성한다.
    즉, 토큰의 유효성 검증을 위한 문자열. ->서버에서는 이 문자열로 유효한 토큰인지 검증한다.

 

header와 payload는 단순히 인코딩된 값이지만, signature는 비밀키가 없으면 복호화가 불가능하다.

따라서 signature는 토큰의 위변조 여부를 확인하는 데 사용된다.

 

# JWT 장단점

 

  • 장점

가장 큰 장점으로는 서버에 인증 정보에 대한 세션과 같은 저장소가 필요 없어서 부하가 훨씬 덜 걸리며 의존성이 없다

서버가 상태성을 갖지 않고도 회원을 인증/인가하고 필요한 서비스를 제공해줄 수가 있다.

OAuth와 같은 경우에는 토큰을 기반으로 다른 로그인 시스템에 접근 및 권한 공유도 가능하다.

 

JWT를 이용하면 따로 서버의 메모리에 저장 공간을 확보할 필요가 없다. 서버가 토큰을 한번 클라이언트에게 보내주면 클라이언트는 토큰을 보관하고 있다가(가장 쉬운 방법은 localstorage에 저장하는 것이다) 요청을 보낼때마다 헤더에 토큰을 실어보내면 된다. 쿠키를 사용할 수 없는(쿠키는 웹브라우저에서 사용할수 있는 기능이다!) 모바일 어플리케이션에는 JWT를 사용한 인증방식이 최적이다.

 

  • 단점

JWT 처리 자체의 비용이 있으므로 인증 요청이 많아질수록 네트워크 부하가 심해질 수 있다.

payload는 암호화가 되어있지 않으므로 유저의 critical한 정보를 담을 순 없다.

토큰을 탈취당하면 대처하기 어렵다는 큰 보안적 이슈가 있다.

클라이언트에 저장 되므로 서버에서 조작하기 어려움.


 

# 세션 vs Token

 

Q) 브라우저에 사용자 로그인 정보를 저장한다면?

 

A) DB에 저장된 사용된 계정의 해시값 등을 꺼내온 정보가 사용자의 암호를 복잡한 알고리즘으로 계산한 값과 일치하는지 확인하는 과정이 필요하여 사이트에서 모든 활동에 대해 실행하기 굉장히 부담되는 작업이다.

시간+자원 손해/매번 아이디,패스워드 정보가 이동되면 보안상 취약

 

이러한 취약성을 보안하기 위해 전통적으로 사용된 '세션'

 

세션방식은 사용자가 로그인에 성공하면 서버는 '세션 표딱지'란 걸 출력한다.(a.k.a 입장표)

사용자 브라우저 측에 반쪽 찢은 세션표,

다른 반쪽은 자기 책상(메모리),서랍(하드디스크),데이터베이스(창고)에 적합한 곳에 둔다.

 

브라우저가 이 표를 Session ID란 이름의 쿠키1로 저장하고 해당 사이트에 요청을 보낼때마다 이 표를 같이 실어보낸다.

어떤 사용자가 서버에 로그인 된것이 지속되는 상태를 '세션'이라고 한다.

 

이 방식의 허점은 동시다발로 접속자가 많아지면 메모리 부족 현상이 발생, 서버에 문제가 생겨서 꺼져버리면 데이터가 다 날라감

 

서비스가 어느정도 규모가 있어서 서버를 여러대 두고 사이트를 운영할 때는 여러 서버가 가동중인데 클라이언트 요청이 들어오면 해당 요청으로 여러 서버들 사이에 분산을 해서 로드발란싱을 해주는데

 

만약 로그인은 1번 서버에 연결돼서 하고, 이메일 페이지로 가는 요청은 3번 서버로 분산되면

1번엔 세션이 있는데 3번엔 없으면 세션이 유지되지 않는다

 

그렇다고 요청이 각자 할당된 서버로만 보내지는 것도 번거로운 일

이 부담을 없애기 위해 '토큰 방식'이 출현한다.

 

# JSON WEB TOKEN . 줄여서 JWT 

 

사용자가 받는 토큰은 아래와 같이 마침표기준으로 3개의 데이터(headerpayloadverify signature)로 조합되어 있다.

 

Base64로 인코딩되어 있다면 사용자가 자바스크립트로 뭐든 다시 디코딩 작업으로 볼 수 있고, 악용이 될 수 있는건데?

 

하지만 토큰은 1헤더, 3번 서명 데이터로 인해서 보안할 수 있다.

 

1번 헤더는 디코딩하면 두가지 정보가 담겨있다.

a. 토큰 타입 (=JWT)

b. alg(알고리즘약자) 알고리즘 타입

 

1번 헤더, 2번 페이로드 그리고 '서버에 감춰놓은 비밀 값'이 셋을 암호화 알고리즘에 넣고 돌리면 3번 서명값이 나온다!

 

서버는 요청에 토큰 값이 실려들어오면 1번,2번의 값을 '서버에 감춰놓은 비밀 값'와 함께 돌려봐서 계산된 결과값이 3번 서명 값과 일치하는 결과가 나오는지 확인한다.

 

3번 서명 값과 계산 값이 일치하고 유효기간도 지나지 않았다면 사용자는 로그인 된 회원으로서 인가를 받을 수 있다.

 

서버는 사용자들의 상태를 어딘가에 따로 기억해 둘 필요가 없이 이 비밀 값만 손에 쥐고 있다면, 요청이 들어올때마다 스캔하여 사용자들을 걸러낼 수 있다!

이처럼 상태값을 갖지 않는걸 stateless, 세션은 반대로 stateful이다.

 

그런데 세션과 토큰 모두 존재 목적은 거의 같지만 

그 중 가장 큰 차이점은 세션은 데이터베이스 서버에 저장된다는 것,

토큰은 클라이언트 측에서만 저장한다는 점

 

세션은 DB에 저장되기에 항상 DB와 커넥션이 일어나고 상태를 유지해야하는 반면(stateful)

 

쿠키는 맨 처음 생성될때는 DB를 거치겠지만 DB가 클라이언트에 토큰값을 주면 JWT가 만료되는 refresh 토큰 등을 제외하면 DB와 커넥션 없이 클라이언트에만 저장되어 DB와 상태를 유지할 필요가 없다(stateless)

 

 

 

세션의 기능을 대신할 수 있고 관리도 편하면 JWT가 언제나 정답이 되지 않을까??

 

답은 그렇지 않다

 

예를 들면 한 기기에서 로그인 가능한 서비스를 만들려는 경우 PC에서 로그인한 상태의 어떤 사용자가 핸드폰에서 또 로그인하면 PC에서는 로그아웃되도록 기존 세션을 종료할 수 있는 것.

 

세션은 서버에서 끊어버리면 되는데, JWT에서는 불가능한 일이다.

사용자에서 이미 건낸 토큰을 뺏을 수도 없고, 토큰 발급 내역이나 정보를 서버가 기록하고 있는 것도 아니다

 


 

그래서 보안 방법으로 토큰을 두개 발급한다.

수명이 몇시간~ 몇 분 이하로 짧은 access토큰 꽤 길게, 보통 2주 정도로 잡혀 있는 refresh 토큰이다.

 

두 토큰을 발급하고 사용자에게 보내고 나서 refresh 토큰은 상응값을 데이터베이스에도 저장한다.

 

클라이언트는 access토큰의 수명이 다하면 refresh 토큰을 보낸다.

서버는 그걸 데이터베이스에 저장된 값과 대조해보고, 맞다면 새 access토큰을 발급해준다.

 

인가를 받을 때 쓰는 수명이 짧은 토큰이 access토큰,

엑세스 토큰을 재발급 받을 때 쓰는 것이 refresh 토큰이다.

 

그렇게 되면 엑세스 토큰이 탈취 당해도 유효기간을 그리 길지 않다.

누구를 강제 로그아웃 시키려면 리프레시 토큰을 갖다가 DB에서 지워버려가지고 토큰 갱신이 안 되게 하면 되는 것~!

 

그치만 토큰이 살아 있는 동안에는 암것도 대처할 수 없는게 큰 약점이긴하다.

 


# Refresh Token

 

세션은 탈취된 경우 세션 저장소에서 탈취된 세션 ID를 삭제하면되지만, JWT 는 서버에서 관리하지 않기 때문에 속수무책으로 당할 수 밖에 없음.

 

그래서 탈취되어도 피해가 최소한 되도록 유효시간을 짧게 가져감.

 

하지만 만료 시간을 30분으로 설정하면 일반 사용자는 30분마다 새로 로그인하여 토큰을 발급받아야 함.

사용자가 매번 로그인 하는 과정을 생략하기 위해 필요한게 Refresh Token.

 

 

 

# 발급과정

 

  1. Refresh Token 은 로그인 토큰(Access Token) 보다 긴 유효 시간을 가지며, Access Token 이 만료된 사용자가 재발급을 원할 경우 Refresh Toekn을 함께 전달함.
  2. 서버는 Access Token 에 담긴 사용자의 정보를 확인하고 Refresh Token 이 아직 만료되지 않았다면, 새로운 토큰을 발급해줌

Refresh Token 은 사용자가 로그인할 때 같이 발급되며, 클라이언트가 안전한 곳에 보관하고 있어야 함.

Access Token과 달리 매 요청마다 주고 받지 않기 때문에 탈취 당할 위험이 적으며, 요청 주기가 길기 때문에 별도의 저장소에 보관함. (정책마다 다르게 사용)

 

# Refresh Token 저장소

 

Refresh Token 은 서버에서 별도의 저장소에 보관하는 것이 좋다.

  • Refresh Token 은 사용자 정보가 없기 때문에 저장소에 값이 있으면 검증 시 어떤 사용자의 토큰인지 판단하기 용이
  • 탈취당했을 때 저장소에서 Refresh Token 정보를 삭제하면 Access Token 만료 후에 재발급이 안되게 강제 로그아웃 처리 가능
  • 일반적으로 Redis 많이 사용

 

# 재발급 시나리오

 

 

1. 클라이언트에서 ID와 비밀번호를 넘기며 로그인 요청한다.

2. ID와 비밀번호의 유효성을 검증하고 access token과 refresh token을 발급한다. access token의 만료 기간은 15분, refresh token의 만료기간은 2주이다.

3. 클라이언트에서 서버로 인증/인가가 필요한 서비스를 요청시 access token을 보낸다.

4. 서버에서 access token을 통해 인증/인가를 마치고 서비스를 제공한다.

5. access token이 만료되면 클라이언트는 refresh token을 통해 access token 재발급 요청을 한다. access token이 만료되기 직전에 클라이언트에서 스스로 요청하거나, 만료되고 난 뒤 서버에서 401 에러 응답을 받게 되면 그 때 재발급 요청을 해도 된다. 

6. 서버에서 refresh token의 유효성을 검증 후, access token과 refresh token을 재발급하여 클라이언트로 보내준다.

 

4 - 5 번의 검증이 끝나면 새로운 토큰 세트 (access + refresh ) 발급 후 서버는 refresh token 저장소의 value 업데이트