Skip to content

Commit 194ba3f

Browse files
committed
feat/OPS-328 : RefreshToken을 서버에서 저장하도록 변경. API 일부 수정.
1 parent cd43e9a commit 194ba3f

File tree

5 files changed

+170
-46
lines changed

5 files changed

+170
-46
lines changed

src/main/java/org/tuna/zoopzoop/backend/domain/auth/controller/ApiV1AuthController.java

Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@
88
import org.springframework.http.HttpStatus;
99
import org.springframework.http.ResponseCookie;
1010
import org.springframework.http.ResponseEntity;
11+
import org.springframework.security.core.AuthenticationException;
1112
import org.springframework.web.bind.annotation.*;
12-
import org.springframework.web.reactive.function.client.WebClient;
13-
import org.tuna.zoopzoop.backend.domain.auth.service.KakaoUserInfoService;
13+
import org.tuna.zoopzoop.backend.domain.auth.entity.RefreshToken;
14+
import org.tuna.zoopzoop.backend.domain.auth.service.RefreshTokenService;
1415
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
15-
import org.tuna.zoopzoop.backend.domain.member.service.MemberService;
16-
import org.tuna.zoopzoop.backend.global.config.jwt.JwtProperties;
1716
import org.tuna.zoopzoop.backend.global.rsData.RsData;
1817
import org.tuna.zoopzoop.backend.global.security.jwt.JwtUtil;
1918

@@ -23,69 +22,87 @@
2322
@Tag(name = "ApiV1AuthController", description = "인증/인가 REST API 컨트롤러")
2423
public class ApiV1AuthController {
2524
private final JwtUtil jwtUtil;
26-
private final MemberService memberService;
27-
private final JwtProperties jwtProperties;
28-
private final KakaoUserInfoService kakaoUserInfoService;
29-
private final WebClient webClient;
25+
private final RefreshTokenService refreshTokenService;
3026

3127
/**
3228
* 사용자 로그아웃 API
3329
* @param response Servlet 기반 웹에서 server -> client로 http 응답을 보내기 위한 객체, 자동 주입.
3430
*/
3531
@GetMapping("/logout")
3632
@Operation(summary = "사용자 로그아웃")
37-
public ResponseEntity<RsData<Void>> logout(HttpServletResponse response) {
33+
public ResponseEntity<RsData<Void>> logout(
34+
@CookieValue(name = "sessionId")
35+
String sessionId,
36+
HttpServletResponse response) {
37+
38+
// 서버에서 RefreshToken 삭제
39+
refreshTokenService.deleteBySessionId(sessionId);
40+
41+
// 클라이언트 쿠키 삭제 (AccessToken + SessionId)
3842
ResponseCookie accessCookie = ResponseCookie.from("accessToken", "")
3943
.httpOnly(true)
4044
.path("/")
41-
.maxAge(0) // 쿠키 삭제
45+
.maxAge(0)
4246
.sameSite("Lax")
4347
.build();
4448

45-
ResponseCookie refreshCookie = ResponseCookie.from("refreshToken", "")
49+
ResponseCookie sessionCookie = ResponseCookie.from("sessionId", "")
4650
.httpOnly(true)
4751
.path("/")
48-
.maxAge(0) // 쿠키 삭제
52+
.maxAge(0)
4953
.sameSite("Lax")
5054
.build();
5155

5256
response.addHeader(HttpHeaders.SET_COOKIE, accessCookie.toString());
53-
response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString());
57+
response.addHeader(HttpHeaders.SET_COOKIE, sessionCookie.toString());
5458

5559
return ResponseEntity
5660
.status(HttpStatus.OK)
57-
.body(new RsData<>(
58-
"200",
59-
"정상적으로 로그아웃 했습니다.",
60-
null
61-
)
62-
);
61+
.body(new RsData<>("200", "정상적으로 로그아웃 했습니다.", null));
6362
}
6463

6564
/**
6665
* refreshToken 기반으로 accessToken 재발급
67-
* @param refreshToken 쿠키에 포함된 현재 로그인한 사용자의 refreshToken
66+
* @param sessionId 쿠키에 포함된 현재 로그인한 사용자의 sessionId.
6867
* @param response Servlet 기반 웹에서 server -> client로 http 응답을 보내기 위한 객체, 자동 주입.
6968
*/
7069

7170
@PostMapping("/refresh")
72-
@Operation(summary = "사용자 액세스 토큰 재발급 (리프레시 토큰이 유효할 경우)")
73-
public ResponseEntity<RsData<Void>> refreshToken(@CookieValue(name = "refreshToken", required = false) String refreshToken,
74-
HttpServletResponse response) {
71+
@Operation(summary = "사용자 액세스 토큰 재발급 (서버 저장 RefreshToken 사용)")
72+
public ResponseEntity<RsData<Void>> refreshToken(
73+
@CookieValue(name = "sessionId")
74+
String sessionId,
75+
HttpServletResponse response
76+
) {
77+
78+
if (sessionId == null) {
79+
return ResponseEntity
80+
.status(HttpStatus.UNAUTHORIZED)
81+
.body(new RsData<>("401", "세션이 존재하지 않습니다.", null));
82+
}
83+
84+
// sessionId로 RefreshToken 조회
85+
RefreshToken refreshTokenEntity;
86+
try {
87+
refreshTokenEntity = refreshTokenService.getBySessionId(sessionId);
88+
} catch (AuthenticationException e) {
89+
return ResponseEntity
90+
.status(HttpStatus.UNAUTHORIZED)
91+
.body(new RsData<>("401", e.getMessage(), null));
92+
}
93+
94+
String refreshToken = refreshTokenEntity.getRefreshToken();
7595

76-
if (refreshToken == null || !jwtUtil.validateToken(refreshToken) || !jwtUtil.isRefreshToken(refreshToken)) {
96+
// RefreshToken 유효성 검사
97+
if (!jwtUtil.validateToken(refreshToken) || !jwtUtil.isRefreshToken(refreshToken)) {
7798
return ResponseEntity
7899
.status(HttpStatus.UNAUTHORIZED)
79-
.body(new RsData<>(
80-
"401",
81-
"유효하지 않은 리프레시 토큰입니다.",
82-
null
83-
));
100+
.body(new RsData<>("401", "유효하지 않은 리프레시 토큰입니다.", null));
84101
}
85102

86-
String providerKey = jwtUtil.getProviderKeyFromToken(refreshToken);
87-
Member member = memberService.findByProviderKey(providerKey);
103+
Member member = refreshTokenEntity.getMember();
88104

105+
// 새 AccessToken 발급
89106
String newAccessToken = jwtUtil.generateToken(member);
90107

91108
ResponseCookie accessCookie = ResponseCookie.from("accessToken", newAccessToken)
@@ -99,10 +116,6 @@ public ResponseEntity<RsData<Void>> refreshToken(@CookieValue(name = "refreshTok
99116

100117
return ResponseEntity
101118
.status(HttpStatus.OK)
102-
.body(new RsData<>(
103-
"200",
104-
"액세스 토큰을 재발급 했습니다.",
105-
null
106-
));
119+
.body(new RsData<>("200", "액세스 토큰을 재발급 했습니다.", null));
107120
}
108121
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.tuna.zoopzoop.backend.domain.auth.entity;
2+
3+
import jakarta.persistence.*;
4+
import lombok.*;
5+
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
6+
import org.tuna.zoopzoop.backend.global.jpa.entity.BaseEntity;
7+
8+
import java.time.LocalDateTime;
9+
10+
@Getter
11+
@Setter
12+
@Entity
13+
@NoArgsConstructor
14+
@AllArgsConstructor
15+
@Builder
16+
public class RefreshToken extends BaseEntity {
17+
@OneToOne(fetch = FetchType.LAZY)
18+
@JoinColumn(name = "member_id", unique = true, nullable = false)
19+
private Member member;
20+
21+
@Column(name = "session_id", unique = true, nullable = false)
22+
private String sessionId;
23+
24+
@Column(unique = true, nullable = false)
25+
private String refreshToken;
26+
27+
@Column(name = "created_at", nullable = false, updatable = false)
28+
private LocalDateTime createdAt;
29+
30+
@Column(name = "expired_at")
31+
private LocalDateTime expiredAt;
32+
33+
@PrePersist
34+
public void prePersist() {
35+
if(createdAt == null) {
36+
this.createdAt = LocalDateTime.now();
37+
}
38+
}
39+
}

src/main/java/org/tuna/zoopzoop/backend/domain/auth/handler/OAuth2SuccessHandler.java

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.springframework.security.oauth2.core.user.OAuth2User;
1313
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
1414
import org.springframework.stereotype.Component;
15+
import org.tuna.zoopzoop.backend.domain.auth.service.RefreshTokenService;
1516
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
1617
import org.tuna.zoopzoop.backend.domain.member.repository.MemberRepository;
1718
import org.tuna.zoopzoop.backend.domain.member.service.MemberService;
@@ -29,6 +30,7 @@ public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler
2930
private final JwtProperties jwtProperties;
3031
private final MemberRepository memberRepository;
3132
private final MemberService memberService;
33+
private final RefreshTokenService refreshTokenService;
3234

3335
@Value("${front.redirect_domain}")
3436
private String redirect_domain;
@@ -41,9 +43,8 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
4143
Authentication authentication) throws IOException {
4244

4345
// OAuth2 로그인 사용자의 속성
44-
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
45-
4646
// 소셜 로그인 공급자(Google, Kakao)
47+
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
4748
String registrationId = ((OAuth2AuthenticationToken) authentication).getAuthorizedClientRegistrationId();
4849

4950
// 공급자 별로 DB 에서 회원 조회
@@ -58,9 +59,14 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
5859
throw new IllegalArgumentException(registrationId + "는 지원하지 않는 소셜 로그인입니다.");
5960
}
6061

61-
// 조회된 회원 정보를 기반으로 AccessToken 및 RefreshToken 생성
62+
// 조회된 회원 정보를 기반으로 AccessToken 생성
6263
String accessToken = jwtUtil.generateToken(member);
64+
65+
// RefreshToken 생성 및 DB 저장, SessionId 생성
6366
String refreshToken = jwtUtil.generateRefreshToken(member);
67+
String sessionId = refreshTokenService.saveSession(member, refreshToken);
68+
69+
log.info("[OAuth2SuccessHandler] Member: {}, SessionId: {}", member.getId(), sessionId);
6470

6571
String source = request.getParameter("source");
6672
String state = request.getParameter("state");
@@ -73,7 +79,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
7379
String redirectUrl = redirect_domain + "/extension/callback"
7480
+ "?success=true"
7581
+ "&accessToken=" + URLEncoder.encode(accessToken, "UTF-8")
76-
+ "&refreshToken=" + URLEncoder.encode(refreshToken, "UTF-8");
82+
+ "&sessionId=" + URLEncoder.encode(sessionId, "UTF-8");
7783
response.sendRedirect(redirectUrl);
7884
return;
7985
}
@@ -83,7 +89,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
8389
String redirectUrl = redirect_domain + "/api/auth/callback"
8490
+ "?success=true"
8591
+ "&accessToken=" + URLEncoder.encode(accessToken, "UTF-8")
86-
+ "&refreshToken=" + URLEncoder.encode(refreshToken, "UTF-8");
92+
+ "&sessionId=" + URLEncoder.encode(sessionId, "UTF-8");
8793
response.sendRedirect(redirectUrl);
8894

8995
} else {
@@ -98,20 +104,18 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
98104
.sameSite("None")
99105
.build();
100106

101-
ResponseCookie refreshCookie = ResponseCookie.from("refreshToken", refreshToken)
107+
ResponseCookie sessionCookie = ResponseCookie.from("sessionId", sessionId)
102108
.httpOnly(true)
103109
.path("/")
104-
.maxAge(jwtProperties.getRefreshTokenValidity() / 1000)
105-
// .domain() // 프론트엔드 & 백엔드 상위 도메인
106-
// .secure(true) // https 필수 설정.
110+
.maxAge(jwtProperties.getRefreshTokenValidity() / 1000) // RefreshToken 유효기간과 동일하게
107111
.domain(redirect_domain)
108112
.secure(true)
109113
.sameSite("None")
110114
.build();
111115

112116
// HTTP 응답에서 쿠키 값 추가.
113117
response.addHeader(HttpHeaders.SET_COOKIE, accessCookie.toString());
114-
response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString());
118+
response.addHeader(HttpHeaders.SET_COOKIE, sessionCookie.toString());
115119

116120
// 로그인 성공 후 리다이렉트.
117121
// 배포 시에 프론트엔드와 조율이 필요한 부분일 듯 함.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.tuna.zoopzoop.backend.domain.auth.repository;
2+
3+
import org.springframework.data.jpa.repository.JpaRepository;
4+
import org.springframework.stereotype.Repository;
5+
import org.tuna.zoopzoop.backend.domain.auth.entity.RefreshToken;
6+
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
7+
8+
import java.util.Optional;
9+
10+
@Repository
11+
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Integer> {
12+
Optional<RefreshToken> findBySessionId(String sessionId);
13+
Optional<RefreshToken> findByMember(Member member);
14+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.tuna.zoopzoop.backend.domain.auth.service;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.security.authentication.BadCredentialsException;
5+
import org.springframework.stereotype.Service;
6+
import org.tuna.zoopzoop.backend.domain.auth.entity.RefreshToken;
7+
import org.tuna.zoopzoop.backend.domain.auth.repository.RefreshTokenRepository;
8+
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
9+
import org.tuna.zoopzoop.backend.global.security.jwt.JwtUtil;
10+
11+
import java.time.LocalDateTime;
12+
import java.time.ZoneId;
13+
import java.util.Date;
14+
import java.util.UUID;
15+
16+
@Service
17+
@RequiredArgsConstructor
18+
public class RefreshTokenService {
19+
private final RefreshTokenRepository refreshTokenRepository;
20+
private final JwtUtil jwtUtil;
21+
22+
private LocalDateTime getExpirationLocalDateTimeFromToken(String token) {
23+
Date expirationDate = jwtUtil.getExpirationDateFromToken(token); // 기존 메서드
24+
if (expirationDate == null) return null;
25+
26+
return LocalDateTime.ofInstant(expirationDate.toInstant(), ZoneId.systemDefault());
27+
}
28+
29+
public String saveSession(Member member, String refreshToken) {
30+
String sessionId = UUID.randomUUID().toString();
31+
32+
refreshTokenRepository.findByMember(member).ifPresent(refreshTokenRepository::delete);
33+
34+
RefreshToken token = RefreshToken.builder()
35+
.member(member)
36+
.refreshToken(refreshToken)
37+
.sessionId(sessionId)
38+
.expiredAt(getExpirationLocalDateTimeFromToken(refreshToken))
39+
.build();
40+
41+
refreshTokenRepository.save(token);
42+
return sessionId;
43+
}
44+
45+
public RefreshToken getBySessionId(String sessionId) {
46+
return refreshTokenRepository.findBySessionId(sessionId)
47+
.orElseThrow(() -> new BadCredentialsException("세션을 찾을 수 없습니다."));
48+
}
49+
50+
public void deleteBySessionId(String sessionId) {
51+
refreshTokenRepository.findBySessionId(sessionId)
52+
.orElseThrow(() -> new BadCredentialsException("잘못된 요청입니다."));
53+
}
54+
}

0 commit comments

Comments
 (0)