Skip to content

Commit 82c8918

Browse files
authored
Merge pull request #98 from wafflestudio/feat/make-oauth-log
feat: implement OAuth logs
2 parents 4774f9f + 536542f commit 82c8918

7 files changed

Lines changed: 209 additions & 12 deletions

File tree

src/main/kotlin/com/wafflestudio/team8server/common/exception/GlobalExceptionHandler.kt

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.wafflestudio.team8server.common.exception
22

3-
import com.wafflestudio.team8server.course.service.CourseExcelParser
43
import io.swagger.v3.oas.annotations.media.Schema
4+
import jakarta.servlet.http.HttpServletRequest
55
import org.slf4j.LoggerFactory
66
import org.springframework.dao.CannotAcquireLockException
77
import org.springframework.dao.DataIntegrityViolationException
@@ -36,6 +36,8 @@ data class ErrorResponse(
3636

3737
@RestControllerAdvice
3838
class GlobalExceptionHandler {
39+
private val log = LoggerFactory.getLogger(GlobalExceptionHandler::class.java)
40+
3941
// NullSafety에 위배된 예외 처리 -> 404 NOT FOUND (데이터 없음)
4042
@ExceptionHandler(ResourceNotFoundException::class)
4143
fun handleResourceNotFoundException(e: ResourceNotFoundException): ResponseEntity<ErrorResponse> {
@@ -88,14 +90,23 @@ class GlobalExceptionHandler {
8890

8991
// 유효성 검증 실패 예외 처리 → 400 BAD_REQUEST
9092
@ExceptionHandler(MethodArgumentNotValidException::class)
91-
fun handleValidationException(e: MethodArgumentNotValidException): ResponseEntity<ErrorResponse> {
93+
fun handleValidationException(
94+
e: MethodArgumentNotValidException,
95+
request: HttpServletRequest,
96+
): ResponseEntity<ErrorResponse> {
9297
// 모든 검증 에러를 Map으로 변환 (필드명 → 에러 메시지)
9398
val errors =
9499
e.bindingResult.allErrors.associate { error ->
95100
val field = (error as FieldError).field // 필드명 (email, password 등)
96101
val message = error.defaultMessage // 에러 메시지 (@NotBlank의 message)
97102
field to message // map pair 생성
98103
}
104+
log.warn(
105+
"Request validation failed: method={}, path={}, errors={}",
106+
request.method,
107+
request.requestURI,
108+
errors,
109+
)
99110

100111
val response =
101112
ErrorResponse(
@@ -133,7 +144,18 @@ class GlobalExceptionHandler {
133144
}
134145

135146
@ExceptionHandler(HttpMessageNotReadableException::class)
136-
fun handleHttpMessageNotReadableException(e: HttpMessageNotReadableException): ResponseEntity<ErrorResponse> {
147+
fun handleHttpMessageNotReadableException(
148+
e: HttpMessageNotReadableException,
149+
request: HttpServletRequest,
150+
): ResponseEntity<ErrorResponse> {
151+
log.warn(
152+
"Request body parsing failed: method={}, path={}, contentType={}, cause={}({})",
153+
request.method,
154+
request.requestURI,
155+
request.contentType,
156+
e::class.simpleName,
157+
e.message,
158+
)
137159
val response =
138160
ErrorResponse(
139161
status = HttpStatus.BAD_REQUEST.value(),
@@ -146,7 +168,16 @@ class GlobalExceptionHandler {
146168
}
147169

148170
@ExceptionHandler(UnauthorizedException::class)
149-
fun handleUnauthorizedException(e: UnauthorizedException): ResponseEntity<ErrorResponse> {
171+
fun handleUnauthorizedException(
172+
e: UnauthorizedException,
173+
request: HttpServletRequest,
174+
): ResponseEntity<ErrorResponse> {
175+
log.warn(
176+
"Unauthorized request handled: method={}, path={}, message={}",
177+
request.method,
178+
request.requestURI,
179+
e.message,
180+
)
150181
val response =
151182
ErrorResponse(
152183
status = HttpStatus.UNAUTHORIZED.value(), // 401
@@ -184,8 +215,6 @@ class GlobalExceptionHandler {
184215
// 예상하지 못한 예외를 잡는 handler
185216
@ExceptionHandler(Exception::class)
186217
fun handleUnexpectedException(e: Exception): ResponseEntity<ErrorResponse> {
187-
// 배포 시 로깅 (logger.error("Unexpected error", e))
188-
val log = LoggerFactory.getLogger(CourseExcelParser::class.java)
189218
log.error("UNEXPECTED_ERROR", e)
190219
val response =
191220
ErrorResponse(
@@ -219,7 +248,6 @@ class GlobalExceptionHandler {
219248
CannotAcquireLockException::class,
220249
)
221250
fun handleDbLockException(e: Exception): ResponseEntity<ErrorResponse> {
222-
val log = LoggerFactory.getLogger(GlobalExceptionHandler::class.java)
223251
log.error("DB_LOCK_ERROR", e)
224252

225253
val response =
@@ -234,7 +262,6 @@ class GlobalExceptionHandler {
234262

235263
@ExceptionHandler(QueryTimeoutException::class)
236264
fun handleQueryTimeoutException(e: QueryTimeoutException): ResponseEntity<ErrorResponse> {
237-
val log = LoggerFactory.getLogger(GlobalExceptionHandler::class.java)
238265
log.error("DB_TIMEOUT", e)
239266

240267
val response =
@@ -249,7 +276,6 @@ class GlobalExceptionHandler {
249276

250277
@ExceptionHandler(DataIntegrityViolationException::class)
251278
fun handleDataIntegrityViolationException(e: DataIntegrityViolationException): ResponseEntity<ErrorResponse> {
252-
val log = LoggerFactory.getLogger(GlobalExceptionHandler::class.java)
253279
log.error("DB_CONSTRAINT_VIOLATION", e)
254280

255281
val response =
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.wafflestudio.team8server.config
2+
3+
import org.slf4j.LoggerFactory
4+
import org.springframework.boot.context.event.ApplicationReadyEvent
5+
import org.springframework.context.event.EventListener
6+
import org.springframework.stereotype.Component
7+
8+
@Component
9+
class OAuthConfigurationLogger(
10+
private val props: OAuthProperties,
11+
) {
12+
private val log = LoggerFactory.getLogger(OAuthConfigurationLogger::class.java)
13+
14+
@EventListener(ApplicationReadyEvent::class)
15+
fun logOAuthConfiguration() {
16+
log.info(
17+
"OAuth configuration loaded: kakao.clientId={}, kakao.clientSecret={}, kakao.adminKey={}, " +
18+
"google.clientId={}, google.clientSecret={}",
19+
describe(props.kakao.clientId),
20+
describe(props.kakao.clientSecret),
21+
describe(props.kakao.adminKey),
22+
describe(props.google.clientId),
23+
describe(props.google.clientSecret),
24+
)
25+
}
26+
27+
private fun describe(value: String?): String {
28+
val normalized = value.orEmpty()
29+
return "present=${normalized.isNotBlank()},length=${normalized.length},base64PaddedLike=${isBase64PaddedLike(normalized)}"
30+
}
31+
32+
private fun isBase64PaddedLike(value: String): Boolean {
33+
if (value.isBlank() || value.length % 4 != 0 || !value.endsWith("=")) {
34+
return false
35+
}
36+
return BASE64_PATTERN.matches(value)
37+
}
38+
39+
companion object {
40+
private val BASE64_PATTERN = Regex("^[A-Za-z0-9+/]+={0,2}$")
41+
}
42+
}

src/main/kotlin/com/wafflestudio/team8server/user/controller/AuthController.kt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses
1818
import io.swagger.v3.oas.annotations.security.SecurityRequirement
1919
import io.swagger.v3.oas.annotations.tags.Tag
2020
import jakarta.validation.Valid
21+
import org.slf4j.LoggerFactory
2122
import org.springframework.http.HttpStatus
2223
import org.springframework.web.bind.annotation.PostMapping
2324
import org.springframework.web.bind.annotation.RequestBody
@@ -33,6 +34,8 @@ class AuthController(
3334
private val authService: AuthService,
3435
private val socialAuthService: SocialAuthService,
3536
) {
37+
private val log = LoggerFactory.getLogger(AuthController::class.java)
38+
3639
@Operation(
3740
summary = "회원가입",
3841
description =
@@ -263,7 +266,14 @@ class AuthController(
263266
@ResponseStatus(HttpStatus.OK)
264267
fun kakaoLogin(
265268
@Valid @RequestBody request: SocialLoginRequest,
266-
): LoginResponse = socialAuthService.kakaoLogin(request.code, request.redirectUri)
269+
): LoginResponse {
270+
log.info(
271+
"Social login request received: provider=kakao, codePresent={}, redirectUri={}",
272+
request.code.isNotBlank(),
273+
request.redirectUri,
274+
)
275+
return socialAuthService.kakaoLogin(request.code, request.redirectUri)
276+
}
267277

268278
@Operation(
269279
summary = "구글 소셜 로그인",
@@ -340,7 +350,14 @@ class AuthController(
340350
@ResponseStatus(HttpStatus.OK)
341351
fun googleLogin(
342352
@Valid @RequestBody request: SocialLoginRequest,
343-
): LoginResponse = socialAuthService.googleLogin(request.code, request.redirectUri)
353+
): LoginResponse {
354+
log.info(
355+
"Social login request received: provider=google, codePresent={}, redirectUri={}",
356+
request.code.isNotBlank(),
357+
request.redirectUri,
358+
)
359+
return socialAuthService.googleLogin(request.code, request.redirectUri)
360+
}
344361

345362
@Operation(
346363
summary = "로그아웃",

src/main/kotlin/com/wafflestudio/team8server/user/service/SocialAuthService.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import com.wafflestudio.team8server.user.service.social.google.GoogleIdTokenVeri
1414
import com.wafflestudio.team8server.user.service.social.google.GoogleOAuthClient
1515
import com.wafflestudio.team8server.user.service.social.kakao.KakaoOAuthClient
1616
import com.wafflestudio.team8server.user.util.NicknameGenerator
17+
import org.slf4j.LoggerFactory
1718
import org.springframework.stereotype.Service
1819
import org.springframework.transaction.annotation.Transactional
1920

@@ -26,15 +27,27 @@ class SocialAuthService(
2627
private val googleOAuthClient: GoogleOAuthClient,
2728
private val googleIdTokenVerifier: GoogleIdTokenVerifier,
2829
) {
30+
private val log = LoggerFactory.getLogger(SocialAuthService::class.java)
31+
2932
@Transactional
3033
fun kakaoLogin(
3134
code: String,
3235
redirectUri: String?,
3336
): LoginResponse {
37+
log.info(
38+
"Social login started: provider=kakao, redirectUri={}",
39+
redirectUri,
40+
)
3441
val kakaoUserInfo =
3542
try {
3643
kakaoOAuthClient.getUserInfo(code, redirectUri)
3744
} catch (e: Exception) {
45+
log.warn(
46+
"Social login failed during OAuth exchange: provider=kakao, redirectUri={}, cause={}({})",
47+
redirectUri,
48+
e::class.simpleName,
49+
e.message,
50+
)
3851
throw UnauthorizedException("카카오 인증에 실패했습니다")
3952
}
4053

@@ -44,6 +57,7 @@ class SocialAuthService(
4457

4558
val existingCredential = socialCredentialRepository.findByProviderAndSocialId(provider, socialId)
4659
if (existingCredential != null) {
60+
log.info("Social login succeeded: provider=kakao, existingUser=true")
4761
val accessToken = jwtTokenProvider.createToken(existingCredential.user.id.ensureNotNull(), existingCredential.user.role.name)
4862
return LoginResponse(
4963
accessToken = accessToken,
@@ -67,6 +81,7 @@ class SocialAuthService(
6781
socialId = socialId,
6882
)
6983
socialCredentialRepository.save(credential)
84+
log.info("Social login succeeded: provider=kakao, existingUser=false")
7085

7186
// JWT 발급 및 응답
7287
val accessToken = jwtTokenProvider.createToken(savedUser.id.ensureNotNull(), savedUser.role.name)
@@ -81,10 +96,20 @@ class SocialAuthService(
8196
code: String,
8297
redirectUri: String?,
8398
): LoginResponse {
99+
log.info(
100+
"Social login started: provider=google, redirectUri={}",
101+
redirectUri,
102+
)
84103
val tokenResult =
85104
try {
86105
googleOAuthClient.exchangeCodeForTokenResult(code, redirectUri)
87106
} catch (e: Exception) {
107+
log.warn(
108+
"Social login failed during token exchange: provider=google, redirectUri={}, cause={}({})",
109+
redirectUri,
110+
e::class.simpleName,
111+
e.message,
112+
)
88113
throw UnauthorizedException("구글 인증에 실패했습니다")
89114
}
90115

@@ -94,6 +119,11 @@ class SocialAuthService(
94119
try {
95120
googleIdTokenVerifier.verifyAndExtract(idToken)
96121
} catch (e: Exception) {
122+
log.warn(
123+
"Social login failed during id token verification: provider=google, cause={}({})",
124+
e::class.simpleName,
125+
e.message,
126+
)
97127
throw UnauthorizedException("구글 인증에 실패했습니다")
98128
}
99129

@@ -106,6 +136,7 @@ class SocialAuthService(
106136
if (!tokenResult.refreshToken.isNullOrBlank()) {
107137
existingCredential.refreshToken = tokenResult.refreshToken
108138
}
139+
log.info("Social login succeeded: provider=google, existingUser=true")
109140
val accessToken = jwtTokenProvider.createToken(existingCredential.user.id.ensureNotNull(), existingCredential.user.role.name)
110141
return LoginResponse(
111142
accessToken = accessToken,
@@ -128,6 +159,7 @@ class SocialAuthService(
128159
refreshToken = tokenResult.refreshToken,
129160
)
130161
socialCredentialRepository.save(credential)
162+
log.info("Social login succeeded: provider=google, existingUser=false")
131163

132164
val accessToken = jwtTokenProvider.createToken(savedUser.id.ensureNotNull(), savedUser.role.name)
133165
return LoginResponse(

src/main/kotlin/com/wafflestudio/team8server/user/service/social/google/GoogleIdTokenVerifier.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.wafflestudio.team8server.user.service.social.google
22

33
import com.wafflestudio.team8server.common.exception.UnauthorizedException
44
import com.wafflestudio.team8server.config.OAuthProperties
5+
import org.slf4j.LoggerFactory
56
import org.springframework.security.oauth2.jwt.Jwt
67
import org.springframework.security.oauth2.jwt.JwtDecoder
78
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder
@@ -18,6 +19,7 @@ data class GoogleUserInfo(
1819
class GoogleIdTokenVerifier(
1920
private val props: OAuthProperties,
2021
) {
22+
private val log = LoggerFactory.getLogger(GoogleIdTokenVerifier::class.java)
2123
private val decoder: JwtDecoder =
2224
NimbusJwtDecoder.withJwkSetUri(props.google.jwkSetUri).build()
2325

@@ -43,19 +45,34 @@ class GoogleIdTokenVerifier(
4345
try {
4446
decoder.decode(idToken)
4547
} catch (e: Exception) {
48+
log.warn(
49+
"Google id token decode failed: cause={}({})",
50+
e::class.simpleName,
51+
e.message,
52+
)
4653
throw UnauthorizedException("구글 id_token 검증에 실패했습니다")
4754
}
4855

4956
private fun validateIssuer(jwt: Jwt) {
5057
val issuer = jwt.issuer?.toString()
5158
if (issuer.isNullOrBlank() || issuer != props.google.issuer) {
59+
log.warn(
60+
"Google id token issuer mismatch: expected={}, actual={}",
61+
props.google.issuer,
62+
issuer,
63+
)
5264
throw UnauthorizedException("구글 토큰 issuer가 올바르지 않습니다")
5365
}
5466
}
5567

5668
private fun validateAudience(jwt: Jwt) {
5769
val aud = jwt.audience
5870
if (aud.isNullOrEmpty() || !aud.contains(props.google.clientId)) {
71+
log.warn(
72+
"Google id token audience mismatch: expectedClientIdLength={}, audienceCount={}",
73+
props.google.clientId.length,
74+
aud?.size ?: 0,
75+
)
5976
throw UnauthorizedException("구글 토큰 audience가 올바르지 않습니다")
6077
}
6178
}

0 commit comments

Comments
 (0)