Skip to content

Commit 37c5a8d

Browse files
authored
feat: apis,infra google social login (#141) (#142)
* [BOOK-469] feat: apis,infra google social login (#141) * [BOOK-469] fix(ci): ci 수정 - 테스트 프로필에 oauth 설정 추가 - 로컬에서 sonar task 스킵되도록 수정 * [BOOK-469] fix: apis,infra - 구글 소셜로그인 리팩토링(#142)
1 parent 1167e92 commit 37c5a8d

File tree

14 files changed

+285
-33
lines changed

14 files changed

+285
-33
lines changed

apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import jakarta.validation.constraints.NotBlank
55
import org.yapp.apis.auth.exception.AuthErrorCode
66
import org.yapp.apis.auth.exception.AuthException
77
import org.yapp.apis.auth.strategy.signin.AppleAuthCredentials
8+
import org.yapp.apis.auth.strategy.signin.GoogleAuthCredentials
89
import org.yapp.apis.auth.strategy.signin.KakaoAuthCredentials
910
import org.yapp.apis.auth.strategy.signin.SignInCredentials
1011
import org.yapp.domain.user.ProviderType
@@ -61,6 +62,8 @@ data class SocialLoginRequest private constructor(
6162
)
6263
AppleAuthCredentials(request.validOauthToken(), authCode)
6364
}
65+
66+
ProviderType.GOOGLE -> GoogleAuthCredentials(request.validOauthToken())
6467
}
6568
}
6669
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.yapp.apis.auth.manager
2+
3+
import mu.KotlinLogging
4+
import org.springframework.stereotype.Component
5+
import org.springframework.web.client.HttpClientErrorException
6+
import org.yapp.apis.auth.exception.AuthErrorCode
7+
import org.yapp.apis.auth.exception.AuthException
8+
import org.yapp.apis.config.GoogleOauthProperties
9+
import org.yapp.infra.external.oauth.google.GoogleApi
10+
import org.yapp.infra.external.oauth.google.response.GoogleUserInfo
11+
12+
@Component
13+
class GoogleApiManager(
14+
private val googleApi: GoogleApi,
15+
private val googleOauthProperties: GoogleOauthProperties,
16+
) {
17+
private val log = KotlinLogging.logger {}
18+
19+
fun getUserInfo(accessToken: String): GoogleUserInfo {
20+
return googleApi.fetchUserInfo(accessToken, googleOauthProperties.url.userInfo)
21+
.onSuccess { userInfo ->
22+
log.info { "Successfully fetched Google user info for userId: ${userInfo.id}" }
23+
}
24+
.getOrElse { exception ->
25+
log.error(exception) { "Failed to fetch Google user info" }
26+
27+
when (exception) {
28+
is HttpClientErrorException -> throw AuthException(
29+
AuthErrorCode.INVALID_OAUTH_TOKEN,
30+
"Invalid Google Access Token.",
31+
)
32+
33+
else -> throw AuthException(
34+
AuthErrorCode.OAUTH_SERVER_ERROR,
35+
"Failed to communicate with Google server.",
36+
)
37+
}
38+
}
39+
}
40+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.yapp.apis.auth.strategy.signin
2+
3+
import mu.KotlinLogging
4+
import org.springframework.stereotype.Component
5+
import org.yapp.apis.auth.dto.response.UserCreateInfoResponse
6+
import org.yapp.apis.auth.exception.AuthErrorCode
7+
import org.yapp.apis.auth.exception.AuthException
8+
import org.yapp.apis.auth.manager.GoogleApiManager
9+
import org.yapp.apis.auth.util.NicknameGenerator
10+
import org.yapp.domain.user.ProviderType
11+
import org.yapp.infra.external.oauth.google.response.GoogleUserInfo
12+
13+
@Component
14+
class GoogleSignInStrategy(
15+
private val googleApiManager: GoogleApiManager
16+
) : SignInStrategy {
17+
18+
private val log = KotlinLogging.logger {}
19+
20+
override fun getProviderType(): ProviderType = ProviderType.GOOGLE
21+
22+
override fun authenticate(credentials: SignInCredentials): UserCreateInfoResponse {
23+
return try {
24+
val googleCredentials = validateCredentials(credentials)
25+
val googleUser = googleApiManager.getUserInfo(googleCredentials.accessToken)
26+
createUserInfo(googleUser)
27+
} catch (exception: Exception) {
28+
log.error("Google authentication failed", exception)
29+
when (exception) {
30+
is AuthException -> throw exception
31+
else -> throw AuthException(AuthErrorCode.FAILED_TO_GET_USER_INFO, exception.message)
32+
}
33+
}
34+
}
35+
36+
private fun validateCredentials(credentials: SignInCredentials): GoogleAuthCredentials {
37+
return credentials as? GoogleAuthCredentials
38+
?: throw AuthException(
39+
AuthErrorCode.INVALID_CREDENTIALS,
40+
"Credentials must be GoogleAuthCredentials"
41+
)
42+
}
43+
44+
private fun createUserInfo(googleUser: GoogleUserInfo): UserCreateInfoResponse {
45+
return UserCreateInfoResponse.of(
46+
email = googleUser.email ?: ("google_${googleUser.id}@google.com"),
47+
nickname = NicknameGenerator.generate(),
48+
profileImageUrl = googleUser.picture,
49+
providerType = ProviderType.GOOGLE,
50+
providerId = googleUser.id
51+
)
52+
}
53+
}

apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInCredentials.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,23 @@ sealed class SignInCredentials {
77
}
88

99
data class KakaoAuthCredentials(
10-
val accessToken: String
10+
val accessToken: String,
1111
) : SignInCredentials() {
1212
override fun getProviderType(): ProviderType = ProviderType.KAKAO
1313
}
1414

1515
data class AppleAuthCredentials(
1616
val idToken: String,
17-
val authorizationCode: String
17+
val authorizationCode: String,
1818
) : SignInCredentials() {
1919
override fun getProviderType(): ProviderType = ProviderType.APPLE
2020
}
21+
22+
data class GoogleAuthCredentials(
23+
val accessToken: String,
24+
) : SignInCredentials() {
25+
override fun getProviderType(): ProviderType {
26+
return ProviderType.GOOGLE
27+
}
28+
}
29+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.yapp.apis.config
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties
4+
5+
@ConfigurationProperties(prefix = "oauth.google")
6+
data class GoogleOauthProperties(
7+
val url: Url
8+
)
9+
10+
data class Url(
11+
val userInfo: String
12+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.yapp.apis.config
2+
3+
import org.springframework.boot.context.properties.EnableConfigurationProperties
4+
import org.springframework.context.annotation.Configuration
5+
6+
@Configuration
7+
@EnableConfigurationProperties(GoogleOauthProperties::class)
8+
class PropertiesConfig

apis/src/main/resources/application.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ swagger:
6161
description: YAPP API Documentation for Development
6262
version: v1.0.0-dev
6363

64+
oauth:
65+
google:
66+
url:
67+
user-info: https://www.googleapis.com/oauth2/v2/userinfo
68+
6469
---
6570
spring:
6671
config:
@@ -85,3 +90,8 @@ springdoc:
8590
enabled: false
8691
api-docs:
8792
enabled: false
93+
94+
oauth:
95+
google:
96+
url:
97+
user-info: https://www.googleapis.com/oauth2/v2/userinfo

apis/src/main/resources/static/kakao-login.html

Lines changed: 85 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -53,38 +53,50 @@
5353
color: #000;
5454
}
5555

56-
.apple-btn {
57-
background-color: #000;
58-
color: #fff;
59-
}
60-
</style>
61-
</head>
62-
<body>
63-
64-
<h1 style="text-align:center">소셜 로그인 테스트</h1>
65-
66-
<div class="tabs">
67-
<div class="tab active" data-tab="kakao">카카오 로그인</div>
68-
<div class="tab" data-tab="apple">애플 로그인</div>
69-
</div>
70-
71-
<div class="tab-content active" id="kakao">
72-
<p style="text-align:center;">카카오 계정으로 로그인하려면 아래 버튼을 클릭하세요.</p>
73-
<div style="text-align:center;">
74-
<button id="kakao-login-btn" class="btn kakao-btn">카카오 로그인</button>
75-
</div>
76-
</div>
77-
78-
<div class="tab-content" id="apple">
79-
<p style="text-align:center;">애플 계정으로 로그인하려면 아래 버튼을 클릭하세요.</p>
80-
<div style="text-align:center;">
81-
<button id="apple-login-btn" class="btn apple-btn">Apple 로그인</button>
82-
</div>
83-
</div>
84-
56+
.apple-btn {
57+
background-color: #000;
58+
color: #fff;
59+
}
60+
.google-btn {
61+
background-color: #4285F4;
62+
color: #fff;
63+
}
64+
</style>
65+
</head>
66+
<body>
67+
68+
<h1 style="text-align:center">소셜 로그인 테스트</h1>
69+
70+
<div class="tabs">
71+
<div class="tab active" data-tab="kakao">카카오 로그인</div>
72+
<div class="tab" data-tab="apple">애플 로그인</div>
73+
<div class="tab" data-tab="google">구글 로그인</div>
74+
</div>
75+
76+
<div class="tab-content active" id="kakao">
77+
<p style="text-align:center;">카카오 계정으로 로그인하려면 아래 버튼을 클릭하세요.</p>
78+
<div style="text-align:center;">
79+
<button id="kakao-login-btn" class="btn kakao-btn">카카오 로그인</button>
80+
</div>
81+
</div>
82+
83+
<div class="tab-content" id="apple">
84+
<p style="text-align:center;">애플 계정으로 로그인하려면 아래 버튼을 클릭하세요.</p>
85+
<div style="text-align:center;">
86+
<button id="apple-login-btn" class="btn apple-btn">Apple 로그인</button>
87+
</div>
88+
</div>
89+
90+
<div class="tab-content" id="google">
91+
<p style="text-align:center;">구글 계정으로 로그인하려면 아래 버튼을 클릭하세요.</p>
92+
<div style="text-align:center;">
93+
<button id="google-login-btn" class="btn google-btn">Google 로그인</button>
94+
</div>
95+
</div>
8596
<!-- 결과 출력 -->
8697
<pre id="result" style="margin-top: 30px; white-space: pre-wrap;"></pre>
8798

99+
<meta name="google-signin-client_id" content="">
88100
<!-- Kakao SDK -->
89101
<script src="https://developers.kakao.com/sdk/js/kakao.js"></script>
90102

@@ -93,6 +105,26 @@ <h1 style="text-align:center">소셜 로그인 테스트</h1>
93105
src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
94106

95107
<script>
108+
// Google Sign-In Callback (Global function)
109+
function onSignIn(googleUser) {
110+
var id_token = googleUser.getAuthResponse().id_token;
111+
fetch(`${API_SERVER}/api/v1/auth/signin`, {
112+
method: 'POST',
113+
headers: {'Content-Type': 'application/json'},
114+
body: JSON.stringify({
115+
providerType: "GOOGLE",
116+
oauthToken: id_token
117+
})
118+
})
119+
.then(res => res.json())
120+
.then(data => {
121+
document.getElementById('result').textContent = 'Google 로그인 성공\n\nJWT: ' + data.accessToken;
122+
})
123+
.catch(err => {
124+
document.getElementById('result').textContent = 'Google 로그인 실패: ' + err.message;
125+
});
126+
}
127+
96128
// 탭 전환
97129
const tabs = document.querySelectorAll('.tab');
98130
const contents = document.querySelectorAll('.tab-content');
@@ -238,7 +270,30 @@ <h1 style="text-align:center">소셜 로그인 테스트</h1>
238270
});
239271
}
240272
});
241-
</script>
242273

274+
// Google Sign-In Initialization
275+
var GoogleAuth; // GoogleAuth object
276+
277+
function initGoogleAuth() {
278+
gapi.client.init({
279+
clientId: document.querySelector('meta[name="google-signin-client_id"]').content,
280+
scope: 'profile email'
281+
}).then(function () {
282+
GoogleAuth = gapi.auth2.getAuthInstance();
283+
// Attach the click listener to the Google login button
284+
document.getElementById('google-login-btn').addEventListener('click', () => {
285+
GoogleAuth.signIn().then(onSignIn, (error) => {
286+
console.error('Google Sign-In failed:', error);
287+
document.getElementById('result').textContent = 'Google 로그인 실패: ' + JSON.stringify(error);
288+
});
289+
});
290+
});
291+
}
292+
293+
function handleGoogleClientLoad() {
294+
gapi.load('client:auth2', initGoogleAuth);
295+
}
296+
</script>
297+
<script async defer src="https://apis.google.com/js/api.js" onload="this.onload=function(){};handleGoogleClientLoad();"></script>
243298
</body>
244299
</html>

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ sonar {
192192
// SonarQube 태스크가 통합 JaCoCo 리포트에 의존하도록 설정
193193
tasks.named("sonar") {
194194
dependsOn("jacocoRootReport")
195+
onlyIf { System.getenv("SONAR_TOKEN") != null }
195196
}
196197

197198
/**

domain/src/main/kotlin/org/yapp/domain/user/ProviderType.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ package org.yapp.domain.user
44
* Enum representing different authentication providers.
55
*/
66
enum class ProviderType {
7-
KAKAO, APPLE
7+
KAKAO, APPLE, GOOGLE
88
}

0 commit comments

Comments
 (0)