좋아! 이제 우리 진짜 실무에서 쓰이는 조합
✅ React SPA + Spring Boot 백엔드 + JWT 인증 + Redis 블랙리스트
흐름으로 업그레이드해볼게! 이거 한 번 이해하면 현대 인증 시스템의 핵심을 잡는 거야 💪
[1] 로그인 요청 (React → Spring)
↓
[2] Access Token + Refresh Token 발급 (Spring에서 응답)
↓
[3] Access Token: localStorage 저장
Refresh Token: HttpOnly 쿠키 저장 (보안!)
↓
[4] 매 요청 시: Access Token을 헤더에 포함하여 API 요청
↓
[5] Access Token 만료 시: Refresh Token으로 토큰 재발급 요청
↓
[6] 로그아웃: Redis에 Access Token 저장 (블랙리스트 처리)
// 로그인 함수
async function login(username, password) {
const res = await fetch('/api/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ username, password })
});
const data = await res.json();
localStorage.setItem('accessToken', data.accessToken);
// Refresh Token은 HttpOnly 쿠키로 자동 저장됨
}
@PostMapping("/api/login")
public ResponseEntity<?> login(@RequestBody LoginRequest req, HttpServletResponse response) {
// 인증 처리 생략
String accessToken = jwtUtil.generateAccessToken(user);
String refreshToken = jwtUtil.generateRefreshToken(user);
// Refresh Token은 HttpOnly 쿠키로
Cookie cookie = new Cookie("refreshToken", refreshToken);
cookie.setHttpOnly(true);
cookie.setPath("/");
cookie.setMaxAge(7 * 24 * 60 * 60); // 7일
response.addCookie(cookie);
return ResponseEntity.ok(Map.of("accessToken", accessToken));
}
fetch('/api/protected', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
}
});
async function refreshToken() {
const res = await fetch('/api/refresh', { method: 'POST', credentials: 'include' });
const data = await res.json();
localStorage.setItem('accessToken', data.accessToken);
}
@PostMapping("/api/refresh")
public ResponseEntity<?> refresh(HttpServletRequest request) {
String refreshToken = extractCookie(request, "refreshToken");
if (!jwtUtil.validate(refreshToken)) return ResponseEntity.status(401).build();
String username = jwtUtil.extractUsername(refreshToken);
String newAccessToken = jwtUtil.generateAccessToken(username);
return ResponseEntity.ok(Map.of("accessToken", newAccessToken));
}
@PostMapping("/api/logout")
public ResponseEntity<?> logout(@RequestHeader("Authorization") String authHeader) {
String token = authHeader.substring(7);
long exp = jwtUtil.getExpiration(token);
redisTemplate.opsForValue().set("blacklist:" + token, "logout", Duration.ofMillis(exp));
return ResponseEntity.ok().build();
}
if (redisTemplate.hasKey("blacklist:" + token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
| 위치 | 저장 내용 | 설명 |
|---|---|---|
| 🔐 localStorage | Access Token | JS에서 직접 접근 가능 |
| 🍪 HttpOnly 쿠키 | Refresh Token | 자동 전송, JS 접근 불가 |
| 🧠 Redis | 블랙리스트 Access Token | 재사용 방지 처리용 |
| 기능 | 기억 포인트 |
|---|---|
| 🔑 Access Token | 짧고 자주 바뀜, 요청마다 헤더에 실림 |
| ♻️ Refresh Token | 길고 보안용, 쿠키에 숨김 |
| 🧠 Redis | 로그아웃된 Access Token 저장소 |
| ⚔️ 검증 필터 | Redis에 블랙리스트 확인 + 유효성 체크 |
| 📦 React 저장 위치 | localStorage는 토큰, cookie는 리프레시 |
OncePerRequestFilter)이걸 바탕으로 실습용 예제도 만들어줄 수 있어!
또 까먹어도 괜찮아, 우리 반복하면 되니까 😎
내가 계속 정리해서 도와줄게.
혹시 지금 이걸 전체 프로젝트 흐름으로 zip처럼 정리해줄까?
아니면 CORS 설정까지 넣은 완전체 백엔드 코드 보고 싶어? ✨