대부분의 서비스에서 기본적으로 구현하는 로그인 기능은 사용자의 신원을 확인하는 인증(Authentication) 을 담당하며, 인증이 완료되면 해당 사용자의 권한에 따른 인가(Authorization) 로직이 이어진다.
인증(Authentication) 누구인가? - 사용자의 신원을 확인하는 절차
인가(Authorization) 무엇을 할 수 있는가? - 인증된 사용자가 특정 리소스에 접근할 수 있는 권한이 있는지를 판단
이러한 인증·인가 절차를 구현하기 위해, 현대 웹 개발에서 사실상 표준처럼 사용되는 방식이 바로 JWT(JSON Web Token) 이다. 오늘은 JWT의 개념과 사용 이유를 정리하고, 관리하는 전략에 대해서 알아보고자 한다.
JWT는 사용자의 정보를 담은 JSON 객체를 Base64Url로 인코딩하고, 여기에 서명(Signature) 을 추가하여 생성된 토큰이다. 이 토큰은 인증·인가 정보를 클라이언트에 안전하게 전달하기 위해 사용된다.
JWT를 생성할 때는 secret key가 필요하며, 이 키가 노출되지 않았다는 전제 하에 토큰의 위·변조가 불가능하기 때문에 안전한 방식으로 간주된다.
⚠️ JWT는 위·변조를 막는 것일 뿐, 정보를 암호화하지는 않는다.
Base64Url 인코딩된 내용은 누구나 디코딩할 수 있기 때문에, 비밀번호나 주민등록번호와 같은 민감 정보를 절대 담아서는 안 된다.
⚠️ JWT는 토큰 탈취 자체를 방지하지는 못한다.
따라서 XSS, CSRF와 같은 공격에 주의해야 하며, 이를 막기 위해 HttpOnly 쿠키 사용, 짧은 유효기간 설정, HTTPS 전송 등이 필요하다.
JWT는 .
으로 구분되어 있는 총 3개의 부분으로 구성된 문자열이다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvZSIsImlhdCI6MTUxNjIzOTAyMn0.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
각 부분을 디코딩하면 JSON 객체를 얻을 수 있는데, 각 부분이 담고있는 정보에 대해서 알아보자.
헤더는 해당 토큰의 암호화 알고리즘과, 토큰 타입이 기재되어 있는 부분으로 Base64Url로 인코딩된다.
{
"alg": "HS256",
"typ": "JWT"
}
페이로드는 사용자의 정보나 토큰의 메타정보를 담고 있다. jwt를 만들 때 개발자가 해당 부분에 들어갈 데이터를 정의할 수 있으며, 이 또한 Base64Url로 인코딩되기 때문에 디코딩 시, 누구나 데이터를 확인할 수 있어 민감한 정보를 담지 않도록 해야한다.
{
"sub": "1234567890",
"name": "Joe",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
각 데이터를 클레임이라 부르며, 아래의 표는 대표적인 클레임 예시이다.
클레임 | 의미 |
---|---|
iss | 발급자 (issuer) |
sub | 사용자 고유 ID (subject) |
aud | 대상자 (audience) |
exp | 만료 시간 (expiration, UNIX timestamp) |
iat | 발급 시간 (issued at) |
nbf | 이 시간 전에는 사용 불가 (not before) |
role , email | 사용자 정의 커스텀 클레임 |
서명은 앞의 두 부분(Header.Payload)을 secret key로 서명한 결과이다. 서버에서는 이 서명을 검증해, 토큰이 위조되지 않았는지 확인한다.
그러면 이렇게 만들어진 JWT 토큰은 어디에 저장하고, 어떻게 관리해야 안전하게 사용할 수 있을까?
JWT는 보안이 보장된 저장과 전송이 전제되지 않으면 오히려 취약점을 유발할 수 있기 때문에, 토큰의 저장 방식과 만료 전략을 함께 설계해야 한다.
초기에는 하나의 JWT 토큰만으로 인증을 처리하는 경우가 많았다. 사용자가 로그인하면 하나의 토큰을 발급하고, 이 토큰을 매 요청마다 헤더에 포함시켜 인증하는 방식이다.
그러나 이 방식은 몇 가지 한계점이 존재했다.
이러한 문제를 해결하기 위한 방안으로, 현재는 대부분 Access Token과 Refresh Token을 나누어 사용하는 구조를 채택하고 있다.
항목 | Access Token | Refresh Token |
---|---|---|
목적 | 인증된 사용자인지 확인 | 새로운 Access Token 발급 |
특징 | 짧은 수명 (10~30분), 요청마다 포함 | 긴 수명 (7~30일), 서버 저장 및 검증 |
담는 정보 | 사용자 식별자, 권한, 만료 시간 등 | 최소한의 사용자 정보 혹은 토큰 ID |
Access Token은 사용자의 요청에 대한 권한을 검증하는 데 사용되며, Refresh Token은 Access Token이 만료되었을 때 새로운 토큰을 발급받기 위해 사용된다.
Refresh Token은 서버에 저장되거나 Redis에 상태로 보관되며, 탈취와 재사용을 막기 위해 RTR(Rotation) 방식이 권장된다.
토큰은 클라이언트 측에 저장되어야 하며, 대표적으로 세 가지 방식이 있다.
Access Token만 클라이언트 메모리(예: React 상태, 전역 변수, TanStack Query cache)에 저장하는 방식
JavaScript에서 접근 가능하지만, 페이지 새로고침 시 초기화되므로 XSS 노출 시간이 짧고 제한적
Refresh Token은 HttpOnly 쿠키에 저장하여 자동 갱신이 가능하도록 구성
✅ 장점:
새로고침 전까지 빠르게 재사용 가능
로컬스토리지보다 XSS 대응에 더 유리
❗ 단점:
페이지 새로고침 시 토큰 소멸 → 자동 로그인 구현에 Refresh 토큰 필수
JavaScript에서 접근할 수 없도록 설정된 쿠키
브라우저가 요청 시 자동으로 포함시켜 보내주기 때문에, 서버에서 쉽게 인증 처리 가능
쿠키에 설정 가능한 보안 옵션:
HttpOnly
: JS 접근 불가Secure
: HTTPS에서만 전송SameSite=Strict
: CSRF 공격 방지저장 방식 | 보안성 | 새로고침 후 유지 | XSS 취약성 | 주로 저장하는 토큰 |
---|---|---|---|---|
메모리 | 중간 | ❌ 유지 안됨 | ⚠️ 제한적 | Access (only) |
로컬스토리지 | 낮음 | 유지됨 | ❌ 매우 취약 | Access or Refresh |
HttpOnly 쿠키 | 높음 | 유지됨 | ✅ 안전 | Access / Refresh |
res.cookie('access_token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000, // 15분
});
Set-Cookie
헤더로 설정됨console.log(document.cookie); // access_token은 보이지 않음
HttpOnly
속성으로 인해 자바스크립트에서 쿠키에 접근할 수 없음항목 | 권장 방식 |
---|---|
Access Token | HttpOnly 쿠키에 저장, 짧은 수명 |
Refresh Token | HttpOnly + Secure 쿠키에 저장, Rotation 적용 |
토큰 갱신 방식 | Refresh Token으로 Access Token 재발급 (RTR) |
로그아웃 처리 | 서버에서 쿠키 삭제, Refresh Token 무효화 |
공격자가 사용자의 브라우저에 악성 스크립트를 삽입하여 민감한 정보를 탈취하는 공격
예방:
사용자가 로그인된 상태에서 공격자가 의도한 요청을 강제로 보내는 공격
예방:
내가 사용하는 스택은 Next.js (v15, App Router 기반) + NestJS 백엔드이다. 이 환경에서는 두 토큰 모두 HttpOnly 쿠키에 저장할 수밖에 없었다. 그 이유는 다음과 같다.
서버 컴포넌트에서 토큰을 사용하려면,
브라우저가 자동으로 서버에 토큰을 실어 보내야 한다.
→ 즉, 쿠키를 사용할 수밖에 없었다.
Next.js에서는 middleware.ts
를 통해 라우트를 보호하는데, 이 코드는 서버에서 실행되기 때문에 localStorage나 메모리에 접근할 수 없다.
const token = request.cookies.get('access_token')?.value;
→ 쿠키에 토큰이 없으면, 미들웨어에서 인증 로직 자체가 동작하지 않는다.
SSR(서버사이드 렌더링)에서는 요청 시점에 인증 정보가 필요하다. 쿠키는 브라우저가 서버에 자동으로 전송해주기 때문에,
서버에서 인증 상태를 쉽게 확인할 수 있다는 장점이 있다.
실전에서는 보안과 아키텍처 제약을 고려할 때, Access Token조차도 HttpOnly 쿠키에 저장하는 방식이 더 안전하고 안정적이었다.
이 선택은 단순한 기술 선택이 아닌, Next.js + NestJS 환경에서 실현 가능한 최선의 보안 전략이었다.