2단계
JWT · refresh · 회전
25 분
JWT · refresh · 회전
세션 관리의 실질 표준. 단 서명 알고리즘 · 만료 · 블랙리스트 세 가지를 정확히 이해해야 안전.
1. JWT 구조
<base64url header>.<base64url payload>.<base64url signature>
- header — 알고리즘 (HS256 · RS256)
- payload — 클레임 (sub · exp · iat 등)
- signature — 서명
payload 는 암호화 아님 · 단순 인코딩. 민감 정보 넣지 말 것.
2. HS256 vs RS256
| 알고리즘 | 키 | 용도 |
|---|---|---|
| HS256 | 대칭 (하나) | 단일 서비스 · 내부 |
| RS256 | 공개/개인 | 마이크로서비스 · OAuth |
단일 서비스는 HS256 + 32 바이트 이상 시크릿. 서비스 여러 개가 검증만 한다면 RS256.
3. 발급 (jose 라이브러리)
import { SignJWT } from "jose";
const SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);
async function issueToken(userId: string) {
return new SignJWT({ sub: userId })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("15m")
.sign(SECRET);
}
4. 만료 전략 — 짧은 access + 긴 refresh
access token → 15분
refresh token → 30일
access 가 탈취돼도 최대 15분. refresh 는 HTTP-only 쿠키로 보관, 요청 시마다 access 갱신.
async function refreshTokens(oldRefresh: string) {
const { payload } = await jwtVerify(oldRefresh, SECRET);
// 회전: 새 refresh 발급하고 old 를 블랙리스트
await db.query("INSERT INTO jwt_blacklist (jti, exp) VALUES ($1, $2)", [
payload.jti, payload.exp,
]);
return {
access: await issueToken(payload.sub),
refresh: await issueRefresh(payload.sub),
};
}
5. 블랙리스트 (또는 화이트리스트)
stateless JWT 의 약점: "로그아웃 즉시 무효화 불가". 해결 두 가지.
- 블랙리스트 — 로그아웃한 jti 저장 · 검증 시 조회
- 화이트리스트 — 발급한 모든 jti 저장 · 없으면 무효
블랙리스트가 저장 공간 적음 (로그아웃 드문 경우).
6. HTTP-only 쿠키 vs Authorization 헤더
| 저장 | XSS | CSRF | 편의 |
|---|---|---|---|
| HTTP-only 쿠키 | 안전 | 위험 | SSR 자동 |
| localStorage | 취약 | 안전 | CORS 설정 필요 |
쿠키 + sameSite=lax + CSRF 토큰 조합이 실용적.
7. 검증 미들웨어
import { jwtVerify } from "jose";
export async function requireAuth(req: Request) {
const token = req.headers.get("authorization")?.replace("Bearer ", "")
?? req.cookies?.get("access_token")?.value;
if (!token) throw new Error("unauthorized");
const { payload } = await jwtVerify(token, SECRET);
if (await isBlacklisted(payload.jti)) throw new Error("revoked");
return payload;
}
모든 API 라우트 첫 줄에서 호출 또는 Next.js middleware 로 일괄 적용.
8. 자주 걸리는 자리
- 시크릿 길이 부족 — 32 바이트 이상.
openssl rand -base64 32 - payload 에 비밀 정보 — base64 인코딩이지 암호화 아님
- 만료 너무 길음 — access 1 시간 이상은 위험
- 알고리즘 downgrade 공격 —
{"alg": "none"}수락 금지. 화이트리스트로 - 블랙리스트 TTL 없음 — 만료 시점 이후 자동 삭제 필요 (테이블 비대화)
9. 운영 체크리스트
- 시크릿 32 바이트 이상 · 환경변수
- access 15 분 · refresh 30 일
- refresh 회전 + 블랙리스트
- 로그아웃 시 쿠키 삭제 + 블랙리스트 추가
- 알고리즘 허용 목록 (HS256 만)
하고픈 말
JWT 는 "편리하지만 만료 · 회전을 안 하면 위험" 이 정확한 표현. access 짧게 · refresh 로 보상하는 구조가 기본.
Next
- 03-oauth-state-pkce