[Spring] JWT 02. AccessToken / RefreshToken 구조 + 재발급 흐름
JWT를 실제 서비스에서 쓰려면 보통 토큰을 2개로 나눕니다.
AccessToken은 짧게, RefreshToken은 길게 가져가서 보안을 올리는 방식입니다.
1. 왜 토큰을 2개로 나눌까?
AccessToken 하나만 오래 쓰면 보안이 약해집니다. (탈취되면 오래 위험)
그래서 AccessToken은 짧게(예: 10~30분), 만료되면 RefreshToken으로 재발급 받습니다.
| 구분 | AccessToken | RefreshToken |
|---|---|---|
| 역할 | API 요청 인증 | AccessToken 재발급 |
| 만료 | 짧음 (10~30분) | 김 (며칠~몇 주) |
| 저장 | 프론트 메모리/스토리지 | 가능하면 더 안전하게(쿠키/DB) |
2. 전체 인증 흐름 (가장 중요)
- 사용자 로그인(아이디/비밀번호)
- 서버가 로그인 성공 시 AccessToken + RefreshToken 발급
- 프론트는 요청할 때마다 AccessToken을 헤더에 실어 보냄
- AccessToken이 만료되면 401 발생
- 프론트가 RefreshToken으로 재발급 API 호출
- 서버가 RefreshToken을 검증하고 새 AccessToken(또는 새 RefreshToken도) 발급
3. (예제) 토큰 발급 코드 - AccessToken / RefreshToken
아래 예시는 jjwt를 사용해 Access/Refresh를 발급하는 기본 형태입니다.
실제 프로젝트에서는 RefreshToken을 DB에 저장해서 관리하는 경우가 많습니다.
import java.nio.charset.StandardCharsets;
import java.util.Calendar;
import java.util.Date;
import javax.crypto.SecretKey;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
public class JwtTokenServiceExample {
private static final String KEY_STR = "여기에_32자이상_랜덤키";
private static final SecretKey KEY =
Keys.hmacShaKeyFor(KEY_STR.getBytes(StandardCharsets.UTF_8));
// AccessToken: 30분
public static String generateAccessToken(String loginId, String loginLevel) {
Calendar c = Calendar.getInstance();
Date now = c.getTime();
c.add(Calendar.MINUTE, 30);
Date exp = c.getTime();
return Jwts.builder()
.signWith(KEY)
.issuer("my-service")
.issuedAt(now)
.expiration(exp)
.claim("loginId", loginId)
.claim("loginLevel", loginLevel)
.compact();
}
// RefreshToken: 7일(예시) = 7*24*60분
public static String generateRefreshToken(String loginId) {
Calendar c = Calendar.getInstance();
Date now = c.getTime();
c.add(Calendar.MINUTE, 7 * 24 * 60);
Date exp = c.getTime();
return Jwts.builder()
.signWith(KEY)
.issuer("my-service")
.issuedAt(now)
.expiration(exp)
.claim("loginId", loginId)
.compact();
}
}
4. (예제) RefreshToken으로 재발급 API 설계
Refresh 재발급은 보통 아래처럼 동작합니다.
- 프론트가 RefreshToken을 서버에 보냄
- 서버는 RefreshToken이 유효한지 확인
- 유효하면 새 AccessToken 발급
- (선택) RefreshToken도 함께 교체(로테이션)하면 더 안전
아래는 컨트롤러 형태 예시입니다.
@PostMapping("/api/account/refresh")
public ResponseEntity<Map<String, String>> refresh(@RequestBody Map<String, String> body) {
String refreshToken = body.get("refreshToken");
// 1) refreshToken 검증(서명/만료/issuer)
TokenVO tokenVO = tokenService.parse("Bearer " + refreshToken);
// 2) DB에 저장된 refreshToken과 일치하는지 확인(권장)
boolean ok = tokenService.checkRefreshToken(tokenVO, "Bearer " + refreshToken);
if(!ok) return ResponseEntity.status(401).build();
// 3) 새 accessToken 발급
MemberDto member = memberDao.selectOne(tokenVO.getLoginId());
String newAccess = tokenService.generateAccessToken(member);
Map<String, String> result = new HashMap<>();
result.put("accessToken", newAccess);
return ResponseEntity.ok(result);
}
5. 프론트에서 사용하는 방법
5-1) 일반 요청
axios.get("/api/mypage", {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
5-2) 만료 시 재발급 후 재요청
async function requestWithRefresh() {
try {
return await axios.get("/api/mypage", {
headers: { Authorization: `Bearer ${accessToken}` }
});
} catch (e) {
if (e.response && e.response.status === 401) {
// refresh 시도
const r = await axios.post("/api/account/refresh", {
refreshToken: refreshToken
});
accessToken = r.data.accessToken;
// 재요청
return await axios.get("/api/mypage", {
headers: { Authorization: `Bearer ${accessToken}` }
});
}
throw e;
}
}
6. 실행 결과 (Output 예시)
로그인 성공 시 서버가 토큰을 내려주면 예시는 아래처럼 나옵니다.
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9....(생략)",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9....(생략)"
}
AccessToken이 만료되어 API 호출이 실패하면 보통 401이 발생합니다.
HTTP/1.1 401 Unauthorized
이때 refresh API를 호출하면 새로운 AccessToken을 받을 수 있습니다.
{
"accessToken": "새로운AccessToken값"
}
7. 자주 하는 실수
- RefreshToken을 클라이언트에 그냥 오래 보관만 하고 서버 검증(저장) 안 함 → 탈취 시 위험
- AccessToken을 너무 길게 설정 → 보안 약해짐
- RefreshToken 재발급 시 기존 refresh를 폐기 안 함 → 세션 무한 증가 가능
마무리
AccessToken은 짧게, RefreshToken은 길게 가져가서
만료 처리와 보안을 동시에 챙기는 게 JWT 운영의 핵심입니다.
다음 글(3편)에서는 Spring Boot에서 실제로 Interceptor 또는 Security Filter로
로그인 필요한 API를 보호하는 방법을 정리해보겠습니다.
'Tech Stack > Spring Boot' 카테고리의 다른 글
| [Spring MVC] DispatcherServlet(디스패처 서블릿)이란 무엇인가? (0) | 2026.03.28 |
|---|---|
| [Spring] JWT 05. RefreshToken DB 저장 + 1회성 폐기(로테이션) + 재발급 흐름 완성 (0) | 2026.02.19 |
| [Spring] JWT 04. 만료 임박 자동 갱신(TokenRenewalInterceptor) 적용 (0) | 2026.02.19 |
| [Spring] JWT 03. Interceptor로 인증 적용하기 (로그인 필요 API 보호) (0) | 2026.02.19 |
| [Spring] JWT 01. Json Web Token 개념정리 (0) | 2026.02.18 |