프로젝트에 JWT를 도입하는 이유 부터 작성해보려 한다.
기존 세션 기반 인증 방식은 다음과 같은 단점이 있다.
- 서버에서 사용자 상태(세션)를 직접 관리해야 한다.
- 서버가 여러 대일 경우, 세션을 공유할 중앙 저장소(Redis 등)가 필요하다.
- 이로 인해 수평 확장(Scale-out)에 어려움이 발생한다.
이를 해결하기 위해 JWT기반 인증 시스템을 도입했다.
JWT는 무상태(stateless) 인증 방식으로, 사용자 인증 정보를 토큰에 담아 클라이언트 측에서 관리한다.
구현 흐름
1. 로그인 시 Access Token + Refresh Token을 발급받는다.
2. Access Token은 HTTP 헤더, Refresh Token은 HttpOnly 쿠키에 저장한다.
3. Access Token 만료 시, Refresh Token으로 새 토큰을 재발급 받는다.
4. Redis에 Refresh Token 저장 → 토큰 탈취 방지 및 세션 만료 관리
Access Token & Refresh Token 생성
// JwtService.java
public String generateAccessToken(User user) {
return Jwts.builder()
.setSubject(user.getEmail())
.claim("userId", user.getId()) // 커스텀 정보 추가 가능
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION_MS))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public String generateRefreshToken() {
return Jwts.builder()
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRATION_MS))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
주의할 점
- ACCESS_TOKEN_EXPIRATION_MS: 일반적으로 5~15분 정도로 설정한다.
- REFRESH_TOKEN_EXPIRATION_MS: 1주 ~ 30일 정도로 설정한다.
- SECRET_KEY: 반드시 강력하고, 외부 노출되지 않도록 환경 변수로 관리해야 한다.
로그인 시 토큰 발급 & 쿠키 설정
// LoginController.java
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request, HttpServletResponse response) {
User user = userService.authenticate(request);
String accessToken = jwtService.generateAccessToken(user);
String refreshToken = jwtService.generateRefreshToken();
redisService.saveRefreshToken(user.getId(), refreshToken); // Redis 저장
// Refresh Token은 HttpOnly 쿠키로 저장
ResponseCookie cookie = ResponseCookie.from("refreshToken", refreshToken)
.httpOnly(true) // 자바스크립트 접근 불가
.secure(true) // HTTPS 환경에서만 사용
.sameSite("Strict") // CSRF 방지
.path("/")
.maxAge(REFRESH_TOKEN_EXPIRATION_MS / 1000)
.build();
response.setHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return ResponseEntity.ok(new TokenResponse(accessToken));
}
HttpOnly Cookie란?
- HttpOnly: 자바스크립트에서 쿠키에 접근할 수 없다. → XSS 공격을 차단한다.
- Secure: HTTPS에서만 쿠키를 전송한다. → 네트워크 가로채기 방지한다.
- SameSite: 다른 사이트에서 오는 요청을 제한한다. → CSRF 방지한다.
Refresh Token은 장기 저장하므로 쿠키 보안 설정은 필수 이다.
Access Token 재발급 처리
// TokenController.java
@PostMapping("/token/refresh")
public ResponseEntity<?> refreshToken(@CookieValue("refreshToken") String refreshToken) {
if (!jwtService.isValidToken(refreshToken)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid Refresh Token");
}
Long userId = jwtService.extractUserId(refreshToken);
String redisToken = redisService.getRefreshToken(userId);
if (!refreshToken.equals(redisToken)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Token Mismatch");
}
User user = userService.findById(userId);
String newAccessToken = jwtService.generateAccessToken(user);
return ResponseEntity.ok(new TokenResponse(newAccessToken));
}
중요한점
- Refresh Token 검증 로직을 꼭 넣어야 한다.
- Redis에 저장한 값과 비교해서 탈취 여부 확인해야 한다.
- 불일치 시 로그인 만료 처리 → 보안 강화한다.
프론트엔드 - Axios Interceptor로 자동 갱신
api.interceptors.response.use(
(res) => res,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const { data } = await axios.post("/api/token/refresh", {}, { withCredentials: true });
localStorage.setItem("accessToken", data.accessToken);
originalRequest.headers["Authorization"] = `Bearer ${data.accessToken}`;
return api(originalRequest);
} catch {
localStorage.removeItem("accessToken");
window.location.href = "/login";
}
}
return Promise.reject(error);
}
);
핵심포인트
- 401 에러 발생 시 자동으로 /token/refresh 요청한다.
- 성공 시 Access Token 갱신하고 기존 요청 재시도 한다.
- withCredentials: true → 쿠키 전송 필수
- 실패 시 로그인 페이지로 이동한다.
마무리
- JWT를 활용하면 서버가 인증 상태를 관리하지 않아도 되므로 수평 확장에 유리하다.
- Refresh Token을 Redis + HttpOnly 쿠키로 안전하게 관리하면 보안성도 확보할 수 있다.
- 프론트엔드에서 Axios Interceptor를 활용하면 사용자 경험을 해치지 않고 자동 재인증 처리가 가능하다.
- 다만, Access Token은 만료를 빠르게 설정하고, Refresh Token 탈취나 중복 사용 여부는 꼭 검증해야 한다.
'프로젝트 > 기업 일정 관리 웹' 카테고리의 다른 글
REST API 에서 Enum 직렬화 문제 (0) | 2025.04.02 |
---|---|
Redis 적용하기 (0) | 2025.04.02 |
도메인 별 SSL 인증서 문제 해결 (0) | 2025.04.02 |
CORS 정책 오류 해결 (0) | 2025.04.02 |
Access Token & Refresh Token 설계와 구현 (0) | 2025.04.02 |