Skip to content

Commit ab42553

Browse files
committed
Fix multi-device refresh token sessions
1 parent e75fcdb commit ab42553

22 files changed

Lines changed: 397 additions & 169 deletions

ontime-back/src/main/java/devkor/ontime_back/config/SecurityConfig.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import devkor.ontime_back.repository.UserAlarmSettingRepository;
1919
import devkor.ontime_back.repository.UserRepository;
2020
import devkor.ontime_back.service.AnalyticsPreferenceService;
21+
import devkor.ontime_back.service.AuthTokenService;
2122
import jakarta.validation.Validator;
2223
import lombok.RequiredArgsConstructor;
2324
import org.springframework.beans.factory.annotation.Value;
@@ -60,6 +61,7 @@ public class SecurityConfig {
6061
private final AppleLoginService appleLoginService;
6162
private final GoogleLoginService googleLoginService;
6263
private final AnalyticsPreferenceService analyticsPreferenceService;
64+
private final AuthTokenService authTokenService;
6365

6466
@Bean
6567
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
@@ -81,7 +83,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
8183
.requestMatchers("/health").permitAll() // 로드밸런서 연결 확인용 url
8284
.anyRequest().authenticated()
8385
)
84-
.addFilterBefore(new KakaoLoginFilter("/oauth2/kakao/login", objectMapper, validator, jwtTokenProvider, userRepository, userAlarmSettingRepository, analyticsPreferenceService),
86+
.addFilterBefore(new KakaoLoginFilter("/oauth2/kakao/login", objectMapper, validator, jwtTokenProvider, userRepository, userAlarmSettingRepository, analyticsPreferenceService, authTokenService),
8587
UsernamePasswordAuthenticationFilter.class)
8688
.addFilterBefore(new GoogleLoginFilter("/oauth2/google/login", objectMapper, validator, googleLoginService, userRepository),
8789
UsernamePasswordAuthenticationFilter.class)
@@ -114,7 +116,7 @@ public AuthenticationManager authenticationManager() {
114116

115117
@Bean
116118
public LoginSuccessHandler loginSuccessHandler() {
117-
return new LoginSuccessHandler(jwtTokenProvider, userRepository);
119+
return new LoginSuccessHandler(userRepository, authTokenService);
118120
}
119121

120122
@Bean
@@ -134,7 +136,7 @@ public CustomJsonUsernamePasswordAuthenticationFilter customJsonUsernamePassword
134136

135137
@Bean
136138
public JwtAuthenticationFilter jwtAuthenticationProcessingFilter() {
137-
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(jwtTokenProvider, userRepository);
139+
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(jwtTokenProvider, userRepository, authTokenService);
138140
return jwtAuthenticationFilter;
139141
}
140142

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package devkor.ontime_back.entity;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.FetchType;
6+
import jakarta.persistence.GeneratedValue;
7+
import jakarta.persistence.GenerationType;
8+
import jakarta.persistence.Id;
9+
import jakarta.persistence.Index;
10+
import jakarta.persistence.JoinColumn;
11+
import jakarta.persistence.ManyToOne;
12+
import jakarta.persistence.Table;
13+
import jakarta.persistence.UniqueConstraint;
14+
import lombok.AccessLevel;
15+
import lombok.AllArgsConstructor;
16+
import lombok.Builder;
17+
import lombok.Getter;
18+
import lombok.NoArgsConstructor;
19+
import org.hibernate.annotations.OnDelete;
20+
import org.hibernate.annotations.OnDeleteAction;
21+
22+
import java.time.Instant;
23+
24+
@Getter
25+
@Entity
26+
@Builder
27+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
28+
@AllArgsConstructor
29+
@Table(
30+
uniqueConstraints = {
31+
@UniqueConstraint(name = "uk_user_refresh_token_token", columnNames = "refresh_token")
32+
},
33+
indexes = {
34+
@Index(name = "idx_user_refresh_token_user", columnList = "user_id")
35+
}
36+
)
37+
public class UserRefreshToken {
38+
39+
@Id
40+
@GeneratedValue(strategy = GenerationType.IDENTITY)
41+
private Long userRefreshTokenId;
42+
43+
@ManyToOne(fetch = FetchType.LAZY)
44+
@JoinColumn(name = "user_id", nullable = false)
45+
@OnDelete(action = OnDeleteAction.CASCADE)
46+
private User user;
47+
48+
@Column(name = "refresh_token", nullable = false, length = 1024)
49+
private String refreshToken;
50+
51+
@Column(nullable = false)
52+
private Instant createdAt;
53+
54+
@Column(nullable = false)
55+
private Instant updatedAt;
56+
57+
public static UserRefreshToken create(User user, String refreshToken) {
58+
Instant now = Instant.now();
59+
return UserRefreshToken.builder()
60+
.user(user)
61+
.refreshToken(refreshToken)
62+
.createdAt(now)
63+
.updatedAt(now)
64+
.build();
65+
}
66+
67+
public void rotate(String refreshToken) {
68+
this.refreshToken = refreshToken;
69+
this.updatedAt = Instant.now();
70+
}
71+
}

ontime-back/src/main/java/devkor/ontime_back/global/generallogin/handler/LoginSuccessHandler.java

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package devkor.ontime_back.global.generallogin.handler;
22

3-
import devkor.ontime_back.global.jwt.JwtTokenProvider;
43
import devkor.ontime_back.repository.UserRepository;
4+
import devkor.ontime_back.service.AuthTokenService;
55
import lombok.RequiredArgsConstructor;
66
import lombok.extern.slf4j.Slf4j;
77
import org.springframework.security.core.Authentication;
@@ -17,25 +17,16 @@
1717
@RequiredArgsConstructor
1818
public class LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
1919

20-
private final JwtTokenProvider jwtTokenProvider;
2120
private final UserRepository userRepository;
21+
private final AuthTokenService authTokenService;
2222

2323
@Override
2424
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
2525
Authentication authentication) {
2626
String email = extractUsername(authentication); // 인증 정보에서 Username(email) 추출
2727
userRepository.findByEmail(email)
2828
.ifPresent(user -> {
29-
// 수정된 부분: User의 ID(PK)를 AccessToken 생성에 사용
30-
String accessToken = jwtTokenProvider.createAccessToken(email, user.getId());
31-
32-
String refreshToken = jwtTokenProvider.createRefreshToken(); // RefreshToken 발급
33-
34-
// 수정된 부분: 응답 헤더에 AccessToken, RefreshToken 실어서 응답
35-
jwtTokenProvider.sendAccessAndRefreshToken(response, accessToken, refreshToken);
36-
37-
user.updateAccessToken(accessToken);
38-
user.updateRefreshToken(refreshToken);
29+
authTokenService.issueLoginTokens(user, response);
3930
userRepository.saveAndFlush(user);
4031

4132
log.info("Login succeeded for userId: {}", user.getId());

ontime-back/src/main/java/devkor/ontime_back/global/jwt/JwtAuthenticationFilter.java

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import devkor.ontime_back.entity.User;
55
import devkor.ontime_back.repository.UserRepository;
66
import devkor.ontime_back.response.*;
7+
import devkor.ontime_back.service.AuthTokenService;
78
import jakarta.servlet.DispatcherType;
89
import jakarta.servlet.FilterChain;
910
import jakarta.servlet.http.HttpServletRequest;
@@ -36,6 +37,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
3637

3738
private final JwtTokenProvider jwtTokenProvider;
3839
private final UserRepository userRepository;
40+
private final AuthTokenService authTokenService;
3941

4042
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
4143

@@ -91,15 +93,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
9193
// DB에 없으면 InvalidRefreshTokenException 발생
9294
public void reIssueAccessToken(HttpServletResponse response, String refreshToken) throws IOException {
9395
log.info("Checking stored refresh credential");
94-
User user = userRepository.findByRefreshToken(refreshToken)
95-
.orElseThrow(() -> new InvalidRefreshTokenException("Invalid Refresh token!~!"));
96+
authTokenService.rotateRefreshToken(refreshToken, response);
9697
log.info("Stored refresh credential matched");
97-
98-
String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(), user.getId());
99-
100-
user.updateAccessToken(accessToken);
101-
jwtTokenProvider.sendAccessToken(response, accessToken);
102-
userRepository.saveAndFlush(user);
10398
}
10499

105100
// accessToken으로 유저의 권한정보만 저장하고 인증 허가(스프링 시큐리티 필터체인 中 인증체인 통과해 다음 체인으로 이동)

ontime-back/src/main/java/devkor/ontime_back/global/jwt/JwtTokenProvider.java

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -142,16 +142,6 @@ public void setRefreshTokenHeader(HttpServletResponse response, String refreshTo
142142
response.setHeader(refreshHeader, refreshToken);
143143
}
144144

145-
// refreshToken db에 업데이트
146-
public void updateRefreshToken(String email, String refreshToken) {
147-
userRepository.findByEmail(email)
148-
.ifPresentOrElse(
149-
user -> user.updateRefreshToken(refreshToken),
150-
() -> {
151-
throw new RuntimeException("일치하는 회원이 없습니다.");
152-
});
153-
}
154-
155145
// token 유효성 확인
156146
public boolean isTokenValid(String token) {
157147
try {
@@ -166,9 +156,14 @@ public boolean isTokenValid(String token) {
166156

167157
public boolean isAccessTokenValid(String token) {
168158
try {
169-
userRepository.findByAccessToken(token)
170-
.orElseThrow(() -> new InvalidAccessTokenException("유효하지 않은 엑세스 토큰입니다."));
171-
JWT.require(Algorithm.HMAC512(secretKey)).build().verify(token);
159+
Long userId = JWT.require(Algorithm.HMAC512(secretKey))
160+
.build()
161+
.verify(token)
162+
.getClaim(USER_ID_CLAIM)
163+
.asLong();
164+
if (userId == null || userRepository.findById(userId).isEmpty()) {
165+
throw new InvalidAccessTokenException("유효하지 않은 엑세스 토큰입니다.");
166+
}
172167
log.info("Access credential is valid");
173168
return true;
174169
} catch (Exception e) {

ontime-back/src/main/java/devkor/ontime_back/global/oauth/apple/AppleLoginService.java

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import devkor.ontime_back.repository.UserRepository;
1616
import devkor.ontime_back.response.InvalidTokenException;
1717
import devkor.ontime_back.service.AnalyticsPreferenceService;
18+
import devkor.ontime_back.service.AuthTokenService;
1819
import io.jsonwebtoken.Claims;
1920
import io.jsonwebtoken.Jwts;
2021
import io.jsonwebtoken.SignatureAlgorithm;
@@ -72,6 +73,7 @@ public class AppleLoginService {
7273
private final UserAlarmSettingRepository userAlarmSettingRepository;
7374
private final JwtTokenProvider jwtTokenProvider;
7475
private final AnalyticsPreferenceService analyticsPreferenceService;
76+
private final AuthTokenService authTokenService;
7577

7678
private final RestTemplate restTemplate = new RestTemplate();
7779
public Authentication handleLogin(String appleRefreshToken, User user, HttpServletResponse response) throws IOException {
@@ -80,12 +82,7 @@ public Authentication handleLogin(String appleRefreshToken, User user, HttpServl
8082
user.updateSocialLoginToken(appleRefreshToken);
8183
}
8284

83-
String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(), user.getId());
84-
String refreshToken = jwtTokenProvider.createRefreshToken();
85-
86-
jwtTokenProvider.sendAccessAndRefreshToken(response, accessToken, refreshToken);
87-
user.updateAccessToken(accessToken);
88-
user.updateRefreshToken(refreshToken);
85+
authTokenService.issueLoginTokens(user, response);
8986
userRepository.saveAndFlush(user);
9087

9188
Authentication authentication = new UsernamePasswordAuthenticationToken(
@@ -135,13 +132,7 @@ public Authentication handleRegister(String appleRefreshToken, OAuthAppleUserDto
135132

136133
User savedUser = userRepository.save(newUser);
137134

138-
String accessToken = jwtTokenProvider.createAccessToken(savedUser.getEmail(), savedUser.getId());
139-
String refreshToken = jwtTokenProvider.createRefreshToken();
140-
141-
jwtTokenProvider.sendAccessAndRefreshToken(response, accessToken, refreshToken);
142-
143-
savedUser.updateAccessToken(accessToken);
144-
savedUser.updateRefreshToken(refreshToken);
135+
authTokenService.issueLoginTokens(savedUser, response);
145136
userRepository.save(savedUser);
146137
userAlarmSettingRepository.save(UserAlarmSetting.defaultFor(savedUser));
147138
analyticsPreferenceService.createDefaultPreference(savedUser);

ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import devkor.ontime_back.repository.UserAlarmSettingRepository;
1515
import devkor.ontime_back.repository.UserRepository;
1616
import devkor.ontime_back.service.AnalyticsPreferenceService;
17+
import devkor.ontime_back.service.AuthTokenService;
1718
import io.jsonwebtoken.Claims;
1819
import jakarta.servlet.http.HttpServletResponse;
1920
import lombok.RequiredArgsConstructor;
@@ -47,6 +48,7 @@ public class GoogleLoginService {
4748
private final UserRepository userRepository;
4849
private final UserAlarmSettingRepository userAlarmSettingRepository;
4950
private final AnalyticsPreferenceService analyticsPreferenceService;
51+
private final AuthTokenService authTokenService;
5052
private static final String GOOGLE_USER_INFO_URL = "https://www.googleapis.com/userinfo/v2/me";
5153
private static final String GOOGLE_REVOKE_URL = "https://oauth2.googleapis.com/revoke?token=";
5254

@@ -59,18 +61,20 @@ public GoogleLoginService(
5961
UserRepository userRepository,
6062
UserAlarmSettingRepository userAlarmSettingRepository,
6163
AnalyticsPreferenceService analyticsPreferenceService,
64+
AuthTokenService authTokenService,
6265
@Value("${google.web.client-id}") String webClientId,
6366
@Value("${google.app.client-id}") String appClientId
6467
) {
6568
this(jwtTokenProvider, userRepository, userAlarmSettingRepository, analyticsPreferenceService,
66-
webClientId, appClientId, createRevokeRestTemplate());
69+
authTokenService, webClientId, appClientId, createRevokeRestTemplate());
6770
}
6871

6972
GoogleLoginService(
7073
JwtTokenProvider jwtTokenProvider,
7174
UserRepository userRepository,
7275
UserAlarmSettingRepository userAlarmSettingRepository,
7376
AnalyticsPreferenceService analyticsPreferenceService,
77+
AuthTokenService authTokenService,
7478
String webClientId,
7579
String appClientId,
7680
RestTemplate revokeRestTemplate
@@ -79,6 +83,7 @@ public GoogleLoginService(
7983
this.userRepository = userRepository;
8084
this.userAlarmSettingRepository = userAlarmSettingRepository;
8185
this.analyticsPreferenceService = analyticsPreferenceService;
86+
this.authTokenService = authTokenService;
8287
this.revokeRestTemplate = revokeRestTemplate;
8388
this.validClientIds = Stream.concat(
8489
Stream.of(webClientId),
@@ -103,12 +108,7 @@ private static RestTemplate createRevokeRestTemplate() {
103108
public Authentication handleLogin(OAuthGoogleRequestDto oAuthGoogleRequestDto, User user, HttpServletResponse response) throws IOException {
104109
user.updateSocialLoginToken(oAuthGoogleRequestDto.getRefreshToken());
105110

106-
String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(), user.getId());
107-
String refreshToken = jwtTokenProvider.createRefreshToken();
108-
109-
jwtTokenProvider.sendAccessAndRefreshToken(response, accessToken, refreshToken);
110-
user.updateAccessToken(accessToken);
111-
user.updateRefreshToken(refreshToken);
111+
authTokenService.issueLoginTokens(user, response);
112112
userRepository.saveAndFlush(user);
113113

114114
Authentication authentication = new UsernamePasswordAuthenticationToken(
@@ -157,13 +157,7 @@ public Authentication handleRegister(OAuthGoogleRequestDto oAuthGoogleRequestDto
157157

158158
User savedUser = userRepository.save(newUser);
159159

160-
String accessToken = jwtTokenProvider.createAccessToken(savedUser.getEmail(), savedUser.getId());
161-
String refreshToken = jwtTokenProvider.createRefreshToken();
162-
163-
jwtTokenProvider.sendAccessAndRefreshToken(response, accessToken, refreshToken);
164-
165-
savedUser.updateAccessToken(accessToken);
166-
savedUser.updateRefreshToken(refreshToken);
160+
authTokenService.issueLoginTokens(savedUser, response);
167161
userRepository.save(savedUser);
168162
userAlarmSettingRepository.save(UserAlarmSetting.defaultFor(savedUser));
169163
analyticsPreferenceService.createDefaultPreference(savedUser);

0 commit comments

Comments
 (0)