Skip to content

Commit 7e27e6d

Browse files
authored
[feat/OPS-328] 인증/인가 로직 변경 (#80)
* feat/OPS-328 : RefreshToken을 서버에서 저장하도록 변경. API 일부 수정. * feat/OPS-328 : 확장 프로그램 로그인 메소드 추가 및 데이터 캐싱 메소드 추가.
1 parent 10588d9 commit 7e27e6d

File tree

11 files changed

+304
-63
lines changed

11 files changed

+304
-63
lines changed

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

Lines changed: 75 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
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.dto.AuthResultData;
14+
import org.tuna.zoopzoop.backend.domain.auth.entity.AuthResult;
15+
import org.tuna.zoopzoop.backend.domain.auth.entity.RefreshToken;
16+
import org.tuna.zoopzoop.backend.domain.auth.service.RefreshTokenService;
1417
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;
1718
import org.tuna.zoopzoop.backend.global.rsData.RsData;
1819
import org.tuna.zoopzoop.backend.global.security.jwt.JwtUtil;
1920

@@ -23,69 +24,88 @@
2324
@Tag(name = "ApiV1AuthController", description = "인증/인가 REST API 컨트롤러")
2425
public class ApiV1AuthController {
2526
private final JwtUtil jwtUtil;
26-
private final MemberService memberService;
27-
private final JwtProperties jwtProperties;
28-
private final KakaoUserInfoService kakaoUserInfoService;
29-
private final WebClient webClient;
27+
private final RefreshTokenService refreshTokenService;
28+
private final AuthResult authResult;
3029

3130
/**
3231
* 사용자 로그아웃 API
3332
* @param response Servlet 기반 웹에서 server -> client로 http 응답을 보내기 위한 객체, 자동 주입.
3433
*/
3534
@GetMapping("/logout")
3635
@Operation(summary = "사용자 로그아웃")
37-
public ResponseEntity<RsData<Void>> logout(HttpServletResponse response) {
36+
public ResponseEntity<RsData<Void>> logout(
37+
@CookieValue(name = "sessionId")
38+
String sessionId,
39+
HttpServletResponse response) {
40+
41+
// 서버에서 RefreshToken 삭제
42+
refreshTokenService.deleteBySessionId(sessionId);
43+
44+
// 클라이언트 쿠키 삭제 (AccessToken + SessionId)
3845
ResponseCookie accessCookie = ResponseCookie.from("accessToken", "")
3946
.httpOnly(true)
4047
.path("/")
41-
.maxAge(0) // 쿠키 삭제
48+
.maxAge(0)
4249
.sameSite("Lax")
4350
.build();
4451

45-
ResponseCookie refreshCookie = ResponseCookie.from("refreshToken", "")
52+
ResponseCookie sessionCookie = ResponseCookie.from("sessionId", "")
4653
.httpOnly(true)
4754
.path("/")
48-
.maxAge(0) // 쿠키 삭제
55+
.maxAge(0)
4956
.sameSite("Lax")
5057
.build();
5158

5259
response.addHeader(HttpHeaders.SET_COOKIE, accessCookie.toString());
53-
response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString());
60+
response.addHeader(HttpHeaders.SET_COOKIE, sessionCookie.toString());
5461

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

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

7173
@PostMapping("/refresh")
72-
@Operation(summary = "사용자 액세스 토큰 재발급 (리프레시 토큰이 유효할 경우)")
73-
public ResponseEntity<RsData<Void>> refreshToken(@CookieValue(name = "refreshToken", required = false) String refreshToken,
74-
HttpServletResponse response) {
74+
@Operation(summary = "사용자 액세스 토큰 재발급 (서버 저장 RefreshToken 사용)")
75+
public ResponseEntity<RsData<Void>> refreshToken(
76+
@CookieValue(name = "sessionId")
77+
String sessionId,
78+
HttpServletResponse response
79+
) {
7580

76-
if (refreshToken == null || !jwtUtil.validateToken(refreshToken) || !jwtUtil.isRefreshToken(refreshToken)) {
81+
if (sessionId == null) {
7782
return ResponseEntity
7883
.status(HttpStatus.UNAUTHORIZED)
79-
.body(new RsData<>(
80-
"401",
81-
"유효하지 않은 리프레시 토큰입니다.",
82-
null
83-
));
84+
.body(new RsData<>("401", "세션이 존재하지 않습니다.", null));
85+
}
86+
87+
// sessionId로 RefreshToken 조회
88+
RefreshToken refreshTokenEntity;
89+
try {
90+
refreshTokenEntity = refreshTokenService.getBySessionId(sessionId);
91+
} catch (AuthenticationException e) {
92+
return ResponseEntity
93+
.status(HttpStatus.UNAUTHORIZED)
94+
.body(new RsData<>("401", e.getMessage(), null));
95+
}
96+
97+
String refreshToken = refreshTokenEntity.getRefreshToken();
98+
99+
// RefreshToken 유효성 검사
100+
if (!jwtUtil.validateToken(refreshToken) || !jwtUtil.isRefreshToken(refreshToken)) {
101+
return ResponseEntity
102+
.status(HttpStatus.UNAUTHORIZED)
103+
.body(new RsData<>("401", "유효하지 않은 리프레시 토큰입니다.", null));
84104
}
85105

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

108+
// 새 AccessToken 발급
89109
String newAccessToken = jwtUtil.generateToken(member);
90110

91111
ResponseCookie accessCookie = ResponseCookie.from("accessToken", newAccessToken)
@@ -97,12 +117,33 @@ public ResponseEntity<RsData<Void>> refreshToken(@CookieValue(name = "refreshTok
97117

98118
response.addHeader(HttpHeaders.SET_COOKIE, accessCookie.toString());
99119

120+
return ResponseEntity
121+
.status(HttpStatus.OK)
122+
.body(new RsData<>("200", "액세스 토큰을 재발급 했습니다.", null));
123+
}
124+
125+
@GetMapping("/result")
126+
@Operation(summary = "확장프로그램 백그라운드 풀링 대응 API")
127+
public ResponseEntity<RsData<AuthResultData>> pullingResult(
128+
@RequestParam String state
129+
) {
130+
AuthResultData resultData = authResult.get(state);
131+
if(resultData == null) {
132+
return ResponseEntity
133+
.status(HttpStatus.NOT_FOUND)
134+
.body(new RsData<>(
135+
"404",
136+
"state에 해당하는 토큰이 준비되지 않았거나, 잘못된 state 입니다.",
137+
null
138+
)
139+
);
140+
}
100141
return ResponseEntity
101142
.status(HttpStatus.OK)
102143
.body(new RsData<>(
103-
"200",
104-
"액세스 토큰을 재발급 했습니다.",
105-
null
144+
"200",
145+
"토큰이 정상적으로 발급되었습니다.",
146+
resultData
106147
));
107148
}
108149
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.tuna.zoopzoop.backend.domain.auth.dto;
2+
3+
public class AuthResultData {
4+
private final String accessToken;
5+
private final String sessionId;
6+
7+
public AuthResultData(String accessToken, String sessionId) {
8+
this.accessToken = accessToken;
9+
this.sessionId = sessionId;
10+
}
11+
12+
public String getAccessToken() {
13+
return accessToken;
14+
}
15+
16+
public String getSessionId() {
17+
return sessionId;
18+
}
19+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.tuna.zoopzoop.backend.domain.auth.entity;
2+
3+
import org.springframework.stereotype.Component;
4+
import org.tuna.zoopzoop.backend.domain.auth.dto.AuthResultData;
5+
import java.util.Map;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
8+
@Component
9+
public class AuthResult {
10+
private final Map<String, AuthResultData> results = new ConcurrentHashMap<>();
11+
12+
public void put(String state, String accessToken, String sessionId) {
13+
results.put(state, new AuthResultData(accessToken, sessionId));
14+
}
15+
16+
public AuthResultData get(String state) {
17+
return results.remove(state);
18+
}
19+
20+
public void consume(String state) {
21+
results.remove(state);
22+
}
23+
}
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/global/CustomOAuth2AuthorizationRequestResolver.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package org.tuna.zoopzoop.backend.domain.auth.global;
22

3+
import com.fasterxml.jackson.databind.ObjectMapper;
34
import jakarta.servlet.http.HttpServletRequest;
45
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
56
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
67
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
78
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
89

10+
import java.util.Base64;
11+
import java.util.HashMap;
12+
import java.util.Map;
13+
914
public class CustomOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
1015

1116
private final OAuth2AuthorizationRequestResolver defaultResolver;
@@ -28,13 +33,25 @@ private OAuth2AuthorizationRequest customize(OAuth2AuthorizationRequest req, Htt
2833
if (req == null) return null;
2934

3035
String source = request.getParameter("source"); // 로그인 시작 시 전달된 source
31-
3236
OAuth2AuthorizationRequest.Builder builder = OAuth2AuthorizationRequest.from(req);
3337

3438
if ("extension".equals(source)) {
35-
// state에 source 정보를 안전하게 포함
36-
builder.state("source:extension;" + req.getState());
39+
String state = request.getParameter("state");
40+
Map<String, String> stateData = new HashMap<>();
41+
stateData.put("source", "extension");
42+
stateData.put("customState", state);
43+
stateData.put("originalState", req.getState());
44+
45+
try {
46+
String encodedState = Base64.getUrlEncoder()
47+
.encodeToString(new ObjectMapper().writeValueAsBytes(stateData));
48+
builder.state(encodedState);
49+
} catch (Exception e) {
50+
e.printStackTrace();
51+
return builder.build();
52+
}
3753
}
54+
3855
return builder.build();
3956
}
4057
}

0 commit comments

Comments
 (0)