11package com .back .api .auth .service ;
22
3+ import java .time .Duration ;
34import java .time .LocalDateTime ;
45
56import org .apache .commons .lang3 .StringUtils ;
67import org .springframework .stereotype .Service ;
78import org .springframework .transaction .annotation .Transactional ;
89
910import com .back .api .auth .dto .JwtDto ;
11+ import com .back .api .auth .dto .cache .RefreshTokenCache ;
1012import com .back .domain .auth .entity .ActiveSession ;
1113import com .back .domain .auth .entity .RefreshToken ;
12- import com .back .domain .auth .repository .ActiveSessionRepository ;
14+ import com .back .domain .auth .repository .RefreshTokenRedisRepository ;
1315import com .back .domain .auth .repository .RefreshTokenRepository ;
1416import com .back .domain .user .entity .User ;
1517import com .back .domain .user .repository .UserRepository ;
1820import com .back .global .http .HttpRequestContext ;
1921import com .back .global .security .JwtClaims ;
2022import com .back .global .security .JwtProvider ;
23+ import com .back .global .utils .TokenHash ;
2124
2225import lombok .RequiredArgsConstructor ;
2326
@@ -27,53 +30,20 @@ public class AuthTokenService {
2730
2831 private final JwtProvider jwtProvider ;
2932 private final RefreshTokenRepository tokenRepository ;
30- private final ActiveSessionRepository activeSessionRepository ;
3133 private final UserRepository userRepository ;
3234 private final HttpRequestContext requestContext ;
35+ private final RefreshTokenRedisRepository refreshTokenRedisRepository ;
36+ private final SessionGuard sessionGuard ;
3337
3438 @ Transactional
3539 public JwtDto issueTokens (User user , String sessionId , long tokenVersion ) {
36- String accessToken = jwtProvider .generateAccessToken (user , sessionId , tokenVersion );
37- String refreshTokenStr = jwtProvider .generateRefreshToken (user , sessionId , tokenVersion );
40+ JwtDto dto = createJwtDto (user , sessionId , tokenVersion );
3841
39- // === seconds 기준 ===
40- long accessValiditySeconds = jwtProvider .getAccessTokenValiditySeconds ();
41- long refreshValiditySeconds = jwtProvider .getRefreshTokenValiditySeconds ();
42+ saveRefreshToRedis (user .getId (), dto .refreshToken (), sessionId , tokenVersion );
4243
43- // === 현재 시각 ===
44- long nowEpochMillis = System .currentTimeMillis ();
45- LocalDateTime issuedAt = LocalDateTime .now ();
46-
47- // === DB 저장용 만료 시각 (LocalDateTime) ===
48- LocalDateTime expiresAt = issuedAt .plusSeconds (refreshValiditySeconds );
44+ saveRefreshMetaToDB (user , dto .refreshToken (), sessionId , tokenVersion );
4945
50- RefreshToken refreshToken = RefreshToken .builder ()
51- .user (user )
52- .token (refreshTokenStr )
53- .issuedAt (issuedAt )
54- .expiresAt (expiresAt )
55- .revoked (false )
56- .userAgent (requestContext .getUserAgent ())
57- .ipAddress (requestContext .getClientIp ())
58- .sessionId (sessionId )
59- .tokenVersion (tokenVersion )
60- .build ();
61-
62- tokenRepository .save (refreshToken );
63-
64- // === API 응답용 epoch millis ===
65- long accessExpiresAtMillis = nowEpochMillis + (accessValiditySeconds * 1000 );
66- long refreshExpiresAtMillis = nowEpochMillis + (refreshValiditySeconds * 1000 );
67-
68- return new JwtDto (
69- JwtDto .BEARER ,
70- accessToken ,
71- accessExpiresAtMillis ,
72- accessValiditySeconds * 1000 ,
73- refreshTokenStr ,
74- refreshExpiresAtMillis ,
75- refreshValiditySeconds * 1000
76- );
46+ return dto ;
7747 }
7848
7949 @ Transactional
@@ -96,21 +66,98 @@ public JwtDto rotateTokenByRefreshToken(String refreshTokenStr) {
9666 String sid = claims .sessionId ();
9767 long tokenVersion = claims .tokenVersion ();
9868
99- ActiveSession active = activeSessionRepository . findByUserIdForUpdate ( userId )
100- . orElseThrow (() -> new ErrorException ( AuthErrorCode . UNAUTHORIZED ) );
69+ // 1. 세션을 읽고 현재 세션과 비교
70+ ActiveSession active = sessionGuard . requireActiveSessionForUpdate ( userId );
10171
102- if (!active .getSessionId ().equals (sid ) || active .getTokenVersion () != tokenVersion ) {
72+ sessionGuard .assertMatches (active , sid , tokenVersion );
73+
74+ // 2. Redis 검증: 현재 유효 refresh 인지 확인 (해시 비교)
75+ RefreshTokenCache cached = refreshTokenRedisRepository .find (userId )
76+ .orElseThrow (() -> new ErrorException (AuthErrorCode .ACCESS_OTHER_DEVICE ));
77+
78+ String hash = TokenHash .sha256 (refreshTokenStr );
79+ if (!hash .equals (cached .getRefreshTokenHash ())
80+ || !sid .equals (cached .getSessionId ())
81+ || tokenVersion != cached .getTokenVersion ()) {
10382 throw new ErrorException (AuthErrorCode .ACCESS_OTHER_DEVICE );
10483 }
10584
106- int revoked = tokenRepository .revokeIfActive (refreshTokenStr );
107- if (revoked != 1 ) {
108- throw new ErrorException (AuthErrorCode .REFRESH_TOKEN_NOT_FOUND );
85+ int updated = tokenRepository .revokeIfActive (hash );
86+ if (updated != 1 ) {
87+ throw new ErrorException (AuthErrorCode .ACCESS_OTHER_DEVICE );
10988 }
11089
11190 User user = userRepository .findById (userId )
11291 .orElseThrow (() -> new ErrorException (AuthErrorCode .UNAUTHORIZED ));
11392
11493 return issueTokens (user , active .getSessionId (), active .getTokenVersion ());
11594 }
95+
96+ private JwtDto createJwtDto (User user , String sessionId , long tokenVersion ) {
97+ String accessToken = jwtProvider .generateAccessToken (user , sessionId , tokenVersion );
98+ String refreshToken = jwtProvider .generateRefreshToken (user , sessionId , tokenVersion );
99+
100+ // === seconds 기준 ===
101+ long accessValiditySeconds = jwtProvider .getAccessTokenValiditySeconds ();
102+ long refreshValiditySeconds = jwtProvider .getRefreshTokenValiditySeconds ();
103+
104+ // === 현재 시각 ===
105+ long nowEpochMillis = System .currentTimeMillis ();
106+
107+ // === API 응답용 epoch millis ===
108+ long accessExpiresAtMillis = nowEpochMillis + (accessValiditySeconds * 1000 );
109+ long refreshExpiresAtMillis = nowEpochMillis + (refreshValiditySeconds * 1000 );
110+
111+ return new JwtDto (
112+ JwtDto .BEARER ,
113+ accessToken ,
114+ accessExpiresAtMillis ,
115+ accessValiditySeconds * 1000 ,
116+ refreshToken ,
117+ refreshExpiresAtMillis ,
118+ refreshValiditySeconds * 1000
119+ );
120+ }
121+
122+ private void saveRefreshToRedis (long userId , String refreshToken , String sid , long tokenVersion ) {
123+ JwtClaims claims = jwtProvider .payloadOrNull (refreshToken );
124+ if (claims == null ) {
125+ throw new ErrorException (AuthErrorCode .INVALID_TOKEN );
126+ }
127+
128+ long refreshValiditySeconds = jwtProvider .getRefreshTokenValiditySeconds ();
129+
130+ RefreshTokenCache cache = RefreshTokenCache .builder ()
131+ .refreshTokenHash (TokenHash .sha256 (refreshToken ))
132+ .sessionId (sid )
133+ .tokenVersion (tokenVersion )
134+ .jti (claims .jti ())
135+ .issuedAtEpochMs (System .currentTimeMillis ())
136+ .build ();
137+
138+ refreshTokenRedisRepository .save (userId , cache , Duration .ofSeconds (refreshValiditySeconds ));
139+ }
140+
141+ private void saveRefreshMetaToDB (User user , String refreshToken , String sid , long tokenVersion ) {
142+ JwtClaims claims = jwtProvider .payloadOrNull (refreshToken );
143+ String jti = (claims == null ) ? null : claims .jti ();
144+
145+ LocalDateTime issuedAt = LocalDateTime .now ();
146+ LocalDateTime expiresAt = issuedAt .plusSeconds (jwtProvider .getRefreshTokenValiditySeconds ());
147+
148+ RefreshToken meta = RefreshToken .builder ()
149+ .user (user )
150+ .token (TokenHash .sha256 (refreshToken ))
151+ .jti (jti )
152+ .issuedAt (issuedAt )
153+ .expiresAt (expiresAt )
154+ .revoked (false )
155+ .userAgent (requestContext .getUserAgent ())
156+ .ipAddress (requestContext .getClientIp ())
157+ .sessionId (sid )
158+ .tokenVersion (tokenVersion )
159+ .build ();
160+
161+ tokenRepository .save (meta );
162+ }
116163}
0 commit comments