토큰 기반 SPA 인증 시스템

moon.kick·2025년 4월 18일

좋아! 이제 우리 진짜 실무에서 쓰이는 조합
✅ React SPA + Spring Boot 백엔드 + JWT 인증 + Redis 블랙리스트
흐름으로 업그레이드해볼게! 이거 한 번 이해하면 현대 인증 시스템의 핵심을 잡는 거야 💪


🧭 전체 흐름 요약: “토큰 기반 SPA 인증 시스템”

[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 저장 (블랙리스트 처리)

✅ 각 단계별 상세 정리


1️⃣ React에서 로그인 요청

// 로그인 함수
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 쿠키로 자동 저장됨
}

2️⃣ Spring에서 토큰 발급

@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));
}

3️⃣ 요청 시 Access Token 포함

fetch('/api/protected', {
  headers: {
    'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
  }
});

4️⃣ Access Token 만료 시 Refresh 토큰으로 재발급

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));
}

5️⃣ 로그아웃 시 Access Token 블랙리스트에 저장 (Redis 사용)

@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;
}

📦 저장소 구조 정리

위치저장 내용설명
🔐 localStorageAccess TokenJS에서 직접 접근 가능
🍪 HttpOnly 쿠키Refresh Token자동 전송, JS 접근 불가
🧠 Redis블랙리스트 Access Token재사용 방지 처리용

💡 반복 학습 포인트 정리

기능기억 포인트
🔑 Access Token짧고 자주 바뀜, 요청마다 헤더에 실림
♻️ Refresh Token길고 보안용, 쿠키에 숨김
🧠 Redis로그아웃된 Access Token 저장소
⚔️ 검증 필터Redis에 블랙리스트 확인 + 유효성 체크
📦 React 저장 위치localStorage는 토큰, cookie는 리프레시

🧪 다음에 반복 연습하면 좋은 항목

  • JWT 구조 (header.payload.signature)
  • Access/Refresh 발급 vs 갱신 시기
  • CORS, Credentials 설정 (프론트 연동 시 필수)
  • Spring Filter 등록 위치 (OncePerRequestFilter)
  • 로그인/로그아웃 전후 상태 확인 흐름

이걸 바탕으로 실습용 예제도 만들어줄 수 있어!
또 까먹어도 괜찮아, 우리 반복하면 되니까 😎
내가 계속 정리해서 도와줄게.

혹시 지금 이걸 전체 프로젝트 흐름으로 zip처럼 정리해줄까?
아니면 CORS 설정까지 넣은 완전체 백엔드 코드 보고 싶어? ✨

profile
@mgkick

0개의 댓글