-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathOAuthController.java
More file actions
160 lines (136 loc) · 7.13 KB
/
OAuthController.java
File metadata and controls
160 lines (136 loc) · 7.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package umc.codeplay.controller;
import java.util.List;
import java.util.Map;
import org.springframework.http.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.view.RedirectView;
import lombok.RequiredArgsConstructor;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import umc.codeplay.apiPayLoad.ApiResponse;
import umc.codeplay.apiPayLoad.code.status.ErrorStatus;
import umc.codeplay.apiPayLoad.exception.handler.GeneralHandler;
import umc.codeplay.config.properties.BaseOAuthProperties;
import umc.codeplay.config.properties.GoogleOAuthProperties;
import umc.codeplay.config.properties.KakaoOAuthProperties;
import umc.codeplay.domain.Member;
import umc.codeplay.domain.enums.SocialStatus;
import umc.codeplay.dto.MemberResponseDTO;
import umc.codeplay.jwt.JwtUtil;
import umc.codeplay.service.MemberService;
@RestController
@RequestMapping("/oauth")
@RequiredArgsConstructor
@Validated
@Tag(name = "oauth-controller", description = "외부 소셜 로그인 서비스 연동 API, JWT 토큰 헤더 포함을 필요로 하지 않습니다.")
public class OAuthController {
private final JwtUtil jwtUtil;
private final RestTemplate restTemplate = new RestTemplate();
private final GoogleOAuthProperties googleOAuthProperties;
private final KakaoOAuthProperties kakaoOAuthProperties;
private final MemberService memberService;
@GetMapping("/authorize/{provider}")
@Operation(
summary = "소셜 로그인 서비스로 로그인합니다.",
description =
"{provider}엔 google, kakao 가 들어갈 수 있습니다. 해당 소셜 로그인 서비스로 리다이렉트합니다. 로그인이 완료되면 스프링 서버에서 사용 가능한 JWT 토큰/리프레시 토큰을 반환합니다.")
public RedirectView redirectToOAuth(@PathVariable("provider") String provider) {
// CSRF 방어용 state, PKCE(code_challenge)..는 굳이
BaseOAuthProperties properties =
switch (provider) {
case "google" -> googleOAuthProperties;
case "kakao" -> kakaoOAuthProperties;
default -> throw new GeneralHandler(ErrorStatus.INVALID_OAUTH_PROVIDER);
};
String url = properties.getUrl();
RedirectView redirectView = new RedirectView();
redirectView.setUrl(url);
return redirectView;
}
@Hidden
@GetMapping("/callback/{provider}")
public ApiResponse<MemberResponseDTO.LoginResultDTO> OAuthCallback(
@RequestParam("code") String code, @PathVariable("provider") String provider) {
BaseOAuthProperties properties =
switch (provider) {
case "google" -> googleOAuthProperties;
case "kakao" -> kakaoOAuthProperties;
default -> throw new GeneralHandler(ErrorStatus.INVALID_OAUTH_PROVIDER);
};
// (1) 받은 code 로 구글 토큰 엔드포인트에 Access/ID Token 교환
Map<String, Object> tokenResponse = requestOAuthToken(code, properties);
// (2) 받아온 Access Token(or ID Token)을 통해 사용자 정보 가져오기
// String idToken = (String) tokenResponse.get("id_token"); // OIDC
String accessToken = (String) tokenResponse.get("access_token");
Map<String, Object> userInfo = requestOAuthUserInfo(accessToken, properties);
String email = null;
// String name = null;
switch (provider) {
case "google" -> {
// (3-a) 구글 UserInfo Endpoint 로 이메일, 프로필 등 조회
email = (String) userInfo.get("email");
// name = (String) userInfo.get("name");
}
case "kakao" -> {
// (3-b) 카카오 UserInfo Endpoint 로 이메일, 프로필 등 조회
Map<String, Object> kakaoAccount =
(Map<String, Object>) userInfo.get("kakao_account");
Map<String, Object> kakaoProperties =
(Map<String, Object>) userInfo.get("properties");
email = (String) kakaoAccount.get("email");
// name = (String) kakaoProperties.get("nickname");
}
}
// (4) 우리 DB에서 회원 조회 or 생성
Member member =
memberService.findOrCreateOAuthMember(
email, SocialStatus.valueOf(provider.toUpperCase()));
// (5) JWTUtil 이용해서 Access/Refresh 토큰 발급
var authorities = List.of(new SimpleGrantedAuthority("ROLE_" + member.getRole().name()));
String serviceAccessToken = jwtUtil.generateToken(email, authorities);
String serviceRefreshToken = jwtUtil.generateRefreshToken(email, authorities);
// (6) 최종적으로 JWT(액세스/리프레시)를 프론트에 응답
return ApiResponse.onSuccess(
MemberResponseDTO.LoginResultDTO.builder()
.email(email)
.token(serviceAccessToken)
.refreshToken(serviceRefreshToken)
.build());
}
private Map<String, Object> requestOAuthToken(String code, BaseOAuthProperties properties) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", properties.getClientId());
params.add("client_secret", properties.getClientSecret());
params.add("redirect_uri", properties.getRedirectUri());
params.add("code", code);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
ResponseEntity<Map> response =
restTemplate.postForEntity(properties.getTokenUri(), request, Map.class);
if (response.getStatusCode() == HttpStatus.OK) {
return response.getBody();
}
throw new GeneralHandler(ErrorStatus.OAUTH_TOKEN_REQUEST_FAILED);
}
private Map<String, Object> requestOAuthUserInfo(
String accessToken, BaseOAuthProperties properties) {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + accessToken);
HttpEntity<Void> request = new HttpEntity<>(headers);
ResponseEntity<Map> response =
restTemplate.exchange(
properties.getUserInfoUri(), HttpMethod.GET, request, Map.class);
if (response.getStatusCode() == HttpStatus.OK) {
return response.getBody();
}
throw new GeneralHandler(ErrorStatus.OAUTH_USERINFO_REQUEST_FAILED);
}
}