Skip to content

Commit c5e41f7

Browse files
authored
Merge pull request #17 from T1F5/feature/#10
feat: 카카오 로그인 구현
2 parents c4fda50 + f8dc08f commit c5e41f7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1272
-3
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@ out/
3535

3636
### VS Code ###
3737
.vscode/
38+
39+
### yml ###
40+
/src/main/resources/application*.yml

build.gradle

+15
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,27 @@ dependencies {
2929
runtimeOnly 'com.h2database:h2'
3030
runtimeOnly 'com.mysql:mysql-connector-j'
3131

32+
// security
33+
implementation 'org.springframework.boot:spring-boot-starter-security'
34+
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
35+
3236
// Validation
3337
implementation 'org.springframework.boot:spring-boot-starter-validation'
3438

39+
// Redis
40+
// implementation 'org.springframework.boot:spring-boot-starter-data-redis'
41+
42+
// Actuator
43+
implementation 'org.springframework.boot:spring-boot-starter-actuator'
44+
3545
// Querydsl
3646
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
3747

48+
// JWT
49+
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
50+
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
51+
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
52+
3853
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
3954

4055
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"

src/main/java/com/unit/daybook/domain/.gitkeep

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.unit.daybook.domain.auth.common;
2+
3+
import com.unit.daybook.domain.auth.dto.request.OauthProvider;
4+
5+
public interface OAuthApiClient {
6+
OauthProvider oAuthProvider();
7+
String requestAccessToken(OAuthLoginParams params);
8+
OAuthInfoResponse requestOAuthInfo(String accessToken);
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.unit.daybook.domain.auth.common;
2+
3+
import com.unit.daybook.domain.auth.dto.request.OauthProvider;
4+
5+
public interface OAuthInfoResponse {
6+
String getSnsId();
7+
String getEmail();
8+
String getNickname();
9+
OauthProvider getOAuthProvider();
10+
String getProfileImageUrl();
11+
String getGender();
12+
String getBirthday();
13+
String getAgeRange();
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.unit.daybook.domain.auth.common;
2+
3+
import org.springframework.util.MultiValueMap;
4+
5+
import com.unit.daybook.domain.auth.dto.request.OauthProvider;
6+
7+
public interface OAuthLoginParams {
8+
OauthProvider oAuthProvider();
9+
MultiValueMap<String, String> makeBody();
10+
}

src/main/java/com/unit/daybook/domain/auth/controller/.gitkeep

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.unit.daybook.domain.auth.controller;
2+
3+
import org.springframework.http.HttpHeaders;
4+
import org.springframework.http.ResponseEntity;
5+
import org.springframework.web.bind.annotation.PostMapping;
6+
import org.springframework.web.bind.annotation.RequestBody;
7+
import org.springframework.web.bind.annotation.RequestMapping;
8+
import org.springframework.web.bind.annotation.RequestParam;
9+
import org.springframework.web.bind.annotation.RestController;
10+
11+
import com.unit.daybook.domain.auth.dto.response.SocialLoginResponse;
12+
import com.unit.daybook.domain.auth.kakao.KakaoLoginParams;
13+
import com.unit.daybook.domain.auth.service.AuthService;
14+
import com.unit.daybook.global.util.CookieUtil;
15+
16+
import lombok.RequiredArgsConstructor;
17+
18+
@RestController
19+
@RequestMapping("/auth")
20+
@RequiredArgsConstructor
21+
public class AuthController {
22+
23+
private final AuthService authService;
24+
private final CookieUtil cookieUtil;
25+
26+
@PostMapping("/kakao")
27+
public ResponseEntity<SocialLoginResponse> memberSocialLogin(
28+
@RequestBody KakaoLoginParams params
29+
) {
30+
SocialLoginResponse response = authService.socialLoginMember(params);
31+
32+
String accessToken = response.accessToken();
33+
String refreshToken = response.refreshToken();
34+
HttpHeaders tokenHeaders = cookieUtil.generateTokenCookies(accessToken, refreshToken);
35+
36+
return ResponseEntity.ok().headers(tokenHeaders).body(response);
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.unit.daybook.domain.auth.dto.request;
2+
3+
public record KakaoCodeRequest (String code) {
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.unit.daybook.domain.auth.dto.request;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@AllArgsConstructor
8+
public enum OauthProvider {
9+
KAKAO("KAKAO"),
10+
;
11+
private final String value;
12+
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.unit.daybook.domain.auth.dto.response;
2+
3+
public record AuthTokenResponse(
4+
String accessToken,
5+
String refreshToken,
6+
String grantType,
7+
Long expiresIn
8+
) {
9+
public static AuthTokenResponse of(String accessToken, String refreshToken, String grantType, Long expiresIn) {
10+
return new AuthTokenResponse(accessToken, refreshToken, grantType, expiresIn);
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.unit.daybook.domain.auth.dto.response;
2+
3+
import com.unit.daybook.domain.member.domain.Member;
4+
5+
public record SocialLoginResponse(
6+
Long memberId,
7+
String accessToken,
8+
String refreshToken) {
9+
10+
public static SocialLoginResponse of(
11+
Member member, TokenPairResponse tokenPairResponse) {
12+
return new SocialLoginResponse(
13+
member.getId(),
14+
tokenPairResponse.accessToken(),
15+
tokenPairResponse.refreshToken());
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.unit.daybook.domain.auth.dto.response;
2+
3+
public record TokenPairResponse(
4+
String accessToken,
5+
String refreshToken
6+
) {
7+
8+
public static TokenPairResponse from(String accessToken, String refreshToken) {
9+
return new TokenPairResponse(accessToken, refreshToken);
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.unit.daybook.domain.auth.kakao;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.beans.factory.annotation.Value;
5+
import org.springframework.http.HttpEntity;
6+
import org.springframework.http.HttpHeaders;
7+
import org.springframework.http.HttpMethod;
8+
import org.springframework.http.MediaType;
9+
import org.springframework.stereotype.Component;
10+
import org.springframework.util.LinkedMultiValueMap;
11+
import org.springframework.util.MultiValueMap;
12+
13+
import com.unit.daybook.domain.auth.common.OAuthApiClient;
14+
import com.unit.daybook.domain.auth.common.OAuthInfoResponse;
15+
import com.unit.daybook.domain.auth.common.OAuthLoginParams;
16+
import com.unit.daybook.domain.auth.dto.request.OauthProvider;
17+
import com.unit.daybook.global.error.exception.CustomException;
18+
import com.unit.daybook.global.error.exception.ErrorCode;
19+
import org.springframework.web.client.RestTemplate;
20+
21+
@Component
22+
@RequiredArgsConstructor
23+
public class KakaoApiClient implements OAuthApiClient {
24+
private static final String GRANT_TYPE = "authorization_code";
25+
26+
@Value("${oauth.kakao.url.auth}")
27+
private String authUrl;
28+
29+
@Value("${oauth.kakao.url.api}")
30+
private String apiUrl;
31+
32+
@Value("${oauth.kakao.client-id}")
33+
private String clientId;
34+
35+
@Value("${oauth.kakao.client-secret")
36+
private String clientSecret;
37+
38+
private final RestTemplate restTemplate;
39+
40+
@Override
41+
public OauthProvider oAuthProvider() {
42+
return OauthProvider.KAKAO;
43+
}
44+
45+
@Override
46+
public String requestAccessToken(OAuthLoginParams params) {
47+
String url = authUrl + "/oauth/token";
48+
49+
HttpHeaders httpHeaders = new HttpHeaders();
50+
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
51+
52+
MultiValueMap<String, String> body = params.makeBody();
53+
body.add("grant_type", GRANT_TYPE);
54+
body.add("client_id", clientId);
55+
body.add("client_secret", clientSecret);
56+
57+
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, httpHeaders);
58+
59+
KakaoTokens response = restTemplate.exchange(url, HttpMethod.POST, request, KakaoTokens.class).getBody();
60+
61+
if (response == null) {
62+
throw new CustomException(ErrorCode.KAKAO_RESPONSE_NOT_FOUND);
63+
}
64+
return response.getAccessToken();
65+
}
66+
67+
@Override
68+
public OAuthInfoResponse requestOAuthInfo(String accessToken) {
69+
String url = apiUrl + "/v2/user/me";
70+
71+
HttpHeaders httpHeaders = new HttpHeaders();
72+
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
73+
httpHeaders.set("Authorization", "Bearer " + accessToken);
74+
75+
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
76+
body.add("property_keys", "[\"id\", \"kakao_account.\", \"properties.\", \"has_signed_up.\"]");
77+
78+
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, httpHeaders);
79+
80+
return restTemplate.exchange(url, HttpMethod.POST, request, KakaoInfoResponse.class).getBody();
81+
}
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.unit.daybook.domain.auth.kakao;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import com.unit.daybook.domain.auth.common.OAuthInfoResponse;
6+
import com.unit.daybook.domain.auth.dto.request.OauthProvider;
7+
8+
import lombok.Getter;
9+
10+
@Getter
11+
@JsonIgnoreProperties(ignoreUnknown = true)
12+
public class KakaoInfoResponse implements OAuthInfoResponse {
13+
14+
// https://kapi.kakao.com/v2/user/me 요청 결과 값
15+
@JsonProperty("kakao_account")
16+
private KakaoAccount kakaoAccount;
17+
18+
@JsonIgnoreProperties(ignoreUnknown = true)
19+
private String id;
20+
21+
@Getter
22+
@JsonIgnoreProperties(ignoreUnknown = true)
23+
static class KakaoAccount {
24+
private KakaoProfile profile;
25+
private String id;
26+
private String email;
27+
private String gender;
28+
private String birthday;
29+
private String age_range;
30+
}
31+
32+
@Getter
33+
@JsonIgnoreProperties(ignoreUnknown = true)
34+
static class KakaoProfile {
35+
private String id;
36+
private String nickname;
37+
private String profile_image;
38+
}
39+
40+
@Override
41+
public String getSnsId() {
42+
return id;
43+
}
44+
45+
@Override
46+
public String getEmail() {
47+
return kakaoAccount.email;
48+
}
49+
50+
@Override
51+
public String getNickname() {
52+
return kakaoAccount.profile.nickname;
53+
}
54+
55+
@Override
56+
public OauthProvider getOAuthProvider() {
57+
return OauthProvider.KAKAO;
58+
}
59+
60+
@Override
61+
public String getProfileImageUrl() {
62+
return kakaoAccount.profile.profile_image;
63+
}
64+
65+
@Override
66+
public String getGender() {
67+
return kakaoAccount.gender;
68+
}
69+
70+
@Override
71+
public String getBirthday() {
72+
if (kakaoAccount.birthday != null) {
73+
String month = kakaoAccount.birthday.substring(0, 2);
74+
String day = kakaoAccount.birthday.substring(2);
75+
return month + "-" + day;
76+
} else {
77+
return null;
78+
}
79+
}
80+
81+
@Override
82+
public String getAgeRange() {
83+
return kakaoAccount.age_range;
84+
}
85+
86+
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.unit.daybook.domain.auth.kakao;
2+
3+
import org.springframework.util.LinkedMultiValueMap;
4+
import org.springframework.util.MultiValueMap;
5+
6+
import com.unit.daybook.domain.auth.common.OAuthLoginParams;
7+
import com.unit.daybook.domain.auth.dto.request.OauthProvider;
8+
9+
import lombok.Getter;
10+
import lombok.NoArgsConstructor;
11+
12+
@Getter
13+
@NoArgsConstructor
14+
public class KakaoLoginParams implements OAuthLoginParams {
15+
private String code;
16+
17+
@Override
18+
public OauthProvider oAuthProvider() {
19+
return OauthProvider.KAKAO;
20+
}
21+
22+
// Kakao Server에서 response받은 code 값 chaining
23+
@Override
24+
public MultiValueMap<String, String> makeBody() {
25+
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
26+
body.add("code", code);
27+
return body;
28+
}
29+
}

0 commit comments

Comments
 (0)