Skip to content

Commit 9dfd126

Browse files
committed
Merge branch 'develop' into feat/PRODUCT-104
# Conflicts: # src/main/java/timeeat/controller/member/MemberController.java # src/test/java/timeeat/controller/member/MemberControllerTest.java # src/test/java/timeeat/document/member/MemberDocumentTest.java
2 parents 576e884 + 7fe57a4 commit 9dfd126

File tree

13 files changed

+355
-6
lines changed

13 files changed

+355
-6
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package timeeat.client.oauth;
2+
3+
import java.net.URI;
4+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
5+
import org.springframework.http.HttpStatusCode;
6+
import org.springframework.http.MediaType;
7+
import org.springframework.stereotype.Component;
8+
import org.springframework.util.LinkedMultiValueMap;
9+
import org.springframework.util.MultiValueMap;
10+
import org.springframework.web.client.RestClient;
11+
import org.springframework.web.util.UriComponentsBuilder;
12+
13+
@Component
14+
@EnableConfigurationProperties(OauthProperties.class)
15+
public class OauthClient {
16+
17+
private final RestClient restClient;
18+
private final OauthProperties properties;
19+
20+
public OauthClient(RestClient.Builder restClientBuilder, OauthProperties oauthProperties) {
21+
this.restClient = restClientBuilder
22+
.defaultStatusHandler(HttpStatusCode::is5xxServerError, new OauthServerErrorHandler())
23+
.build();
24+
this.properties = oauthProperties;
25+
}
26+
27+
public URI getOauthLoginUrl() {
28+
return UriComponentsBuilder.fromUriString("https://kauth.kakao.com/oauth/authorize")
29+
.queryParam("client_id", properties.getClientId())
30+
.queryParam("redirect_uri", properties.getRedirectUri())
31+
.queryParam("response_type", "code")
32+
.build()
33+
.toUri();
34+
}
35+
36+
public OauthToken requestOauthToken(String code) {
37+
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
38+
body.add("grant_type", "authorization_code");
39+
body.add("client_id", properties.getClientId());
40+
body.add("redirect_uri", properties.getRedirectUri());
41+
body.add("code", code);
42+
43+
return restClient.post()
44+
.uri("https://kauth.kakao.com/oauth/token")
45+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
46+
.body(body)
47+
.retrieve()
48+
.body(OauthToken.class);
49+
}
50+
51+
public OauthMemberInformation requestMemberInformation(OauthToken token) {
52+
return restClient.get()
53+
.uri("https://kapi.kakao.com/v2/user/me")
54+
.headers(headers -> headers.setBearerAuth(token.accessToken()))
55+
.retrieve()
56+
.body(OauthMemberInformation.class);
57+
}
58+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package timeeat.client.oauth;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
5+
6+
@JsonDeserialize(using = OauthMemberInformationDeserializer.class)
7+
@JsonIgnoreProperties(ignoreUnknown = true)
8+
public record OauthMemberInformation(long socialId, String nickname) {
9+
}
10+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package timeeat.client.oauth;
2+
3+
import com.fasterxml.jackson.core.JsonParser;
4+
import com.fasterxml.jackson.databind.DeserializationContext;
5+
import com.fasterxml.jackson.databind.JsonDeserializer;
6+
import com.fasterxml.jackson.databind.JsonNode;
7+
import java.io.IOException;
8+
9+
public class OauthMemberInformationDeserializer extends JsonDeserializer<OauthMemberInformation> {
10+
11+
@Override
12+
public OauthMemberInformation deserialize(JsonParser jsonParser,
13+
DeserializationContext deserializationContext) throws IOException {
14+
JsonNode root = jsonParser.getCodec().readTree(jsonParser);
15+
16+
long id = root.path("id").asLong();
17+
String nickname = root
18+
.path("kakao_account")
19+
.path("profile")
20+
.path("nickname")
21+
.asText(null);
22+
return new OauthMemberInformation(id, nickname);
23+
}
24+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package timeeat.client.oauth;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
import org.springframework.boot.context.properties.ConfigurationProperties;
6+
7+
@Getter
8+
@AllArgsConstructor
9+
@ConfigurationProperties(prefix = "oauth")
10+
public class OauthProperties {
11+
12+
private final String clientId;
13+
private final String redirectUri;
14+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package timeeat.client.oauth;
2+
3+
import java.io.IOException;
4+
import org.springframework.http.HttpRequest;
5+
import org.springframework.http.client.ClientHttpResponse;
6+
import org.springframework.stereotype.Component;
7+
import org.springframework.web.client.RestClient.ResponseSpec.ErrorHandler;
8+
9+
@Component
10+
public class OauthServerErrorHandler implements ErrorHandler {
11+
12+
@Override
13+
public void handle(HttpRequest request, ClientHttpResponse response) throws IOException {
14+
// TODO : 500 에러 처리
15+
throw new RuntimeException("Oauth server error occurred");
16+
}
17+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package timeeat.client.oauth;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
6+
@JsonIgnoreProperties(ignoreUnknown = true)
7+
public record OauthToken(@JsonProperty("access_token") String accessToken) {
8+
}
Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,43 @@
11
package timeeat.controller.member;
22

3+
import java.net.URI;
34
import lombok.RequiredArgsConstructor;
5+
import org.springframework.http.HttpStatus;
46
import org.springframework.http.ResponseEntity;
7+
import org.springframework.web.bind.annotation.GetMapping;
58
import org.springframework.web.bind.annotation.PostMapping;
69
import org.springframework.web.bind.annotation.RequestBody;
710
import org.springframework.web.bind.annotation.RestController;
811
import timeeat.controller.web.jwt.JwtManager;
12+
import timeeat.service.member.MemberService;
913

1014
@RestController
1115
@RequiredArgsConstructor
1216
public class MemberController {
1317

1418
private final JwtManager jwtManager;
19+
private final MemberService memberService;
20+
21+
@GetMapping("/api/member/login/auth")
22+
public ResponseEntity<Void> redirectOauthLoginPage() {
23+
URI oauthLoginUrl = memberService.getOauthLoginUrl();
24+
25+
return ResponseEntity
26+
.status(HttpStatus.FOUND)
27+
.location(oauthLoginUrl)
28+
.build();
29+
}
1530

1631
@PostMapping("/api/member/login")
17-
public ResponseEntity<TokenResponse> login() {
18-
// TODO : memberService.login() 메서드를 호출하여 로그인 처리
32+
public ResponseEntity<TokenResponse> login(@RequestBody MemberLoginRequest request) {
33+
memberService.login(request);
1934

2035
TokenResponse response = new TokenResponse(
2136
jwtManager.issueAccessToken(1L),
2237
jwtManager.issueRefreshToken(1L));
23-
return ResponseEntity.ok(response);
38+
return ResponseEntity
39+
.status(HttpStatus.CREATED)
40+
.body(response);
2441
}
2542

2643
@PostMapping("/api/member/reissue")
@@ -30,6 +47,8 @@ public ResponseEntity<TokenResponse> reissueToken(@RequestBody ReissueRequest re
3047
TokenResponse response = new TokenResponse(
3148
jwtManager.issueAccessToken(id),
3249
jwtManager.issueRefreshToken(id));
33-
return ResponseEntity.ok(response);
50+
return ResponseEntity
51+
.status(HttpStatus.CREATED)
52+
.body(response);
3453
}
3554
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package timeeat.controller.member;
2+
3+
public record MemberLoginRequest(String code) {
4+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package timeeat.service.member;
2+
3+
import java.net.URI;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.stereotype.Service;
7+
import timeeat.client.oauth.OauthClient;
8+
import timeeat.client.oauth.OauthMemberInformation;
9+
import timeeat.client.oauth.OauthToken;
10+
import timeeat.controller.member.MemberLoginRequest;
11+
12+
@Slf4j
13+
@Service
14+
@RequiredArgsConstructor
15+
public class MemberService {
16+
17+
private final OauthClient oauthClient;
18+
19+
public URI getOauthLoginUrl() {
20+
return oauthClient.getOauthLoginUrl();
21+
}
22+
23+
public void login(MemberLoginRequest request) {
24+
OauthToken oauthToken = oauthClient.requestOauthToken(request.code());
25+
OauthMemberInformation oauthMemberInformation = oauthClient.requestMemberInformation(oauthToken);
26+
log.info("Oauth 로그인 성공: {}", oauthMemberInformation); // TODO 회원 정보 저장 로직 추가
27+
}
28+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package timeeat.client.oauth;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.junit.jupiter.api.Assertions.assertAll;
5+
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
6+
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
7+
8+
import java.net.URI;
9+
import org.junit.jupiter.api.Nested;
10+
import org.junit.jupiter.api.Test;
11+
import org.springframework.beans.factory.annotation.Autowired;
12+
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
13+
import org.springframework.http.HttpMethod;
14+
import org.springframework.http.MediaType;
15+
import org.springframework.test.web.client.MockRestServiceServer;
16+
import org.springframework.test.web.client.response.MockRestResponseCreators;
17+
18+
@RestClientTest(OauthClient.class)
19+
class OauthClientTest {
20+
21+
@Autowired
22+
private MockRestServiceServer mockServer;
23+
24+
@Autowired
25+
private OauthClient oauthClient;
26+
27+
@Autowired
28+
private OauthProperties properties;
29+
30+
@Nested
31+
class GetOauthLoginUrl {
32+
33+
@Test
34+
void Oauth_로그인_URL을_생성할_수_있다() {
35+
URI uri = oauthClient.getOauthLoginUrl();
36+
37+
assertAll(
38+
() -> assertThat(uri.getHost()).isEqualTo("kauth.kakao.com"),
39+
() -> assertThat(uri.getPath()).isEqualTo("/oauth/authorize"),
40+
() -> assertThat(uri.getQuery()).contains("client_id=%s".formatted(properties.getClientId())),
41+
() -> assertThat(uri.getQuery()).contains("redirect_uri=%s".formatted(properties.getRedirectUri())),
42+
() -> assertThat(uri.getQuery()).contains("response_type=code")
43+
);
44+
}
45+
}
46+
47+
@Nested
48+
class RequestOauthToken {
49+
50+
@Test
51+
void Oauth_토큰을_요청할_수_있다() {
52+
setMockServer(HttpMethod.POST, "https://kauth.kakao.com/oauth/token", """
53+
{
54+
"token_type":"bearer",
55+
"access_token":"test-access-token",
56+
"expires_in":43199,
57+
"refresh_token":"test-refresh-token",
58+
"refresh_token_expires_in":5184000,
59+
"scope":"account_email profile"
60+
}""");
61+
String code = "test_code";
62+
63+
OauthToken token = oauthClient.requestOauthToken(code);
64+
65+
assertThat(token.accessToken()).isEqualTo("test-access-token");
66+
}
67+
}
68+
69+
@Nested
70+
class RequestMemberInformation {
71+
72+
@Test
73+
void Oauth_회원정보를_요청할_수_있다() {
74+
setMockServer(HttpMethod.GET, "https://kapi.kakao.com/v2/user/me", """
75+
{
76+
"id":123456789,
77+
"connected_at": "2022-04-11T01:45:28Z",
78+
"kakao_account": {
79+
"profile_nickname_needs_agreement": false,
80+
"profile_image_needs_agreement": false,
81+
"profile": {
82+
"nickname": "홍길동",
83+
"is_default_nickname": false
84+
}
85+
}
86+
}""");
87+
OauthToken token = new OauthToken("test-access-token");
88+
89+
OauthMemberInformation memberInfo = oauthClient.requestMemberInformation(token);
90+
91+
assertAll(
92+
() -> assertThat(memberInfo.socialId()).isEqualTo(123456789L),
93+
() -> assertThat(memberInfo.nickname()).isEqualTo("홍길동")
94+
);
95+
}
96+
}
97+
98+
public void setMockServer(HttpMethod method, String uri, String responseBody) {
99+
mockServer.expect(requestTo(uri))
100+
.andExpect(method(method))
101+
.andRespond(MockRestResponseCreators.withSuccess(responseBody, MediaType.APPLICATION_JSON));
102+
}
103+
}

0 commit comments

Comments
 (0)