Spring Security + JWT + Redis를 활용한 로그인 인증 시스템 구축

박우진·2025년 4월 7일

JAVA Spring

목록 보기
4/4

목표

  • JWT 기반 인증 시스템 구현
  • Access Token + Refresh Token 구조
  • Redis에 Refresh Token 저장으로 재발급 기능 보완
  • HttpOnly 쿠키 기반으로 토큰 저장 (XSS 방지)

1️⃣ JWT 인증이란?

JWT(Json Web Token)는 클라이언트가 로그인한 후 서버로부터 토큰을 받아, 이후 요청마다 이 토큰을 보내면서 인증을 수행하는 방식이다. 서버는 세션을 유지하지 않아도 되기 때문에, 무상태(stateless) 아키텍처에 적합하다.

Access Token

  • 사용자 인증 후 발급되는 짧은 유효기간의 토큰
  • 클라이언트가 요청 시마다 포함하여 인증에 사용
  • 만료되면 Refresh Token으로 재발급 요청

Refresh Token

  • Access Token 만료 시, 새 토큰을 발급받기 위한 토큰
  • Redis에 저장하여 유효성 검증에 사용

2️⃣ 아키텍처 구성도

[사용자] ⇄ [Spring 서버] ⇄ [Redis]
             ↓
       [JWT 발급 & 검증]
  • 사용자가 로그인하면 Access/Refresh Token이 발급됨
  • Refresh Token은 Redis에 저장
  • Access Token은 짧은 주기로 만료되며, 만료 시 Refresh Token을 통해 재발급
  • 로그아웃 시 Redis의 Refresh Token도 제거됨

3️⃣ 구현 코드 정리

JwtService.java

public String generateAccessToken(String sub) {
    return jwtTokenProvider.createAccessToken(sub, List.of("ROLE_USER"));
}

public String generateRefreshToken(String sub) {
    return jwtTokenProvider.createRefreshToken(sub);
}

쿠키에 토큰 저장 (HttpOnly, Secure)

public void setTokenCookies(HttpServletResponse response, String sub, String accessToken, String refreshToken) {
    redisTemplate.opsForValue().set("refresh:" + sub, refreshToken, 7, TimeUnit.DAYS);

    ResponseCookie accessTokenCookie = ResponseCookie.from("accessToken", accessToken)
            .httpOnly(true).secure(true).path("/").maxAge(3600).build();

    ResponseCookie refreshTokenCookie = ResponseCookie.from("refreshToken", refreshToken)
            .httpOnly(true).secure(true).path("/").maxAge(7 * 24 * 3600).build();

    response.addHeader("Set-Cookie", accessTokenCookie.toString());
    response.addHeader("Set-Cookie", refreshTokenCookie.toString());
}

토큰 제거 (로그아웃 시 Redis에서도 제거)

public void removeTokenCookie(HttpServletResponse response) {
    String refreshToken = jwtTokenProvider.resolveRefreshToken(request);
    if (refreshToken != null && validateToken(refreshToken)) {
        String userId = jwtTokenProvider.getUsernameFromToken(refreshToken);
        redisTemplate.delete("refresh:" + userId);
    }

    // 쿠키 무효화
}

4️⃣ 재발급 로직 (Reissue)

public void reissueToken(HttpServletRequest request, HttpServletResponse response) {
    String refreshToken = jwtTokenProvider.resolveRefreshToken(request);
    String userId = jwtTokenProvider.getUsernameFromToken(refreshToken);
    String redisRefreshToken = redisTemplate.opsForValue().get("refresh:" + userId);

    if (!refreshToken.equals(redisRefreshToken) || !validateToken(refreshToken)) {
        throw new RuntimeException("Invalid refresh token");
    }

    String newAccessToken = generateAccessToken(userId);

    ResponseCookie accessTokenCookie = ResponseCookie.from("accessToken", newAccessToken)
            .httpOnly(true).secure(true).path("/").maxAge(3600).build();

    response.addHeader("Set-Cookie", accessTokenCookie.toString());
}

5️⃣ 왜 Redis에 Refresh Token을 저장할까?

Access Token은 stateless하게 처리되지만, Refresh Token은 탈취에 대비한 관리가 필요함.

  • Redis에 저장해두면 재발급 시 서버 검증 가능
  • 탈취되었을 경우 Redis에서 삭제하여 무력화 가능
  • 로그아웃 시 토큰도 서버에서 제거 가능

즉, Refresh Token을 서버 측에서 상태 기반으로 관리할 수 있게 해주는 게 Redis의 역할이다.


6️⃣ application.yml 예시

jwt:
  secret: yourSecretKey
  access-token-expiration: 3600000
  refresh-token-expiration: 604800000

spring:
  redis:
    host: localhost
    port: 6379

마무리

  • HttpOnly Cookie 사용으로 클라이언트 보안 강화
  • Redis를 활용해 Refresh Token 관리
  • 사용자 경험을 해치지 않으면서 보안성을 유지한 방식

0개의 댓글