개발자 취업준비/springboot

JWT 알아보기

naspeciallist 2025. 3. 30. 20:34


이 글은 스프링부트3 백엔드 개발자 되기 책을 바탕으로 공부한 내용을 정리한 게시글 입니다.

 

사용자가 서버에 접근 할 때 이 사용자가 인증된 사용자인지 확인하는 방법으로는 여러가지가 있습니다.

사용자 인증 확인 방법으로는 대표적으로 서버 기반 인증과 토큰 기반 인증이 있습니다.

 

저번 게시물에서 스프링시큐리티를 통해 기본적으로 제공받은 인증 방법은 세션 기반 인증 방법입니다. 세션기반 인증을 사용해 사용자마다 정보를 담은 세션을 생성하고 저장하여 인증을 하였습니다. 이를 세션 기반 인증이라고 합니다.

 

토큰기반 인증은 토큰을 사용하는 방법입니다. 토큰은 서버에서 클라이언트를 구분하기 위한  유일한 값인데 서버가 토큰을 생성하여 클라이언트에게 제공해 줍니다. 그러면 클라이언트는 이 토큰을 가지고 있다가 여러 요청때 이 토큰을 이용하여 요청을 하게 됩니다. 그러면 서버는 이 토큰을 확인하고 유효한 사용자인지 검증하게 됩니다.

 

 

1. 토큰 기반 인증이란?


 

위에서 설명했던것처럼 토큰기반인증은 토큰을 이용하여 인증을 처리하는 방법입니다.

 

1. 세션 쿠키 기반 인증방식

기존에는 쿠키와 세션으로 인증방식을 진행하였습니다. 로그인을 하였을 때 Set-Cookie의 형태로 반환을 받은 쿠키를 이용하여 로그인이 필요한 요청을 할 때마다 받은 쿠키를 이용하여 요청을 하는 동작 구조였습니다.

 

이처럼 쿠키를 사용하여 사용자의 정보를 저장함으로써 번거롭게 로그인이 필요한 요청마다 로그인을 해야 할 필요가 없어졌습니다. 하지만 쿠키를 이용하여 인증방식을 처리할 시 아래와 같은 문제점들이 발생하였습니다.

 

  • 쿠키는 노출이 되었을 때 id,pw 에 대한 민감 정보까지 노출이 되어 보안이 좋지 않다.
  • 웹 브라우저 마다 쿠키의 지원 형태가 다르기 때문에 다른 브라우저간 공유가 불가능하다.
  • 쿠키의 크기에 제한(4kb)이 있어 원하는 만큼 데이터를 담을 수 없다.
  • 서버는 매번 id,pw 를 받아서 인증을 해야 하는 불편함이 있어 조작된 데이터가 넘어오는 경우를 방지 할 수 없다.

쿠키의 개인민감 정보 유출을 막기 위해 나온것이 session 입니다.

쿠키를 이용한 사용자의 인증과정에서 개인 민감정보를 다룰 때 노출 될 수 있다는 단점을 보안하기 위해 session을 사용하게 되었습니다.

 

쿠키와 같은 매개체를 주고 받을 때 매번 id,pw를 작성해 전달해 인증을 처리하는 기존 방식을 보안하였습니다. id,pw를 주고 받는 것이 아는 인증 정보 자체를 특정 세션 저장소에 저장하고 이 값을 쿠키에 담아 클라이언트가 쿠키를 요청 할 때마다 세션 저장소에 있는 정보랑 동일 한지 비교하여 로그인을 확인하는 방식으로 session을 사용하여 기존에 쿠키를 이용한 인증 처리 방식을 보안하였습니다.

 

완전 쿠키만을 전송 할 때와 다르게 세션 저장소에 식별 할 수 있는 값을 넣어두고 그 값이 일치했는지 아닌지 확인을 하며 되므로 보안적인 측면에서 좀 더 유용해졌습니다. 만약 보안의 문제가 발생하여 탈취당한 세션 ID쿠키가 발생하더라도 세션 저장소를 지워버리면 탈취된 세션 ID로 서버에 접근해도 인증 수단이 없어져 접근을 막을 수 있다는 장점이 있습니다.

 

하지만 위와 같이 세션 ID를 탈취당해 세션저장소를 지워버리게 되는 등 모종에 이유로 세션저장소에 장애가 난다면 인증 전체에 문제가 생겨 정상적인 사용자가 인증을 하지 못하는 문제가 발생하게 됩니다.

 

또한 세션과 쿠키를 이용하여 인증과정을 처리하는 방식은 http의 장점인 stateless(무상태)를 위배합니다. 서버의 세션 저장소 라는 곳에 세션 ID 즉 상태를 저장하는 상황이기 때문에 stateful 하게 되는 것입니다. stateful의 단점은 해당 서버를 여러개로 늘려야하는 상황일 때 발생합니다. 사용자의 로그인 정보(세션)은 특정 서버에 저장되어 있기 떄문에 다른 서버에서는 사용자의 로그인 상태를 알 수 없다는 문제가 발생합니다.

 

세션을 사용하면서 단점으로 꼽히는 것은 요청이 진행할 때 마다 세션 저장소에 세션ID를 조회하는 작업을 통해서 DB접근이라는 로직이 한번 더 수행된다는 단점이 있습니다. 이러한 단점을 해결하기 위해 나타난 것이 토큰기반인증 입니다.

 

 

2. 토큰을 전달하고 인증받는 과정

토큰은 서버에서 클라이언트를 구분하기 위한 유일한 값인데 서버가 토큰을 생성하여 클라이언트에게 제공하면 클라이언트는 이 토큰을 가지고 있다가 여러 요청을 이 토큰과 함꼐 신청합니다. 그럼 서버는 토큰만 보고 유효한 사용자인지 검증합니다.

 

토큰은 요청한 응답과 함께 보냅니다. 실제 과정은 다음과 같습니다.

 

  1. 클라이언트가 아이디와 비밀번호를 서버에게 전달하면서 인증을 요청합니다.
  2. 서버는 아이디와 비밀번호를 확인해 유효한 사용자인지 검증합니다. 유효한 사용자이면 토큰을 생성하여 응답합니다.
  3. 클라이언트는 서버에서 준 토큰을 저장합니다.
  4. 이후 인증이 필요한 API를 사용해 토큰을 함께 보냅니다. 
  5. 서버는 토큰이 유효한지 검증합니다.
  6. 토큰이 유효하면 클라이언트가 요청한 내용을 처리합니다.

 

 

3. 토큰 기반 인증의 특징

토큰 기반 인증은 무상태성, 확장성, 무결성이라는 3가지 특징이 있습니다.

 

무상태성

무상태성은 사용자의 인증 정보가 담겨 있는 토큰이 서버가 아닌 클라이언트에 있으므로 따로 서버에 저장소를 만들어 저장할 필요가 없습니다. 서버에 저장소를 만들게 되면 그만큼 데이터를 유지해야 할 자원을 소비하게 됩니다. 하지만 토큰 기반 인증에는 클라이언트가 인증정보를 담긴 토큰을 생성하고 인증합니다. 그럼 클라이언트에서는 사용자의 인증상태를 유지하면서 이후 요청을 처리해야 하는데 이것을 상태를 관리한다고 합니다. 이렇게 하면 서버 입장에서는 클라이언트의 인증 정보를 저장하거나 유지하지 않아도 되기 때문에 완전한 무상태로 효율적인 검증을 할 수 있습니다.

 

 

확장성

무상태성을 가지게 되면서 확장에도 더 유용하게 됩니다. 서버를 확장할 때 상태관리에 신경을 쓰지 않아도 되므로 서버확장에 용이하게 됩니다. 예를들어 물건을 판매하는 서비스가 있고 결제를 위한 서버와 주문을 위한 서버가 분리되어있다고 가정하겠습니다. 세션과 쿠키를 사용하는 인증 방법에는 각각의 API에 인증을 해야하는 것과 달리 토큰 기반 인증에서는 토큰을 가지는 주체가 서버가 아니라 클라이언트이기 때문에 클라이언트가 가지고 있는 하나의 토큰으로 결제서버와 주문서버에 모두 인증 요청을 보낼 수 있습니다. 

 

 

무결성

토큰을 발급한 이후에는 토큰 정보를 변경하는 행위를 할 수 없습니다. 만약 토큰이 누군가에 의해 한글자라도 변경이 되면 서버는 유효하지 않은 토큰이라고 판단하게 됩니다. 즉 토큰의 무결성이 보장됩니다.

 

2. JWT (Json Web Token) 란?


 

JWT(Json Web Token)란 Json포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token입니다. JWT는 토큰 자체를 정보로 사용하는 Self-Contained 방식으로 정보를 안전하게 전달합니다.

 

JWT는 다음과 같은 로직으로 처리가 됩니다. 애플리케이션이 실행될 때 JWT를 static 변수와 로컬 스토리지에 저장하게 됩니다. static변수에 저장하게 되면 HTTP 통신을 할 때 마다 JWT를 HTTP 헤더에 담아서 보내야 하는데 이를 로컬스토리지에서 불러오게 되면 오버헤드가 발생하기 때문입니다. 클라이언트에서 JWT를 포함하여 요청을 보내면 서버는 허가된 JWT인지를 검사합니다. 또한 로그아웃을 할 경우 로컬 스토리지에 저장된 JWT 데이터를 제거합니다.

 

발급받은 JWT를 이용하여 인증을 하기 위해서는 HTTP 요청헤더 중에 Authorization 키값에 Bearer + JWT 토큰값을 넣어 보내야 합니다.

 

 

[ JWT 구조 ]

 

JWT는 .을 기준으로 헤더(header), 내용(payload), 서명(signature)으로 이루어져 있습니다.

 

aaaaaa. bbbbbb. cccccc
 해더    내용    서명

 

 

헤더에는 토큰의 타입과 해싱 알고리즘을 지정하는 정보를 담습니다. 아래의 경우 JWT 토큰, HS256 해싱 알고리즘을 사용한다는 내용 입니다.

{
 "typ" : "JWT"
 "alg" : "HS256"
}

 

해더의 구성에 대해서 더 자세하게 알아보겠습니다.

typ: 토큰의 타입을 지정합니다. JWT라는 문자열이 들어가게 됩니다.

alg: 해싱 알고리즘을 지정합니다.

 

내용에는 토큰과 관련된 정보를 담습니다. 내용의 한덩어리를 클레임이라고 부르며 클레임은 키 값의 한 쌍으로 이루어져 있습니다. 클레임은 등록된 클레임, 공개 클레임, 비공개 클레임으로 나눌 수 있습니다.

 

등록된 클레임은 토큰에 대한 정보를 담는데 사용합니다.

공개 클레임은 공개되어도 상관없는 클레임을 의미합니다. 충돌을 방지할 수 있는 이름을 가져야 하며 보통 클레임 이름을 URI로 짓습니다. 비공개 클레임은 공개되면 안되는 클레임을 의미합니다. 클라이언트와 서버 간의 통신에 사용됩니다.

예를 들어 다음과 같은 JWT의 내용이 있다고 하겠습니다.

{
 "iss: : "ajufresh@gmail.com" ,
 "iat" : 1622370878,
 "exp" : 1622372678,
 "http://easycoading.com/jwt_claims/is_admin":true,
 "email" : "ajufresh@gmail.com"
 "hello" : "안녕하세요!"
}

 

내용의 구성에 대해서 자세하게 알아보겠습니다.

 

iss: 토큰 발급자입니다. 위에서는 ajufresh@gmail.com이 토큰 발급자입니다.

iat: 토큰이 발급된 시간으로 iat는 issued at을 의미합니다.

exp: 토큰의 만료시간 입니다. 시간은 NumericDate 형식으로 하며 항상 현재 시간 이후로 설정합니다.

http://easycoading.com/jwt_claims/is_admin":true 는 URI로 네이밍된 공개 클레임입니다. 그 외에
 "email" : "ajufresh@gmail.com"  "hello" : "안녕하세요!"로 등록된 값은 비공개 클레임 값입니다.

 

이 외에도 jwt내용에는 sub, aud, nbf,jtl이 있습니다.

 

sub: 토큰 제목

aud: 토큰 대상자

nbf: 토큰의 활성 날짜 이날짜가 지나기 전의 토큰은 활성화 되지 않

jti: JWT의 고유 식별자로서 주로 일회용 토큰에 사용됩니다.

 

 

서명은 해당 토큰이 조작되었거나 변경되지 않았음을 확인하는 용도로 사용하며 헤더의 인코딩 값과 내용의 인코딩 값을 합친 후에 주어진 비밀키를 사용해 해시값을 생성합니다.

 

[ 리프레쉬 토큰 ]

 

토큰을 주고 받는 환경이 보안에 취약해 토큰 자체가 노출이 될 수도 있습니다. 토큰은 이미 발급되면 그 자체로 인증 수단이 되므로 서버는 토큰과 함께 들어온 요청이 토큰이 탈취한 사람의 요청인지 확인 할 수 없습니다.

 

만약 토큰의 유효기간이 하루라면 어떨까요? 하루 동안은 그 토큰으로 무엇이든 할 수 있으니 안좋은 상황입니다. 그러면 반대로 토큰의 유효기간이 짧으면 어떨까요? 그러면 사용자 입장에서 받은 토큰을 너무 짧은 시간만 활용할 수 있으니 불편합니다. 이런 불편한 지점을 해결하기 위해 리프레쉬 토큰이라는게 등장합니다. 리프레쉬 토큰은 사용자를 인증하기 위한 용도가 아니라 엑세스 토큰이 만료 되었을 때 새로운 엑세스 토큰을 발급받기 위해 사용합니다. 엑세스 토큰의 유효기간을 짧게 설정하고 리프레시 토큰의 유효기간을 길게 설정하면 공격자가 엑세스 토큰을 탈취해도 몇 분 뒤에는 사용할 수 없는 토큰이 되므로 더 안전해집니다.

 

리프레쉬 토큰의 사용과정입니다.

  1. 클라이언트가 서버에서 인증을 요청합니다.
  2. 서버는 클라이언트에서 전달한 정보를 바탕으로 인증 정보가 유효한지 확인한 뒤 엑세스 토큰과 리프레시 토큰을 만들어 클라이언트에게 전달합니다. 클라이언트는 전달 받은 토큰을 저장합니다.
  3. 서버에서 생성한 리프레시 토큰은 DB에도 저장해 둡니다.
  4. 인증을 필요로 하는 API를 호출 할 때 클라이언트에 저장된 엑세스 토큰과 함께 API를 요청합니다.
  5. 서버는 전달받은 엑세스 토큰이 유효한지 검사한 뒤에 유효하다면 클라이언트에서 요청한 내용을 처리합니다.
  6. 시간이 지나고 엑세스 토큰이 만료된 뒤에 클라이언트에서 원하는 정보를 얻기 위해 서버에게 API 요청을 보냅니다.
  7. 서버는 엑세스 토큰이 유효한지 검사 합니다. 만료된 토큰이면 유효하지 않기 때문에 토큰이 만료되었다는 에러를 전달합니다. 
  8. 클라이언트는 이 응답을 받고 저장해둔 리프래시 토큰과 함께 엑세스 토큰을 발급하는 요청을 전송합니다. 
  9. 서버는 전달받은 리프래시 토큰이 유효한지 DB에서 리프레시 토큰을 조회한 후 저장해둔 리프래시 토큰과 같은지 확인합니다.
  10. 만약 유효한 리프레시 토큰이라면 새로운 엑세스 토큰을 생성한 뒤 응답합니다. 그 이후에 클라이언트는 다시 API 요청을 합니다.