JWT, JJWT란?
JWT는 Json Web Token의 약자로, Json 형식의 데이터로 이루어진 인증 티켓 같은 거라고 생각해볼 수 있다.

위 사진과 같이 JWT는 Header, Payload, Signature로 이루어져 있다.

각 부분에는 위 사진과 같은 내용(키-값 쌍)들이 포함되어있다.
[Header]
| alg | 서명(Signature)에 사용할 암호화 알고리즘 (예: HS256, RS256 등) |
| typ | 토큰의 타입 (대부분 "JWT") |
이 객체는 Base64URL 방식으로 인코딩되어 JWT의 첫 번째 부분을 이룬다.
[Payload]
| sub | 토큰의 주제(Subject, 주로 사용자 ID) |
| name | 사용자 이름 |
| role | 권한(Role) |
| iat | 토큰 발급 시각 (issued at) |
| exp | 토큰 만료 시각 (expiration) |
[Signature]
| secret | 서버가 가진 비밀 키 (절대 노출되면 안 됨) |
| HMACSHA256 | 해싱 알고리즘 |
- 결과적으로 Signature는 “Header + Payload” 조합이 변조되지 않았다는 것을 증명해준다.
전체 구조
[Header]
{
"sub": "1234567890",
"name": "유동훈",
"role": "user",
"iat": 1698652000,
"exp": 1698655600
}
[Payload]
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuyKteyViO2VnCIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNjk4NjUyMDAwLCJleHAiOjE2OTg2NTU2MDB9
[Signature]
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
실제 JWT 모습
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuyKteyViO2VnCIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNjk4NjUyMDAwLCJleHAiOjE2OTg2NTU2MDB9.
TJVA95OrM7E2cBab30RMHrHDcEfxJoZQnQvV3u8bU8U
JWT 적용 예시 with Servlet

컨트롤러 일부 발췌
@WebServlet("/user")
public class UserController extends HttpServlet implements ControllerHelper {
private static final long serialVersionUID = 1L;
private UserService userService;
private RefreshTokenService refreshService;
private JwtUtil jwt;
@Override
public void init() throws ServletException {
// 1) Service 주입 (리스너가 context에 넣어줬다면 거기서 꺼내기)
this.userService = (UserService) getServletContext().getAttribute("userService");
this.refreshService = (RefreshTokenService) getServletContext().getAttribute("refreshTokenService");
// 2) 시크릿 로딩: 환경변수 -> 없으면 web.xml context-param -> 그래도 없으면 예외
String b64 = System.getenv("JWT_SECRET_BASE64");
if (b64 == null) {
b64 = getServletContext().getInitParameter("JWT_SECRET_BASE64");
}
if (b64 == null) {
throw new ServletException("JWT_SECRET_BASE64 is missing. Set env or context-param.");
}
byte[] secretBytes;
try {
secretBytes = java.util.Base64.getDecoder().decode(b64);
} catch (IllegalArgumentException e) {
throw new ServletException("JWT_SECRET_BASE64 is not valid Base64.", e);
}
this.jwt = new JwtUtil(secretBytes);
}
코드 속 2번 항목처럼 환경 변수에서 시크릿 키를 받아오는 걸 확인할 수 있다.
이후에는 JwtUtil 클래스를 만들어서 JWT의 내부적인 동작을 정의해주면 된다.
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.time.Instant;
import java.util.Date;
public class JwtUtil {
private final Key key;
public JwtUtil(byte[] secretBytes) {
this.key = Keys.hmacShaKeyFor(secretBytes);
}
// 토큰 발급
public String create(long userIdx, long ttlSeconds) {
Instant now = Instant.now();
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.claim("userIdx", userIdx)
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(now.plusSeconds(ttlSeconds)))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
// 검증 + 클레임 추출
public Jws<Claims> parse(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.setAllowedClockSkewSeconds(0)
.build()
.parseClaimsJws(token);
}
public long getUserIdx(String token) {
return parse(token).getBody().get("userIdx", Number.class).longValue();
}
public long verifyAndGetUserId(String token) {
return getUserIdx(token);
}
}
이후에는 JWT가 발급되는 순간들을 만들어줘야 한다.
통상적으로는 로그인이 성공하면 해당 유저에게 토큰을 발급하게 되는데 로그인 상황을 한 번 정의해보자.
private void signin(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String email = request.getParameter("email");
String password = request.getParameter("password");
// TODO: password 암호화
UserDto loginUser = userService.signin(email, password);
if (loginUser == null) {
redirect(request, response, "/user?action=signin-form");
return;
}
// 1) 토큰 발급
String accessToken = jwt.create(loginUser.getId(), 60 * 60 * 6);
String refreshToken = jwt.create(loginUser.getId(), 60 * 60 * 24);
// 2) 검증용 토큰 저장
refreshService.save(loginUser.getId(), refreshToken, 60 * 60 * 24);
// 3) 두 토큰을 HttpOnly 쿠키로 심기
addHttpOnlyCookie(response, "accessToken", accessToken, 60 * 60 * 24);
addHttpOnlyCookie(response, "refreshToken", refreshToken, 60 * 60 * 24 * 7);
redirect(request, response, "/");
}
JwtUtil을 이용해서 로그인에 성공하면 토큰을 jwt.create 해주고 재발급용 토큰인 refreshToken을 DB에 저장해주면 된다.
또한 사용자에게 직접 토큰을 전송하기 위헤서 쿠키를 활용하면 된다.
여기서 일반적으로 쿠키의 유효 기간은 JWT의 유효 기간보다 길게 설정해주어야 한다.
쿠키 유효 기간 vs JWT 유효 기간
JWT는 서버가 유효성을 검증하는 토큰이고, 쿠키는 클라이언트가 그 토큰을 서버로 전달하는 수단이기 때문이다.
만약 쿠키가 먼저 만료되어버리면, 아직 유효한 JWT가 남아 있더라도 클라이언트가 서버에 전달할 수 없게 된다. 즉, 서버는 토큰의 만료 여부조차 확인할 수 없게 된다.
따라서 쿠키의 유효 기간은 JWT보다 길게 설정해야, 서버가 JWT 만료 여부를 정상적으로 판단할 수 있다.
Access Token vs Refresh Token

앞서 말했듯, JWT는 두가지 종류가 존재한다.
- Access Token
- Refresh Token
이 둘은 각각 상호보완적으로 불 수 있다.
일반적으로 사용자는 로그인에 성공하면 서버로부터 Access Token과 Refresh Token, 두 가지를 함께 발급받는다.
- Access Token은 실제로 요청 시 인증을 수행하는 토큰으로, 유효 기간이 짧게 설정되어 있다. (예: 30분 ~ 1시간)
- Refresh Token은 Access Token이 만료되었을 때, 새로운 Access Token을 재발급받기 위한 용도로 사용된다. 보통 유효 기간이 훨씬 길다. (예: 2주 ~ 1달)
사진에서 볼 수 있듯, 서버는 로그인 시 AT와 RT를 함께 발급하고 이 토큰들을 클라이언트(브라우저)에 쿠키 형태로 전달한다. 이후 클라이언트는 요청을 보낼 때마다 Access Token이 담긴 쿠키를 서버로 함께 전송한다. 문제는 Access Token이 만료되었을 때다. 이 경우 서버는 클라이언트의 요청을 처리하지 않고 401 Unauthorized 상태 코드(만료 응답이자 재발급 신호)를 보낸다.
이 시점에서 클라이언트는 자신이 가지고 있던 Refresh Token을 서버로 다시 보내 새로운 Access Token을 발급받는다.
만약 Refresh Token까지 만료되었거나 위·변조되었다면, 이제는 더 이상 재발급이 불가능하므로 다시 로그인을 해야 한다.
Signature 알고리즘 작동 방식

대칭키 방식 (HS256)
서버 혼자 비밀키(secret key)를 가지고 서명과 검증을 모두 수행하는 구조
- 비밀키 생성
- 서버는 JWT에 서명할 때 사용할 비밀키(secret key) 를 생성한다.
- 이 키는 오직 서버 내부에만 존재하며 외부로 노출되지 않는다.
- 클라이언트 로그인 요청
- 클라이언트가 서버로 로그인 정보를 전송한다.
- 로그인 성공 → JWT 발급
- 서버는 로그인 정보를 검증한 뒤,
- 사용자 정보를 포함한 JWT를 생성한다.
- 이때 비밀키로 서명(Signature)을 추가하여 토큰을 완성한다.
- JWT는 응답으로 클라이언트에 전달된다.
- 요청 시 JWT 포함
- 클라이언트는 이후 API 요청마다
- 이 JWT를 쿠키나 Authorization 헤더에 포함시켜 전송한다.
- 서버의 비밀키 검증
- 서버는 요청을 받으면 자신의 비밀키로 서명을 검증한다.
- 서명이 유효하다면 토큰이 위변조되지 않았음을 확인할 수 있다.
- 응답 반환
- 검증이 완료되면 서버는 정상적으로 요청을 처리하고 응답을 돌려준다.

비대칭키 방식 (RS256)
공개키와 개인키를 분리하여 사용하는 방식
(개인키로 서명하고, 공개키로 검증)
- 개인키 생성
- 서버는 서명용 개인키(private key) 를 생성한다.
- 공개키 생성
- 개인키로부터 검증용 공개키(public key) 를 만들어낸다.
- 이 공개키는 외부(다른 서버, 인증 서버, 클라이언트 등) 에 공유될 수 있다.
- 클라이언트 로그인 요청
- 클라이언트가 서버로 로그인 요청을 보낸다.
- 로그인 성공 → JWT 발급 및 전송
- 서버는 개인키로 JWT에 서명(Signature)을 추가하여 토큰을 완성하고, 그 JWT를 클라이언트에 전달한다.
- 요청 시 JWT 포함
- 클라이언트는 이후 요청마다 JWT를 포함시켜 서버로 보낸다.
- 공개키로 검증
- 서버(또는 인증 서버)는 JWT의 서명을 공개키로 검증한다.
- 공개키로 검증이 성공하면 토큰이 정상 발급된 것임을 확인할 수 있다.
- 응답 반환
- 검증이 끝나면 서버는 요청을 처리하고 정상 응답을 전송한다.
| 구분 | HS256 (대칭키) | RS256 (비대칭키) |
| 키 구조 | 하나의 비밀키로 서명+검증 | 개인키로 서명, 공개키로 검증 |
| 검증 주체 | 서버 자신만 가능 | 공개키를 가진 누구나 가능 |
| 보안 수준 | 비교적 단순, 빠름 | 더 안전하지만 복잡 |
| 활용 예시 | 단일 서버 환경 | 마이크로서비스 / OAuth / 외부 인증 연동 |
클라이언트 - 서버 간의 JWT 시퀀스 다이어그램

1~4. 로그인 및 토큰 발급
Client → Server : 로그인 요청
사용자가 아이디/비밀번호를 입력해 서버로 전송.
1. Server → DB : 사용자 조회
- 서버가 DB에서 해당 유저 정보를 조회.
2. DB → Server : 사용자 데이터 반환
- 유저가 존재하고 비밀번호가 일치하면 정보 반환.
3. Server → Client : JWT 발급 및 전달
- 서버는 로그인 성공 시,
- Access Token (단기 인증용)
- Refresh Token (재발급용)
- 두 가지 JWT를 생성해 클라이언트에 전달.
5~6. 요청 시 인증 처리
5. Client → Server : API 요청 (JWT 포함)
- 클라이언트는 이후 요청마다 JWT(Access Token)를 쿠키나 헤더에 실어 보냄.
6. Server : Access Token 검증
- 토큰이 유효하면 요청 정상 처리
- 만료되었다면, Refresh Token을 이용한 재발급 로직으로 분기
6-1~6-3. 재발급 및 재요청
6-1. Server → DB : Refresh Token 조회
- DB에 저장된 Refresh Token을 조회해 유효한지 확인.
6-2. Server : 새로운 Access Token 생성
- Refresh Token이 유효하면 새 Access Token을 생성.
6-3. Server → Client : 새로운 토큰 전달 후 응답
- 새 토큰을 클라이언트에 전송하고, 요청을 다시 처리하여 응답 반환.
'Java > TIL' 카테고리의 다른 글
| [Java] 일급 컬렉션(First-Class-Collection)이란? (1) | 2024.11.02 |
|---|---|
| [Java] 안드로이드와 웹 서버 간 REST API 통신하기 – Retrofit 활용 (1) | 2024.10.24 |