Skip to content

Conversation

@RCNR
Copy link
Contributor

@RCNR RCNR commented Sep 7, 2025

📍 PR 타입 (하나 이상 선택)

  • 기능 추가
  • 버그 수정
  • 의존성, 환경 변수, 빌드 관련 코드 업데이트
  • 기타 사소한 수정

🏷️ 관련 이슈

Close #

📌 개요

  • 레디스 적용
  • 리플레시 토큰 작업 내용 변경
  • redis 적용으로 인한 jwtProvider 내용 일부 변경

🔁 변경 사항

  • 사용자의 리플레시 토큰을 기존 DB 저장에서 redis 적용으로 변경

📸 스크린샷

✅ 체크리스트

  • 코드가 정상적으로 동작하는지 확인
  • PR에 적절한 라벨을 선택
  • 관련 테스트 코드 작성
  • 문서(README, Swagger 등) 업데이트

💡 추가 사항 (리뷰어가 참고해야 할 것)

Summary by CodeRabbit

  • New Features

    • Token refresh API now returns the new access and refresh tokens in the response body.
    • Enhanced JSON handling for Java 8 date/time types in API responses.
  • Refactor

    • Modernized token processing for improved reliability.
  • Chores

    • Introduced Redis-backed storage for refresh tokens to improve scalability and stability.
    • Added dependencies to support Redis and Java 8 date/time serialization.

RCNR added 2 commits September 7, 2025 12:08
- 리플레시 토큰 저장을 db가 아닌 레디스 이용
- 레디스 적용
- 레디스 사용으로 인한 코드 수정
@RCNR RCNR self-assigned this Sep 7, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 7, 2025

Walkthrough

Adds Redis configuration and dependencies, updates JWT key handling, migrates refresh-token storage from DB to Redis, adjusts AuthService to use Redis for refresh token validation and issuance, and modifies AuthController to return the token DTO in reissue responses.

Changes

Cohort / File(s) Summary
Build config
build.gradle
Adds spring-boot-starter-data-redis and jackson-datatype-jsr310 dependencies.
Auth API response tweak
src/main/java/naughty/tuzamate/auth/controller/AuthController.java
reissueToken now returns onSuccess(code, tokenDto) including the token DTO in the response.
JWT parsing/key init update
src/main/java/naughty/tuzamate/auth/jwt/JwtProvider.java
Initializes signing key from Base64-decoded secret; updates parsing from .setSigningKey().parseClaimsJws() to .verifyWith().parseSignedClaims().
Auth service: Redis-backed refresh flow
src/main/java/naughty/tuzamate/auth/service/AuthService.java
Uses StringRedisTemplate to read/write refresh tokens under refreshToken:{userId} with TTL; replaces repository lookup/updates.
RefreshToken service: Redis migration
src/main/java/naughty/tuzamate/auth/service/RefreshTokenService.java
Introduces Redis-based saveRefreshToken, getRefreshToken, deleteRefreshToken; removes JPA-based persistence and related methods.
Redis configuration
src/main/java/naughty/tuzamate/global/config/RedisConfig.java
Adds RedisConfig with LettuceConnectionFactory and RedisTemplate<String,String> using StringRedisSerializer; reads host/port from properties.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant AC as AuthController
  participant AS as AuthService
  participant R as Redis
  participant J as JwtProvider

  C->>AC: POST /auth/reissue (refreshToken)
  AC->>AS: reissueToken(request)
  AS->>J: validate & parse refresh token
  J-->>AS: claims (userId, version, exp)
  AS->>R: GET refreshToken:{userId}
  alt token missing or mismatch
    AS-->>AC: throw TOKEN_INVALID
    AC-->>C: error response
  else valid
    AS->>J: generate new access/refresh tokens
    J-->>AS: TokenDto
    AS->>R: SET refreshToken:{userId} = newToken EX ms TTL
    AS-->>AC: TokenDto
    AC-->>C: onSuccess(code, TokenDto)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

feature

Suggested reviewers

  • weeast1521

Poem

I bounced through bytes and keys so red,
Stashed tokens snug where Redis led.
A whisk of Base64 in night,
New claims parsed clean, the timing right.
Now reissue sings with DTO cheer—
Hippity-hop, your auth is here! 🐇🔑

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@RCNR RCNR changed the title [Feat] redis 적용 [Refactor] redis 적용 Sep 7, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/java/naughty/tuzamate/auth/service/AuthService.java (2)

36-52: Logout: (1) isValid result ignored (expiration not enforced), (2) deletes from DB instead of Redis

  • Ignoring isValid means expired tokens may proceed until getUserId; parsing doesn’t enforce exp. Check and throw on invalid.
  • After migration, delete the Redis key, not the DB row.
     public void logout(String bearer) {
@@
-        jwtProvider.isValid(token);
+        if (!jwtProvider.isValid(token)) {
+            throw new CustomException(JwtErrorCode.TOKEN_INVALID);
+        }
@@
-        refreshTokenRepository.deleteById(userId);
+        redisTemplate.delete("refreshToken:" + userId);
     }

Also remove the unused repository dependency/import:

-import naughty.tuzamate.auth.repository.RefreshTokenRepository;
@@
-    private final RefreshTokenRepository refreshTokenRepository;

54-64: Reissue: enforce expiration check (isValid result ignored)

Ensure expired/invalid refresh tokens are rejected.

-        jwtProvider.isValid(token); // 토큰 유효성 검사
+        if (!jwtProvider.isValid(token)) { // 토큰 유효성 검사
+            throw new CustomException(JwtErrorCode.TOKEN_INVALID);
+        }
🧹 Nitpick comments (7)
build.gradle (1)

37-39: Redis dep: good; JSR-310 module likely redundant under Spring Boot 3.3

Boot already brings jackson-datatype-jsr310 via spring-boot-starter-json. Consider removing the explicit dependency to avoid duplication.

-    implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
src/main/java/naughty/tuzamate/auth/jwt/JwtProvider.java (1)

31-31: BASE64 secret requirement: fail-fast and clearer error

Decoding assumes JWT_SECRET is Base64. Add validation to throw a clear startup error if misconfigured.

-        this.secret = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
+        try {
+            this.secret = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
+        } catch (IllegalArgumentException ex) {
+            throw new IllegalStateException("JWT_SECRET must be a Base64-encoded HMAC key (e.g., 256-bit).", ex);
+        }
src/main/java/naughty/tuzamate/global/config/RedisConfig.java (2)

10-11: Remove unused import

ValueOperations isn’t used here.

-import org.springframework.data.redis.core.ValueOperations;

16-21: Consider password/SSL support

If targeting managed Redis (e.g., ElastiCache), add optional password and SSL config.

src/main/java/naughty/tuzamate/auth/service/AuthService.java (1)

65-71: Key prefix duplication

Extract "refreshToken:" into a constant to avoid typos and ease future changes.

+    private static final String RT_PREFIX = "refreshToken:";
@@
-        String storedRefreshToken = redisTemplate.opsForValue().get("refreshToken:" + userId);
+        String storedRefreshToken = redisTemplate.opsForValue().get(RT_PREFIX + userId);
@@
-                "refreshToken:" + userId,
+                RT_PREFIX + userId,
src/main/java/naughty/tuzamate/auth/service/RefreshTokenService.java (2)

3-4: Clean up unused/internal imports

Remove unused imports and the Netty internal class.

-import io.netty.util.internal.StringUtil;
-import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PostConstruct;
@@
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
@@
-import org.springframework.util.StringUtils;
+import org.springframework.util.StringUtils;
@@
-import javax.swing.text.html.Option;
-import java.time.Duration;
+import java.time.Duration;

Also applies to: 9-11, 14-16, 16-18


35-39: Unify key prefix across services

Define a shared constant (e.g., in a Keys class) to avoid drift with AuthService.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6436191 and 2a569e0.

📒 Files selected for processing (6)
  • build.gradle (2 hunks)
  • src/main/java/naughty/tuzamate/auth/controller/AuthController.java (1 hunks)
  • src/main/java/naughty/tuzamate/auth/jwt/JwtProvider.java (3 hunks)
  • src/main/java/naughty/tuzamate/auth/service/AuthService.java (3 hunks)
  • src/main/java/naughty/tuzamate/auth/service/RefreshTokenService.java (1 hunks)
  • src/main/java/naughty/tuzamate/global/config/RedisConfig.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/main/java/naughty/tuzamate/auth/controller/AuthController.java (2)
src/main/java/naughty/tuzamate/auth/dto/TokenResponse.java (3)
  • TokenResponse (5-19)
  • TokenDto (7-17)
  • TokenDto (13-16)
src/main/java/naughty/tuzamate/auth/controller/KakaoController.java (1)
  • RestController (22-71)
src/main/java/naughty/tuzamate/auth/service/AuthService.java (3)
src/main/java/naughty/tuzamate/auth/repository/RefreshTokenRepository.java (1)
  • RefreshTokenRepository (8-15)
src/main/java/naughty/tuzamate/auth/entity/RefreshToken.java (2)
  • RefreshToken (8-34)
  • update (30-32)
src/main/java/naughty/tuzamate/domain/user/service/UserServiceImpl.java (1)
  • UserServiceImpl (24-85)
src/main/java/naughty/tuzamate/auth/service/RefreshTokenService.java (4)
src/main/java/naughty/tuzamate/auth/jwt/JwtProvider.java (1)
  • Slf4j (20-98)
src/main/java/naughty/tuzamate/domain/user/service/UserServiceImpl.java (1)
  • Service (24-85)
src/main/java/naughty/tuzamate/auth/repository/RefreshTokenRepository.java (3)
  • RefreshTokenRepository (8-15)
  • findByToken (10-10)
  • deleteByUserId (12-12)
src/main/java/naughty/tuzamate/auth/entity/RefreshToken.java (2)
  • RefreshToken (8-34)
  • update (30-32)
🔇 Additional comments (7)
src/main/java/naughty/tuzamate/auth/jwt/JwtProvider.java (1)

75-84: Parser changes LGTM

verifyWith/parseSignedClaims usage matches JJWT 0.12.x.

src/main/java/naughty/tuzamate/global/config/RedisConfig.java (1)

22-46: Bean setup looks correct for String key/value ops

Lettuce + String serializers are appropriate for the current usage.

src/main/java/naughty/tuzamate/auth/controller/AuthController.java (1)

52-58: Cookie attributes: verify they match your client’s cross-site needs

If the app is cross-site (separate frontend domain), you’ll likely need SameSite=None; Secure and client fetch with credentials. Current Lax may block cookie on XHR.

src/main/java/naughty/tuzamate/auth/service/AuthService.java (1)

27-31: StringRedisTemplate is auto-configured
Spring Boot’s RedisAutoConfiguration will still register a default StringRedisTemplate bean (it only skips it if there’s already a bean named “stringRedisTemplate”), so your custom RedisTemplate bean doesn’t interfere.

src/main/java/naughty/tuzamate/auth/service/RefreshTokenService.java (3)

27-33: ValueOperations init: OK

PostConstruct initialization is fine.


41-46: Getter: concise and safe

Returning Optional based on hasText is good.


49-52: Delete by key: LGTM

Matches the chosen key strategy.


response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return CustomResponse.onSuccess(AuthSuccessCode.ACCESS_TOKEN_REISSUE_SUCCESS_CODE);
return CustomResponse.onSuccess(AuthSuccessCode.ACCESS_TOKEN_REISSUE_SUCCESS_CODE, tokenDto);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bug: maxAge expects seconds (you’re passing millis) → cookie lives 1000× longer

Pass a Duration based on milliseconds.

-        return CustomResponse.onSuccess(AuthSuccessCode.ACCESS_TOKEN_REISSUE_SUCCESS_CODE, tokenDto);
+        return CustomResponse.onSuccess(AuthSuccessCode.ACCESS_TOKEN_REISSUE_SUCCESS_CODE, tokenDto);

Apply this diff above where the cookie is built:

+import java.time.Duration;
@@
-        ResponseCookie cookie = ResponseCookie.from("refreshToken", tokenDto.getRefreshToken())
+        ResponseCookie cookie = ResponseCookie.from("refreshToken", tokenDto.getRefreshToken())
                 .httpOnly(true)
                 .secure(true) // 테스트 시 false
                 .path("/")
                 .sameSite("Lax")
-                .maxAge(jwtProvider.getRefreshExpiration())
+                .maxAge(Duration.ofMillis(jwtProvider.getRefreshExpiration()))
                 .build();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return CustomResponse.onSuccess(AuthSuccessCode.ACCESS_TOKEN_REISSUE_SUCCESS_CODE, tokenDto);
++ b/src/main/java/naughty/tuzamate/auth/controller/AuthController.java
@@
import java.time.Duration;
@@
ResponseCookie cookie = ResponseCookie.from("refreshToken", tokenDto.getRefreshToken())
.httpOnly(true)
.secure(true) // 테스트 시 false
.path("/")
.sameSite("Lax")
.maxAge(Duration.ofMillis(jwtProvider.getRefreshExpiration()))
.build();
@@
return CustomResponse.onSuccess(AuthSuccessCode.ACCESS_TOKEN_REISSUE_SUCCESS_CODE, tokenDto);
🤖 Prompt for AI Agents
In src/main/java/naughty/tuzamate/auth/controller/AuthController.java around
line 61, the cookie maxAge is being set using milliseconds (so it ends up 1000×
too long); change the cookie builder to pass a java.time.Duration created from
the millisecond value (e.g., use Duration.ofMillis(yourMillis) for the maxAge
parameter) so the cookie lifetime is interpreted correctly.

Comment on lines 35 to +39
public void saveRefreshToken(Long userId, String token, LocalDateTime expire) {
String key = "refreshToken:" + userId;
Duration ttl = Duration.between(LocalDateTime.now(), expire);
valueOperations.set(key, token, ttl);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

TTL calc may be wrong across time zones; add guard for negative/zero TTL

expire is LocalDateTime (no zone). If server default zone differs from where expire was computed (e.g., Asia/Seoul), TTL can skew. Also guard negative TTL.

-        Duration ttl = Duration.between(LocalDateTime.now(), expire);
-        valueOperations.set(key, token, ttl);
+        Duration ttl = Duration.between(LocalDateTime.now(), expire);
+        if (!ttl.isNegative() && !ttl.isZero()) {
+            valueOperations.set(key, token, ttl);
+        } else {
+            // Expired or invalid TTL: ensure token isn’t stored
+            redisTemplate.delete(key);
+        }

Follow-up (recommended): change the method to accept Instant expireAt and compute Duration.between(Instant.now(), expireAt) to avoid zone issues.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void saveRefreshToken(Long userId, String token, LocalDateTime expire) {
String key = "refreshToken:" + userId;
Duration ttl = Duration.between(LocalDateTime.now(), expire);
valueOperations.set(key, token, ttl);
}
public void saveRefreshToken(Long userId, String token, LocalDateTime expire) {
String key = "refreshToken:" + userId;
Duration ttl = Duration.between(LocalDateTime.now(), expire);
if (!ttl.isNegative() && !ttl.isZero()) {
valueOperations.set(key, token, ttl);
} else {
// Expired or invalid TTL: ensure token isn’t stored
redisTemplate.delete(key);
}
}
🤖 Prompt for AI Agents
In src/main/java/naughty/tuzamate/auth/service/RefreshTokenService.java around
lines 35 to 39, the TTL is computed from LocalDateTime which is zone-ambiguous
and may produce negative/incorrect durations; update the method to use a
zoned/instant-aware calculation and guard against non-positive TTLs. Replace
Duration.between(LocalDateTime.now(), expire) with a computation that converts
the provided expire to an Instant (or accept an Instant expireAt parameter) and
use Duration.between(Instant.now(), expireAt), then check if the resulting
duration is positive before calling valueOperations.set; if duration is zero or
negative, skip saving (or delete/expire immediately) and log/handle the case
appropriately. Consider the follow-up: change the method signature to accept
Instant expireAt to avoid zone issues entirely.

@RCNR RCNR closed this Sep 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant