JWT 기반 인증에서 Access Token과 Refresh Token이 어떻게 동작하는지, 왜 두 개의 토큰이 필요한지 알아봅니다.
긴 만료 시간 (예: 7일)
짧은 만료 시간 (예: 15분)
| 토큰 | 만료 시간 | 용도 | 저장 위치 |
|---|---|---|---|
| Access Token | 15분 | API 호출 | 메모리/localStorage |
| Refresh Token | 7일 | Access Token 갱신 | HttpOnly Cookie |
[사용자] ──── ID/PW ────→ [서버]
│
←── Access Token ─┤ (15분 유효)
←── Refresh Token ┘ (7일 유효, HttpOnly Cookie)// 서버 - 로그인 API
@PostMapping("/auth/login")
public Response login(String username, String password) {
// 1. 인증
authenticate(username, password);
// 2. Access Token 생성 (15분)
String accessToken = jwtProvider.generateAccessToken(username);
// 3. Refresh Token 생성 (7일)
String refreshToken = jwtProvider.generateRefreshToken(username);
// 4. Refresh Token을 Redis에 저장 (서버 측 검증용)
redisService.save(username, refreshToken);
// 5. Refresh Token을 HttpOnly Cookie로 설정
response.addCookie(createHttpOnlyCookie(refreshToken));
// 6. Access Token 반환
return Response.ok(accessToken);
}
[클라이언트] ── Access Token ──→ [서버]
│
←── 데이터 ─────────┘// 클라이언트 - API 호출
const response = await fetch('/api/data', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
[15분 경과]
[클라이언트] ── 만료된 Access Token ──→ [서버]
←── 401 Unauthorized ────┘
[클라이언트] ── Refresh Token (Cookie) ──→ [서버]
←── 새 Access Token ────────┘
[클라이언트] ── 새 Access Token ──→ [서버]
←── 데이터 ───────────┘// 클라이언트 - 자동 토큰 갱신 (Axios Interceptor 예시)
api.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401) {
// Refresh Token으로 새 Access Token 요청
const { data } = await axios.post('/auth/refresh');
// 새 토큰으로 원래 요청 재시도
error.config.headers['Authorization'] = `Bearer ${data.accessToken}`;
return axios(error.config);
}
return Promise.reject(error);
}
);
[7일 경과]
[클라이언트] ── Refresh Token (만료) ──→ [서버]
←── 401 Unauthorized ──────┘
→ 재로그인 필요!Day 1, 09:00 로그인
├─ Access Token 발급 (만료: 09:15)
└─ Refresh Token 발급 (만료: Day 8, 09:00)
Day 1, 09:15 Access Token 만료
└─ Refresh Token으로 갱신 → 새 Access Token (만료: 09:30)
Day 1, 09:30 Access Token 만료
└─ Refresh Token으로 갱신 → 새 Access Token (만료: 09:45)
... (15분마다 자동 갱신) ...
Day 7, 23:45 여전히 정상 동작
└─ Refresh Token 아직 유효
Day 8, 09:00 Refresh Token 만료
└─ 갱신 실패 → 재로그인 필요!저장: localStorage 또는 메모리
전송: Authorization 헤더
위험: XSS 공격에 노출 가능
대책: 짧은 만료 시간 (15분)저장: HttpOnly Cookie
전송: 자동 (Cookie)
보호:
- HttpOnly → JavaScript 접근 불가 (XSS 방어)
- Secure → HTTPS만 전송
- SameSite=Strict → 다른 사이트에서 전송 불가 (CSRF 방어)// Refresh Token 검증
public boolean validateRefreshToken(String username, String token) {
String savedToken = redis.get("refresh_token:" + username);
return savedToken != null && savedToken.equals(token);
}
A: 여러 겹의 보안으로 보호됩니다.
만약 탈취되어도:
A: 15분 후 만료되어 피해 최소화
A: 클라이언트가 토큰 갱신을 시도할 수 있도록
403 Forbidden → "권한 없음" → 갱신 시도 안 함
401 Unauthorized → "인증 필요" → 갱신 시도/auth/refresh)JWT 기반 인증에서 두 개의 토큰을 사용하는 이유:
이 패턴은 대부분의 모던 웹 애플리케이션에서 사용되는 표준적인 방식입니다.
| Jooq란 무엇인가? (0) | 2025.06.03 |
|---|---|
| @EventListener 완벽 가이드 (0) | 2025.06.03 |
| Liquibase로 DB 변경 이력 관리하기 (0) | 2025.06.02 |
| Spring Boot + GitLab CI/CD 배포 방법 (3) | 2025.06.01 |
| [Spring & Swagger] Failed to load API definition. (0) | 2025.05.21 |