From 687b680053483d836b90f7e9d95fa99b3e0227be Mon Sep 17 00:00:00 2001 From: gwonwoo-nam Date: Sat, 29 Nov 2025 11:52:54 +0900 Subject: [PATCH 1/6] feat: apple login --- build.gradle | 6 ++ .../be/user/controller/UserController.kt | 73 ++++++++++++++++--- .../user/dto/response/AppleLoginResponse.kt | 14 ++++ .../be/user/dto/token/AppleOauthInfo.kt | 45 ++++++++++++ .../kotlin/upbrella/be/user/entity/User.kt | 19 +++++ .../be/user/service/OauthLoginService.kt | 20 ++++- .../upbrella/be/user/service/UserService.kt | 25 +++++++ .../upbrella/be/util/AppleJwtGenerator.kt | 62 ++++++++++++++++ .../be/rent/controller/RentControllerTest.kt | 29 +++++--- .../upbrella/be/rent/entity/HistoryTest.kt | 12 ++- .../service/ConditionReportServiceTest.kt | 12 ++- .../be/rent/service/RentServiceTest.kt | 17 +++-- .../be/slack/service/SlackAlarmServiceTest.kt | 17 +++-- .../be/user/controller/UserControllerTest.kt | 4 + .../user/integration/UserIntegrationTest.kt | 5 ++ .../be/user/service/UserServiceDynamicTest.kt | 17 +++-- 16 files changed, 328 insertions(+), 49 deletions(-) create mode 100644 src/main/kotlin/upbrella/be/user/dto/response/AppleLoginResponse.kt create mode 100644 src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.kt create mode 100644 src/main/kotlin/upbrella/be/util/AppleJwtGenerator.kt diff --git a/build.gradle b/build.gradle index eb17f04f..f97de18f 100644 --- a/build.gradle +++ b/build.gradle @@ -87,6 +87,12 @@ dependencies { // datadog implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-datadog' + + // JWT for Apple Sign In + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'org.bouncycastle:bcpkix-jdk15on:1.70' } // Kotlin 컴파일 옵션 설정 diff --git a/src/main/kotlin/upbrella/be/user/controller/UserController.kt b/src/main/kotlin/upbrella/be/user/controller/UserController.kt index dac3381e..f94537fd 100644 --- a/src/main/kotlin/upbrella/be/user/controller/UserController.kt +++ b/src/main/kotlin/upbrella/be/user/controller/UserController.kt @@ -9,6 +9,7 @@ import upbrella.be.user.dto.request.JoinRequest import upbrella.be.user.dto.request.LoginCodeRequest import upbrella.be.user.dto.request.UpdateBankAccountRequest import upbrella.be.user.dto.response.* +import upbrella.be.user.dto.token.AppleOauthInfo import upbrella.be.user.dto.token.KakaoOauthInfo import upbrella.be.user.dto.token.OauthToken import upbrella.be.user.exception.InvalidLoginCodeException @@ -26,6 +27,7 @@ class UserController( private val oauthLoginService: OauthLoginService, private val userService: UserService, private val kakaoOauthInfo: KakaoOauthInfo, + private val appleOauthInfo: AppleOauthInfo, private val rentService: RentService, private val blackListService: BlackListService, ) { @@ -86,16 +88,52 @@ class UserController( )) } + @PostMapping("/users/oauth/apple/login") + fun appleLogin(session: HttpSession, @RequestBody code: LoginCodeRequest): ResponseEntity> { + val appleAccessToken: OauthToken + + try { + appleAccessToken = oauthLoginService.getOauthToken(code.code, appleOauthInfo)!! + } catch (e: HttpClientErrorException) { + throw InvalidLoginCodeException("[ERROR] 로그인 코드가 유효하지 않습니다.") + } + + val appleLoggedInUser = oauthLoginService.processAppleLogin(appleAccessToken.accessToken, appleOauthInfo.loginUri) + session.setAttribute("appleUser", appleLoggedInUser) + + return ResponseEntity + .ok() + .body(CustomResponse( + "success", + 200, + "애플 로그인 성공", + null + )) + } + @PostMapping("/users/login") fun upbrellaLogin(session: HttpSession): ResponseEntity> { - if (session.getAttribute("kakaoUser") == null) { - throw NotSocialLoginedException("[ERROR] 카카오 로그인을 먼저 해주세요.") + val kakaoUser = session.getAttribute("kakaoUser") as? KakaoLoginResponse + val appleUser = session.getAttribute("appleUser") as? AppleLoginResponse + + if (kakaoUser == null && appleUser == null) { + throw NotSocialLoginedException("[ERROR] 소셜 로그인을 먼저 해주세요.") } - val kakaoUser = session.getAttribute("kakaoUser") as KakaoLoginResponse - val loggedInUser = userService.login(kakaoUser.id!!) + val loggedInUser = when { + kakaoUser != null -> { + val user = userService.login(kakaoUser.id!!) + session.removeAttribute("kakaoUser") + user + } + appleUser != null -> { + val user = userService.loginApple(appleUser.sub!!) + session.removeAttribute("appleUser") + user + } + else -> throw NotSocialLoginedException("[ERROR] 소셜 로그인을 먼저 해주세요.") + } - session.removeAttribute("kakaoUser") session.setAttribute("user", loggedInUser) log.info("UUL 로그인 성공") @@ -125,17 +163,30 @@ class UserController( @PostMapping("/users/join") fun kakaoJoin(session: HttpSession, @RequestBody @Valid joinRequest: JoinRequest): ResponseEntity> { - val kakaoUser = session.getAttribute("kakaoUser") as KakaoLoginResponse? + val kakaoUser = session.getAttribute("kakaoUser") as? KakaoLoginResponse + val appleUser = session.getAttribute("appleUser") as? AppleLoginResponse if (session.getAttribute("user") != null) { throw LoginedMemberException("[ERROR] 이미 로그인된 상태입니다.") } - if (kakaoUser == null) { - throw NotSocialLoginedException("[ERROR] 카카오 로그인을 먼저 해주세요.") + if (kakaoUser == null && appleUser == null) { + throw NotSocialLoginedException("[ERROR] 소셜 로그인을 먼저 해주세요.") + } + + val loggedInUser = when { + kakaoUser != null -> { + val user = userService.join(kakaoUser, joinRequest) + session.removeAttribute("kakaoUser") + user + } + appleUser != null -> { + val user = userService.joinApple(appleUser, joinRequest) + session.removeAttribute("appleUser") + user + } + else -> throw NotSocialLoginedException("[ERROR] 소셜 로그인을 먼저 해주세요.") } - val loggedInUser = userService.join(kakaoUser, joinRequest) - session.removeAttribute("kakaoId") session.setAttribute("user", loggedInUser) log.info("UNU 회원가입 성공") @@ -144,7 +195,7 @@ class UserController( .body(CustomResponse( "success", 200, - "카카오 회원가입 성공", + "소셜 회원가입 성공", null )) } diff --git a/src/main/kotlin/upbrella/be/user/dto/response/AppleLoginResponse.kt b/src/main/kotlin/upbrella/be/user/dto/response/AppleLoginResponse.kt new file mode 100644 index 00000000..29f8fad3 --- /dev/null +++ b/src/main/kotlin/upbrella/be/user/dto/response/AppleLoginResponse.kt @@ -0,0 +1,14 @@ +package upbrella.be.user.dto.response + +import com.fasterxml.jackson.annotation.JsonProperty + +data class AppleLoginResponse( + @JsonProperty("sub") + val sub: String?, // Apple user identifier + + @JsonProperty("email") + val email: String?, + + @JsonProperty("email_verified") + val emailVerified: Boolean? +) diff --git a/src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.kt b/src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.kt new file mode 100644 index 00000000..2dae9443 --- /dev/null +++ b/src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.kt @@ -0,0 +1,45 @@ +package upbrella.be.user.dto.token + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import upbrella.be.util.AppleJwtGenerator +import javax.annotation.PostConstruct + +@Component +open class AppleOauthInfo( + @Value("\${APPLE_CLIENT_ID_DEV}") + val clientId: String, + + @Value("\${APPLE_TEAM_ID_DEV}") + private val teamId: String, + + @Value("\${APPLE_KEY_ID_DEV}") + private val keyId: String, + + @Value("\${APPLE_P8_KEY_PATH_DEV}") + private val p8KeyPath: String, + + @Value("\${APPLE_REDIRECT_URI_DEV}") + val redirectUri: String, + + @Value("\${APPLE_LOGIN_URI_DEV}") + val loginUri: String, + + private val appleJwtGenerator: AppleJwtGenerator +) { + private var _clientSecret: String = "" + + val clientSecret: String + get() = _clientSecret + + @PostConstruct + fun init() { + // Apple JWT Client Secret을 동적으로 생성 + _clientSecret = appleJwtGenerator.generateClientSecret( + teamId = teamId, + keyId = keyId, + clientId = clientId, + p8KeyPath = p8KeyPath + ) + } +} diff --git a/src/main/kotlin/upbrella/be/user/entity/User.kt b/src/main/kotlin/upbrella/be/user/entity/User.kt index 49eb6d1b..08fb9255 100644 --- a/src/main/kotlin/upbrella/be/user/entity/User.kt +++ b/src/main/kotlin/upbrella/be/user/entity/User.kt @@ -1,6 +1,7 @@ package upbrella.be.user.entity import upbrella.be.user.dto.request.JoinRequest +import upbrella.be.user.dto.response.AppleLoginResponse import upbrella.be.user.dto.response.KakaoLoginResponse import upbrella.be.util.AesEncryptor import upbrella.be.util.BaseTimeEntity @@ -15,6 +16,7 @@ class User( var name: String, var phoneNumber: String, var email: String, + var provider: String = "KAKAO", // KAKAO or APPLE var adminStatus: Boolean = false, var bank: String? = null, var accountNumber: String? = null, @@ -34,6 +36,23 @@ class User( name = joinRequest.name, phoneNumber = joinRequest.phoneNumber, email = kakaoUser.kakaoAccount?.email ?: "", + provider = "KAKAO", + bank = aesEncryptor.encrypt(joinRequest.bank), + accountNumber = aesEncryptor.encrypt(joinRequest.accountNumber) + ) + } + + fun createNewAppleUser( + appleUser: AppleLoginResponse, + joinRequest: JoinRequest, + aesEncryptor: AesEncryptor + ): User { + return User( + socialId = appleUser.sub.hashCode().toLong(), + name = joinRequest.name, + phoneNumber = joinRequest.phoneNumber, + email = appleUser.email ?: "", + provider = "APPLE", bank = aesEncryptor.encrypt(joinRequest.bank), accountNumber = aesEncryptor.encrypt(joinRequest.accountNumber) ) diff --git a/src/main/kotlin/upbrella/be/user/service/OauthLoginService.kt b/src/main/kotlin/upbrella/be/user/service/OauthLoginService.kt index 7c3ad658..c70f953e 100644 --- a/src/main/kotlin/upbrella/be/user/service/OauthLoginService.kt +++ b/src/main/kotlin/upbrella/be/user/service/OauthLoginService.kt @@ -5,7 +5,9 @@ import org.springframework.stereotype.Service import org.springframework.util.LinkedMultiValueMap import org.springframework.util.MultiValueMap import org.springframework.web.client.RestTemplate +import upbrella.be.user.dto.response.AppleLoginResponse import upbrella.be.user.dto.response.KakaoLoginResponse +import upbrella.be.user.dto.token.AppleOauthInfo import upbrella.be.user.dto.token.KakaoOauthInfo import upbrella.be.user.dto.token.OauthToken @@ -15,6 +17,14 @@ class OauthLoginService( ) { fun getOauthToken(code: String, oauthInfo: KakaoOauthInfo): OauthToken? { + return getOauthTokenInternal(code, oauthInfo.clientId, oauthInfo.clientSecret, oauthInfo.redirectUri) + } + + fun getOauthToken(code: String, oauthInfo: AppleOauthInfo): OauthToken? { + return getOauthTokenInternal(code, oauthInfo.clientId, oauthInfo.clientSecret, oauthInfo.redirectUri) + } + + private fun getOauthTokenInternal(code: String, clientId: String, clientSecret: String, redirectUri: String): OauthToken? { val headers: MultiValueMap = LinkedMultiValueMap().apply { setAll( mapOf( @@ -28,15 +38,15 @@ class OauthLoginService( setAll( mapOf( "grant_type" to "authorization_code", - "client_id" to oauthInfo.clientId, - "client_secret" to oauthInfo.clientSecret, + "client_id" to clientId, + "client_secret" to clientSecret, "code" to code ) ) } val request = HttpEntity(requestPayloads, headers) - val response = restTemplate.postForEntity(oauthInfo.redirectUri, request, OauthToken::class.java) + val response = restTemplate.postForEntity(redirectUri, request, OauthToken::class.java) return response.body } @@ -62,4 +72,8 @@ class OauthLoginService( fun processKakaoLogin(accessToken: String, loginUri: String): KakaoLoginResponse? { return processLogin(accessToken, loginUri, KakaoLoginResponse::class.java) } + + fun processAppleLogin(accessToken: String, loginUri: String): AppleLoginResponse? { + return processLogin(accessToken, loginUri, AppleLoginResponse::class.java) + } } diff --git a/src/main/kotlin/upbrella/be/user/service/UserService.kt b/src/main/kotlin/upbrella/be/user/service/UserService.kt index 4a33229d..149bce70 100644 --- a/src/main/kotlin/upbrella/be/user/service/UserService.kt +++ b/src/main/kotlin/upbrella/be/user/service/UserService.kt @@ -7,6 +7,7 @@ import upbrella.be.rent.service.RentService import upbrella.be.user.dto.request.JoinRequest import upbrella.be.user.dto.request.UpdateBankAccountRequest import upbrella.be.user.dto.response.AllUsersInfoResponse +import upbrella.be.user.dto.response.AppleLoginResponse import upbrella.be.user.dto.response.KakaoLoginResponse import upbrella.be.user.dto.response.SessionUser import upbrella.be.user.dto.response.UmbrellaBorrowedByUserResponse @@ -36,6 +37,12 @@ class UserService( return SessionUser.fromUser(foundUser) } + fun loginApple(appleSub: String): SessionUser { + val foundUser = userReader.findBySocialId(appleSub.hashCode().toLong()) + + return SessionUser.fromUser(foundUser) + } + fun join(kakaoUser: KakaoLoginResponse, joinRequest: JoinRequest): SessionUser { val socialIdHash = kakaoUser.id.hashCode().toLong() @@ -54,6 +61,24 @@ class UserService( return SessionUser.fromUser(joinedUser) } + fun joinApple(appleUser: AppleLoginResponse, joinRequest: JoinRequest): SessionUser { + val socialIdHash = appleUser.sub.hashCode().toLong() + + if (userReader.existsBySocialId(socialIdHash)) { + throw ExistingMemberException("[ERROR] 이미 가입된 회원입니다. 로그인 폼으로 이동합니다.") + } + + if (blackListReader.existsBySocialId(socialIdHash)) { + throw BlackListUserException("[ERROR] 정지된 회원입니다. 정지된 회원은 재가입이 불가능합니다.") + } + + val joinedUser = userWriter.save( + User.createNewAppleUser(appleUser, joinRequest, aesEncryptor) + ) + + return SessionUser.fromUser(joinedUser) + } + fun findUsers(): AllUsersInfoResponse { val users = userReader.findAll() users.forEach { it.decryptData(aesEncryptor) } diff --git a/src/main/kotlin/upbrella/be/util/AppleJwtGenerator.kt b/src/main/kotlin/upbrella/be/util/AppleJwtGenerator.kt new file mode 100644 index 00000000..53661cfb --- /dev/null +++ b/src/main/kotlin/upbrella/be/util/AppleJwtGenerator.kt @@ -0,0 +1,62 @@ +package upbrella.be.util + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.SignatureAlgorithm +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo +import org.bouncycastle.openssl.PEMParser +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter +import org.springframework.core.io.ClassPathResource +import org.springframework.stereotype.Component +import java.io.StringReader +import java.security.PrivateKey +import java.util.* + +@Component +class AppleJwtGenerator { + + /** + * Apple p8 키 파일로 JWT Client Secret 생성 + * + * @param teamId Apple Developer Team ID (10자리) + * @param keyId p8 키 파일의 Key ID + * @param clientId Apple Service ID (com.example.app) + * @param p8KeyPath 클래스패스 내 p8 파일 경로 (예: "keys/AuthKey_ABCD123456.p8") + * @return JWT 토큰 문자열 + */ + fun generateClientSecret( + teamId: String, + keyId: String, + clientId: String, + p8KeyPath: String + ): String { + val now = Date() + val expirationTime = Date(now.time + 15777000000L) // 6개월 (최대) + + val privateKey = loadPrivateKey(p8KeyPath) + + return Jwts.builder() + .setHeaderParam("kid", keyId) + .setHeaderParam("alg", "ES256") + .setIssuer(teamId) + .setIssuedAt(now) + .setExpiration(expirationTime) + .setAudience("https://appleid.apple.com") + .setSubject(clientId) + .signWith(privateKey, SignatureAlgorithm.ES256) + .compact() + } + + /** + * p8 파일에서 PrivateKey 로드 + */ + private fun loadPrivateKey(p8KeyPath: String): PrivateKey { + val resource = ClassPathResource(p8KeyPath) + val p8Content = resource.inputStream.bufferedReader().use { it.readText() } + + val pemParser = PEMParser(StringReader(p8Content)) + val privateKeyInfo = pemParser.readObject() as PrivateKeyInfo + pemParser.close() + + return JcaPEMKeyConverter().getPrivateKey(privateKeyInfo) + } +} diff --git a/src/test/kotlin/upbrella/be/rent/controller/RentControllerTest.kt b/src/test/kotlin/upbrella/be/rent/controller/RentControllerTest.kt index db192b12..da81f55c 100644 --- a/src/test/kotlin/upbrella/be/rent/controller/RentControllerTest.kt +++ b/src/test/kotlin/upbrella/be/rent/controller/RentControllerTest.kt @@ -133,14 +133,15 @@ class RentControllerTest : RestDocsSupport() { ) val userToReturn = User( - 1L, - "테스터1", - "010-1111-1111", - "email", - false, - null, - null, - 11L + socialId = 1L, + name = "테스터1", + phoneNumber = "010-1111-1111", + email = "email", + provider = "KAKAO", + adminStatus = false, + bank = null, + accountNumber = null, + id = 11L ) val storeMeta = FixtureBuilderFactory.builderStoreMeta().sample() @@ -217,7 +218,17 @@ class RentControllerTest : RestDocsSupport() { conditionReport = "필요하다면 상태 신고를 해주세요." ) - val newUser = User(1L, "테스터1", "010-1111-1111", "email", false, null, null, 11L) + val newUser = User( + socialId = 1L, + name = "테스터1", + phoneNumber = "010-1111-1111", + email = "email", + provider = "KAKAO", + adminStatus = false, + bank = null, + accountNumber = null, + id = 11L + ) val session = MockHttpSession() session.setAttribute("user", sessionUser) diff --git a/src/test/kotlin/upbrella/be/rent/entity/HistoryTest.kt b/src/test/kotlin/upbrella/be/rent/entity/HistoryTest.kt index e93a8aec..73bbc88f 100644 --- a/src/test/kotlin/upbrella/be/rent/entity/HistoryTest.kt +++ b/src/test/kotlin/upbrella/be/rent/entity/HistoryTest.kt @@ -46,7 +46,17 @@ class HistoryTest { missed = false, ) - userToRent = User(1L, "테스터", "010-1234-5678", "email", false, null, null, 11L) + userToRent = User( + socialId = 1L, + name = "테스터", + phoneNumber = "010-1234-5678", + email = "email", + provider = "KAKAO", + adminStatus = false, + bank = null, + accountNumber = null, + id = 11L + ) } @Test diff --git a/src/test/kotlin/upbrella/be/rent/service/ConditionReportServiceTest.kt b/src/test/kotlin/upbrella/be/rent/service/ConditionReportServiceTest.kt index abf5520a..524510ca 100644 --- a/src/test/kotlin/upbrella/be/rent/service/ConditionReportServiceTest.kt +++ b/src/test/kotlin/upbrella/be/rent/service/ConditionReportServiceTest.kt @@ -59,7 +59,17 @@ class ConditionReportServiceTest { missed = false, ) - userToRent = User(1L, "테스터", "010-1234-5678", "email", false, null, null, 11L) + userToRent = User( + socialId = 1L, + name = "테스터", + phoneNumber = "010-1234-5678", + email = "email", + provider = "KAKAO", + adminStatus = false, + bank = null, + accountNumber = null, + id = 11L + ) history = History( id = 33L, diff --git a/src/test/kotlin/upbrella/be/rent/service/RentServiceTest.kt b/src/test/kotlin/upbrella/be/rent/service/RentServiceTest.kt index a6160974..cd9472e4 100644 --- a/src/test/kotlin/upbrella/be/rent/service/RentServiceTest.kt +++ b/src/test/kotlin/upbrella/be/rent/service/RentServiceTest.kt @@ -118,14 +118,15 @@ class RentServiceTest { ) userToRent = User( - 0L, - "테스터", - "010-1234-5678", - "email", - false, - null, - null, - 11L + socialId = 0L, + name = "테스터", + phoneNumber = "010-1234-5678", + email = "email", + provider = "KAKAO", + adminStatus = false, + bank = null, + accountNumber = null, + id = 11L ) history = History( diff --git a/src/test/kotlin/upbrella/be/slack/service/SlackAlarmServiceTest.kt b/src/test/kotlin/upbrella/be/slack/service/SlackAlarmServiceTest.kt index bcd70bc2..5df4e71c 100644 --- a/src/test/kotlin/upbrella/be/slack/service/SlackAlarmServiceTest.kt +++ b/src/test/kotlin/upbrella/be/slack/service/SlackAlarmServiceTest.kt @@ -70,14 +70,15 @@ class SlackAlarmServiceTest { ) userToRent = User( - 0L, - "테스터", - "010-1234-5678", - "email", - false, - null, - null, - 11L + socialId = 0L, + name = "테스터", + phoneNumber = "010-1234-5678", + email = "email", + provider = "KAKAO", + adminStatus = false, + bank = null, + accountNumber = null, + id = 11L ) history = History( diff --git a/src/test/kotlin/upbrella/be/user/controller/UserControllerTest.kt b/src/test/kotlin/upbrella/be/user/controller/UserControllerTest.kt index 3ad23dc8..894e727c 100644 --- a/src/test/kotlin/upbrella/be/user/controller/UserControllerTest.kt +++ b/src/test/kotlin/upbrella/be/user/controller/UserControllerTest.kt @@ -33,6 +33,7 @@ import upbrella.be.user.dto.request.JoinRequest import upbrella.be.user.dto.request.KakaoAccount import upbrella.be.user.dto.request.LoginCodeRequest import upbrella.be.user.dto.response.* +import upbrella.be.user.dto.token.AppleOauthInfo import upbrella.be.user.dto.token.KakaoOauthInfo import upbrella.be.user.dto.token.OauthToken import upbrella.be.user.entity.User @@ -52,6 +53,8 @@ class UserControllerTest ( @Mock private val kakaoOauthInfo: KakaoOauthInfo, @Mock + private val appleOauthInfo: AppleOauthInfo, + @Mock private val rentService: RentService, @Mock private val aesEncryptor: AesEncryptor, @@ -64,6 +67,7 @@ class UserControllerTest ( oauthLoginService = oauthLoginService, userService = userService, kakaoOauthInfo = kakaoOauthInfo, + appleOauthInfo = appleOauthInfo, rentService = rentService, blackListService = blackListService, ) diff --git a/src/test/kotlin/upbrella/be/user/integration/UserIntegrationTest.kt b/src/test/kotlin/upbrella/be/user/integration/UserIntegrationTest.kt index 3bef1212..2f5f6b37 100644 --- a/src/test/kotlin/upbrella/be/user/integration/UserIntegrationTest.kt +++ b/src/test/kotlin/upbrella/be/user/integration/UserIntegrationTest.kt @@ -25,6 +25,7 @@ import upbrella.be.user.controller.UserController import upbrella.be.user.dto.request.KakaoAccount import upbrella.be.user.dto.request.LoginCodeRequest import upbrella.be.user.dto.response.KakaoLoginResponse +import upbrella.be.user.dto.token.AppleOauthInfo import upbrella.be.user.dto.token.KakaoOauthInfo import upbrella.be.user.dto.token.OauthToken import upbrella.be.user.entity.BlackList @@ -55,6 +56,9 @@ class UserIntegrationTest : RestDocsSupport() { @Autowired private lateinit var kakaoOauthInfo: KakaoOauthInfo + @Autowired + private lateinit var appleOauthInfo: AppleOauthInfo + @Autowired private lateinit var rentService: RentService @@ -78,6 +82,7 @@ class UserIntegrationTest : RestDocsSupport() { oauthLoginService = oauthLoginService, userService = userService, kakaoOauthInfo = kakaoOauthInfo, + appleOauthInfo = appleOauthInfo, rentService = rentService, blackListService = blackListService, ) diff --git a/src/test/kotlin/upbrella/be/user/service/UserServiceDynamicTest.kt b/src/test/kotlin/upbrella/be/user/service/UserServiceDynamicTest.kt index a3645523..986b4e74 100644 --- a/src/test/kotlin/upbrella/be/user/service/UserServiceDynamicTest.kt +++ b/src/test/kotlin/upbrella/be/user/service/UserServiceDynamicTest.kt @@ -36,14 +36,15 @@ class UserServiceDynamicTest { fun joinTest(): Collection { // given val user = User( - 23132L, - "홍길동", - "010-2084-3478", - "email@email.com", - false, - aesEncryptor.encrypt("신한"), - aesEncryptor.encrypt("110-421-674103"), - 1L + socialId = 23132L, + name = "홍길동", + phoneNumber = "010-2084-3478", + email = "email@email.com", + provider = "KAKAO", + adminStatus = false, + bank = aesEncryptor.encrypt("신한"), + accountNumber = aesEncryptor.encrypt("110-421-674103"), + id = 1L ) val joinRequest = JoinRequest( From 18c034207cc1effd11796f47c24cd2fa025e499b Mon Sep 17 00:00:00 2001 From: gwonwoo-nam Date: Sat, 29 Nov 2025 12:37:53 +0900 Subject: [PATCH 2/6] fix test --- src/test/resources/application-test.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 27c61b88..022cb8f9 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -21,3 +21,10 @@ logging: type: descriptor: sql: TRACE + +APPLE_CLIENT_ID_DEV: test-client-id +APPLE_TEAM_ID_DEV: test-team-id +APPLE_KEY_ID_DEV: test-key-id +APPLE_P8_KEY_PATH_DEV: keys/AuthKey_TEST.p8 +APPLE_REDIRECT_URI_DEV: https://test.redirect.uri +APPLE_LOGIN_URI_DEV: https://test.login.uri From 316cedaca848e6beb575bb6bc0a75acef9184450 Mon Sep 17 00:00:00 2001 From: gwonwoo-nam Date: Sat, 29 Nov 2025 12:38:06 +0900 Subject: [PATCH 3/6] test auth --- src/test/resources/keys/AuthKey_TEST.p8 | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/test/resources/keys/AuthKey_TEST.p8 diff --git a/src/test/resources/keys/AuthKey_TEST.p8 b/src/test/resources/keys/AuthKey_TEST.p8 new file mode 100644 index 00000000..fbc09ca2 --- /dev/null +++ b/src/test/resources/keys/AuthKey_TEST.p8 @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgPGJKUUO/Pk7S7u5c +VLfvBLGWRfHR3Ja6mZjKJg8Q0c2gCgYIKoZIzj0DAQehRANCAARLYCMOVGDALwBK +bKD6QdqFv8i5dX3LVxHiCcBPdlQPqFHpkOLQPF8eHvKuqTmVNjjZ9L5q6pDKxIUH +Qr0qJKqC +-----END PRIVATE KEY----- From 2f6db8385a072e20aaf2ff6f10611cf732280839 Mon Sep 17 00:00:00 2001 From: gwonwoo-nam Date: Sat, 29 Nov 2025 15:35:14 +0900 Subject: [PATCH 4/6] fix test --- .../be/umbrella/integration/UmbrellaIntegrationTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/kotlin/upbrella/be/umbrella/integration/UmbrellaIntegrationTest.kt b/src/test/kotlin/upbrella/be/umbrella/integration/UmbrellaIntegrationTest.kt index 91a8a778..7e4bd8f6 100644 --- a/src/test/kotlin/upbrella/be/umbrella/integration/UmbrellaIntegrationTest.kt +++ b/src/test/kotlin/upbrella/be/umbrella/integration/UmbrellaIntegrationTest.kt @@ -86,13 +86,14 @@ class UmbrellaIntegrationTest : RestDocsSupport() { } // 대여 중 우산 2개 생성 - repeat(2) { + repeat(2) { i -> val umbrella = FixtureBuilderFactory.builderUmbrella() .set("id", null) .set("storeMeta", storeMeta) .set("rentable", false) .set("deleted", false) .set("missed", false) + .set("uuid", 100L + i) // uuid를 100번대로 설정 .sample() umbrellaList.add(umbrella) @@ -108,6 +109,7 @@ class UmbrellaIntegrationTest : RestDocsSupport() { .set("rentable", false) .set("deleted", false) .set("missed", true) + .set("uuid", 200L) // uuid를 200으로 설정 .sample() umbrellaList.add(umbrella) From 69578287a9fa5ac565c8db7012596b0ded7329c9 Mon Sep 17 00:00:00 2001 From: gwonwoo-nam Date: Sat, 29 Nov 2025 18:45:06 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20api=EB=A1=9C=20Return=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=8F=99=EC=9E=91=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/upbrella/be/config/AuthConfig.kt | 2 + .../be/config/ProductionCorsConfig.kt | 1 + .../upbrella/be/config/SessionCookieConfig.kt | 23 ++++ .../be/user/controller/UserController.kt | 52 ++++++++- .../be/user/dto/token/AppleOauthInfo.kt | 61 +++++++++-- .../upbrella/be/user/dto/token/OauthToken.kt | 3 +- .../be/user/service/OauthLoginService.kt | 40 ++++++- .../upbrella/be/util/AppleIdTokenDecoder.kt | 100 ++++++++++++++++++ src/main/resources/application-dev.yml | 13 ++- src/main/resources/application-prod.yml | 6 ++ .../be/user/service/OauthLoginServiceTest.kt | 3 + 11 files changed, 286 insertions(+), 18 deletions(-) create mode 100644 src/main/kotlin/upbrella/be/config/SessionCookieConfig.kt create mode 100644 src/main/kotlin/upbrella/be/util/AppleIdTokenDecoder.kt diff --git a/src/main/kotlin/upbrella/be/config/AuthConfig.kt b/src/main/kotlin/upbrella/be/config/AuthConfig.kt index 3fb0d665..88261df9 100644 --- a/src/main/kotlin/upbrella/be/config/AuthConfig.kt +++ b/src/main/kotlin/upbrella/be/config/AuthConfig.kt @@ -24,7 +24,9 @@ class AuthConfig( .excludePathPatterns( "/users/login/**", "/users/oauth/login/**", + "/users/oauth/apple/login/**", "/users/join/**", + "/auth/apple/**", "/stores/**", "/index.html", "/error/**", diff --git a/src/main/kotlin/upbrella/be/config/ProductionCorsConfig.kt b/src/main/kotlin/upbrella/be/config/ProductionCorsConfig.kt index 2d7e6bd0..00bbdc69 100644 --- a/src/main/kotlin/upbrella/be/config/ProductionCorsConfig.kt +++ b/src/main/kotlin/upbrella/be/config/ProductionCorsConfig.kt @@ -16,6 +16,7 @@ class ProductionCorsConfig : WebMvcConfigurer { "https://upbrella.co.kr", "https://www.upbrella.co.kr", "https://api.upbrella.co.kr", + "https://appleid.apple.com", "http://localhost:3000", "http://www.localhost:3000" ) diff --git a/src/main/kotlin/upbrella/be/config/SessionCookieConfig.kt b/src/main/kotlin/upbrella/be/config/SessionCookieConfig.kt new file mode 100644 index 00000000..8b557105 --- /dev/null +++ b/src/main/kotlin/upbrella/be/config/SessionCookieConfig.kt @@ -0,0 +1,23 @@ +package upbrella.be.config + +import org.springframework.boot.web.servlet.ServletContextInitializer +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile +import javax.servlet.SessionCookieConfig + +@Configuration +@Profile("prod") +class SessionCookieConfig { + + @Bean + fun servletContextInitializer(): ServletContextInitializer { + return ServletContextInitializer { servletContext -> + val sessionCookieConfig: SessionCookieConfig = servletContext.sessionCookieConfig + sessionCookieConfig.setSecure(true) + sessionCookieConfig.setHttpOnly(true) + sessionCookieConfig.setDomain("upbrella.co.kr") + sessionCookieConfig.setPath("/") + } + } +} diff --git a/src/main/kotlin/upbrella/be/user/controller/UserController.kt b/src/main/kotlin/upbrella/be/user/controller/UserController.kt index f94537fd..91924863 100644 --- a/src/main/kotlin/upbrella/be/user/controller/UserController.kt +++ b/src/main/kotlin/upbrella/be/user/controller/UserController.kt @@ -4,6 +4,7 @@ import org.slf4j.LoggerFactory import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import org.springframework.web.client.HttpClientErrorException +import org.springframework.web.servlet.view.RedirectView import upbrella.be.rent.service.RentService import upbrella.be.user.dto.request.JoinRequest import upbrella.be.user.dto.request.LoginCodeRequest @@ -88,17 +89,62 @@ class UserController( )) } + @PostMapping("/auth/apple") + fun appleLoginCallback( + session: HttpSession, + @RequestParam code: String, + @RequestParam(required = false) state: String?, + @RequestParam(required = false) id_token: String?, + @RequestParam(required = false) user: String? + ): RedirectView { + log.info("Apple login callback received - code: ${code.take(10)}...") + + val appleOauthToken: OauthToken + + try { + appleOauthToken = oauthLoginService.getOauthToken(code, appleOauthInfo)!! + } catch (e: HttpClientErrorException) { + log.error("Apple login failed", e) + return RedirectView("https://upbrella.co.kr/login?error=apple_login_failed") + } catch (e: Exception) { + log.error("Unexpected error during Apple login", e) + return RedirectView("https://upbrella.co.kr/login?error=server_error") + } + + // id_token이 없으면 에러 + if (appleOauthToken.idToken.isNullOrEmpty()) { + log.error("Apple ID token is null or empty") + return RedirectView("https://upbrella.co.kr/login?error=no_id_token") + } + + try { + val appleLoggedInUser = oauthLoginService.processAppleLogin(appleOauthToken.idToken!!) + session.setAttribute("appleUser", appleLoggedInUser) + + log.info("Apple social login success - redirecting to frontend") + return RedirectView("https://upbrella.co.kr/login?apple=success") + } catch (e: Exception) { + log.error("Apple ID token validation failed", e) + return RedirectView("https://upbrella.co.kr/login?error=token_validation_failed") + } + } + @PostMapping("/users/oauth/apple/login") fun appleLogin(session: HttpSession, @RequestBody code: LoginCodeRequest): ResponseEntity> { - val appleAccessToken: OauthToken + val appleOauthToken: OauthToken try { - appleAccessToken = oauthLoginService.getOauthToken(code.code, appleOauthInfo)!! + appleOauthToken = oauthLoginService.getOauthToken(code.code, appleOauthInfo)!! } catch (e: HttpClientErrorException) { throw InvalidLoginCodeException("[ERROR] 로그인 코드가 유효하지 않습니다.") } - val appleLoggedInUser = oauthLoginService.processAppleLogin(appleAccessToken.accessToken, appleOauthInfo.loginUri) + // id_token이 없으면 에러 + if (appleOauthToken.idToken.isNullOrEmpty()) { + throw InvalidLoginCodeException("[ERROR] Apple ID token을 받지 못했습니다.") + } + + val appleLoggedInUser = oauthLoginService.processAppleLogin(appleOauthToken.idToken!!) session.setAttribute("appleUser", appleLoggedInUser) return ResponseEntity diff --git a/src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.kt b/src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.kt index 2dae9443..406d7721 100644 --- a/src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.kt +++ b/src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.kt @@ -1,14 +1,23 @@ package upbrella.be.user.dto.token import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Profile import org.springframework.stereotype.Component import upbrella.be.util.AppleJwtGenerator import javax.annotation.PostConstruct +interface AppleOauthInfo { + val clientId: String + val redirectUri: String + val loginUri: String + val clientSecret: String +} + @Component -open class AppleOauthInfo( +@Profile("dev", "test") +class DevAppleOauthInfo( @Value("\${APPLE_CLIENT_ID_DEV}") - val clientId: String, + override val clientId: String, @Value("\${APPLE_TEAM_ID_DEV}") private val teamId: String, @@ -20,21 +29,59 @@ open class AppleOauthInfo( private val p8KeyPath: String, @Value("\${APPLE_REDIRECT_URI_DEV}") - val redirectUri: String, + override val redirectUri: String, @Value("\${APPLE_LOGIN_URI_DEV}") - val loginUri: String, + override val loginUri: String, private val appleJwtGenerator: AppleJwtGenerator -) { +) : AppleOauthInfo { private var _clientSecret: String = "" - val clientSecret: String + override val clientSecret: String + get() = _clientSecret + + @PostConstruct + fun init() { + _clientSecret = appleJwtGenerator.generateClientSecret( + teamId = teamId, + keyId = keyId, + clientId = clientId, + p8KeyPath = p8KeyPath + ) + } +} + +@Component +@Profile("prod") +class ProdAppleOauthInfo( + @Value("\${APPLE_CLIENT_ID_PROD}") + override val clientId: String, + + @Value("\${APPLE_TEAM_ID_PROD}") + private val teamId: String, + + @Value("\${APPLE_KEY_ID_PROD}") + private val keyId: String, + + @Value("\${APPLE_P8_KEY_PATH_PROD}") + private val p8KeyPath: String, + + @Value("\${APPLE_REDIRECT_URI_PROD}") + override val redirectUri: String, + + @Value("\${APPLE_LOGIN_URI_PROD}") + override val loginUri: String, + + private val appleJwtGenerator: AppleJwtGenerator +) : AppleOauthInfo { + private var _clientSecret: String = "" + + override val clientSecret: String get() = _clientSecret @PostConstruct fun init() { - // Apple JWT Client Secret을 동적으로 생성 _clientSecret = appleJwtGenerator.generateClientSecret( teamId = teamId, keyId = keyId, diff --git a/src/main/kotlin/upbrella/be/user/dto/token/OauthToken.kt b/src/main/kotlin/upbrella/be/user/dto/token/OauthToken.kt index 60510333..29d0e667 100644 --- a/src/main/kotlin/upbrella/be/user/dto/token/OauthToken.kt +++ b/src/main/kotlin/upbrella/be/user/dto/token/OauthToken.kt @@ -8,5 +8,6 @@ data class OauthToken( val accessToken: String = "", val refreshToken: String = "", val tokenType: String = "", - val expiresIn: Long = 0 + val expiresIn: Long = 0, + val idToken: String? = null // Apple OAuth에서 사용 ) diff --git a/src/main/kotlin/upbrella/be/user/service/OauthLoginService.kt b/src/main/kotlin/upbrella/be/user/service/OauthLoginService.kt index c70f953e..a7a3d17d 100644 --- a/src/main/kotlin/upbrella/be/user/service/OauthLoginService.kt +++ b/src/main/kotlin/upbrella/be/user/service/OauthLoginService.kt @@ -10,10 +10,12 @@ import upbrella.be.user.dto.response.KakaoLoginResponse import upbrella.be.user.dto.token.AppleOauthInfo import upbrella.be.user.dto.token.KakaoOauthInfo import upbrella.be.user.dto.token.OauthToken +import upbrella.be.util.AppleIdTokenDecoder @Service class OauthLoginService( - private val restTemplate: RestTemplate + private val restTemplate: RestTemplate, + private val appleIdTokenDecoder: AppleIdTokenDecoder ) { fun getOauthToken(code: String, oauthInfo: KakaoOauthInfo): OauthToken? { @@ -21,7 +23,7 @@ class OauthLoginService( } fun getOauthToken(code: String, oauthInfo: AppleOauthInfo): OauthToken? { - return getOauthTokenInternal(code, oauthInfo.clientId, oauthInfo.clientSecret, oauthInfo.redirectUri) + return getAppleOauthToken(code, oauthInfo.clientId, oauthInfo.clientSecret, oauthInfo.redirectUri) } private fun getOauthTokenInternal(code: String, clientId: String, clientSecret: String, redirectUri: String): OauthToken? { @@ -51,6 +53,35 @@ class OauthLoginService( return response.body } + private fun getAppleOauthToken(code: String, clientId: String, clientSecret: String, redirectUri: String): OauthToken? { + val headers: MultiValueMap = LinkedMultiValueMap().apply { + setAll( + mapOf( + "Accept" to "application/json", + "Content-Type" to "application/x-www-form-urlencoded;charset=utf-8" + ) + ) + } + + val requestPayloads: MultiValueMap = LinkedMultiValueMap().apply { + setAll( + mapOf( + "grant_type" to "authorization_code", + "client_id" to clientId, + "client_secret" to clientSecret, + "code" to code, + "redirect_uri" to redirectUri + ) + ) + } + + val request = HttpEntity(requestPayloads, headers) + // Apple의 토큰 엔드포인트 사용 + val response = restTemplate.postForEntity("https://appleid.apple.com/auth/token", request, OauthToken::class.java) + + return response.body + } + private fun processLogin(accessToken: String, loginUri: String, responseType: Class): T? { val headers = HttpHeaders().apply { set("Authorization", "Bearer $accessToken") @@ -73,7 +104,8 @@ class OauthLoginService( return processLogin(accessToken, loginUri, KakaoLoginResponse::class.java) } - fun processAppleLogin(accessToken: String, loginUri: String): AppleLoginResponse? { - return processLogin(accessToken, loginUri, AppleLoginResponse::class.java) + fun processAppleLogin(idToken: String): AppleLoginResponse { + // Apple ID token을 디코딩하여 사용자 정보 추출 + return appleIdTokenDecoder.decodeIdToken(idToken) } } diff --git a/src/main/kotlin/upbrella/be/util/AppleIdTokenDecoder.kt b/src/main/kotlin/upbrella/be/util/AppleIdTokenDecoder.kt new file mode 100644 index 00000000..cd8b25d4 --- /dev/null +++ b/src/main/kotlin/upbrella/be/util/AppleIdTokenDecoder.kt @@ -0,0 +1,100 @@ +package upbrella.be.util + +import com.fasterxml.jackson.databind.ObjectMapper +import io.jsonwebtoken.Claims +import io.jsonwebtoken.Jwts +import org.springframework.stereotype.Component +import org.springframework.web.client.RestTemplate +import upbrella.be.user.dto.response.AppleLoginResponse +import java.math.BigInteger +import java.security.KeyFactory +import java.security.PublicKey +import java.security.spec.RSAPublicKeySpec +import java.util.Base64 + +@Component +class AppleIdTokenDecoder( + private val objectMapper: ObjectMapper, + private val restTemplate: RestTemplate +) { + + private data class ApplePublicKey( + val kty: String, + val kid: String, + val use: String, + val alg: String, + val n: String, + val e: String + ) + + private data class ApplePublicKeys( + val keys: List + ) + + fun decodeIdToken(idToken: String): AppleLoginResponse { + // 1. ID Token 검증 + val claims = verifyAndParseIdToken(idToken) + + // 2. Claims에서 사용자 정보 추출 + return AppleLoginResponse( + sub = claims["sub"] as? String, + email = claims["email"] as? String, + emailVerified = claims["email_verified"] as? Boolean + ) + } + + private fun verifyAndParseIdToken(idToken: String): Claims { + // 1. Apple 공개키 가져오기 + val publicKeys = getApplePublicKeys() + + // 2. ID Token의 header에서 kid 추출 + val header = parseHeader(idToken) + val kid = header["kid"] as? String ?: throw IllegalArgumentException("kid not found in token header") + + // 3. kid에 맞는 공개키 찾기 + val matchingKey = publicKeys.keys.find { it.kid == kid } + ?: throw IllegalArgumentException("No matching public key found for kid: $kid") + + // 4. 공개키 생성 + val publicKey = generatePublicKey(matchingKey) + + // 5. JWT 검증 및 파싱 + return try { + Jwts.parserBuilder() + .setSigningKey(publicKey) + .build() + .parseClaimsJws(idToken) + .body + } catch (e: Exception) { + throw IllegalArgumentException("Invalid ID token: ${e.message}", e) + } + } + + private fun getApplePublicKeys(): ApplePublicKeys { + val response = restTemplate.getForEntity( + "https://appleid.apple.com/auth/keys", + ApplePublicKeys::class.java + ) + + return response.body ?: throw IllegalStateException("Failed to get Apple public keys") + } + + private fun parseHeader(idToken: String): Map { + val headerEncoded = idToken.split(".")[0] + val headerDecoded = String(Base64.getUrlDecoder().decode(headerEncoded)) + return objectMapper.readValue(headerDecoded, Map::class.java) as Map + } + + private fun generatePublicKey(key: ApplePublicKey): PublicKey { + val nBytes = Base64.getUrlDecoder().decode(key.n) + val eBytes = Base64.getUrlDecoder().decode(key.e) + + val n = BigInteger(1, nBytes) + val e = BigInteger(1, eBytes) + + val publicKeySpec = RSAPublicKeySpec(n, e) + val keyFactory = KeyFactory.getInstance("RSA") + + return keyFactory.generatePublic(publicKeySpec) + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index eb3e919c..0b877186 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -2,9 +2,9 @@ spring: config: import: "classpath:application.properties" datasource: - url: jdbc:mysql://localhost:3306/upbrella_dev?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=utf8 - username: root - password: ${DEV_DATABASE_PASSWORD} + url: ${DATABASE_URL} + username: ${DATABASE_USERNAME} + password: ${DATABASE_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver jpa: properties: @@ -16,6 +16,13 @@ spring: max-file-size: 10MB max-request-size: 10MB +server: + servlet: + session: + cookie: + same-site: lax + secure: false + management: endpoints: web: diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index b219f3d8..c8688ac2 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -17,6 +17,12 @@ spring: max-file-size: 10MB max-request-size: 10MB +server: + servlet: + session: + cookie: + same-site: none + management: endpoints: web: diff --git a/src/test/kotlin/upbrella/be/user/service/OauthLoginServiceTest.kt b/src/test/kotlin/upbrella/be/user/service/OauthLoginServiceTest.kt index 4f12c8c9..0fdbed66 100644 --- a/src/test/kotlin/upbrella/be/user/service/OauthLoginServiceTest.kt +++ b/src/test/kotlin/upbrella/be/user/service/OauthLoginServiceTest.kt @@ -25,6 +25,9 @@ class OauthLoginServiceTest { @Mock private lateinit var restTemplate: RestTemplate + @Mock + private lateinit var appleIdTokenDecoder: upbrella.be.util.AppleIdTokenDecoder + private lateinit var kakaoOauthInfo: KakaoOauthInfo @InjectMocks From 6825b4314cbdac8a20209c775309bb31b2d1b410 Mon Sep 17 00:00:00 2001 From: gwonwoo-nam Date: Sat, 29 Nov 2025 18:47:31 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/upbrella/be/config/AuthConfig.kt | 1 - .../be/user/controller/UserController.kt | 28 ------------------- 2 files changed, 29 deletions(-) diff --git a/src/main/kotlin/upbrella/be/config/AuthConfig.kt b/src/main/kotlin/upbrella/be/config/AuthConfig.kt index 88261df9..ea61d1a8 100644 --- a/src/main/kotlin/upbrella/be/config/AuthConfig.kt +++ b/src/main/kotlin/upbrella/be/config/AuthConfig.kt @@ -24,7 +24,6 @@ class AuthConfig( .excludePathPatterns( "/users/login/**", "/users/oauth/login/**", - "/users/oauth/apple/login/**", "/users/join/**", "/auth/apple/**", "/stores/**", diff --git a/src/main/kotlin/upbrella/be/user/controller/UserController.kt b/src/main/kotlin/upbrella/be/user/controller/UserController.kt index 91924863..9a2fd1f9 100644 --- a/src/main/kotlin/upbrella/be/user/controller/UserController.kt +++ b/src/main/kotlin/upbrella/be/user/controller/UserController.kt @@ -129,34 +129,6 @@ class UserController( } } - @PostMapping("/users/oauth/apple/login") - fun appleLogin(session: HttpSession, @RequestBody code: LoginCodeRequest): ResponseEntity> { - val appleOauthToken: OauthToken - - try { - appleOauthToken = oauthLoginService.getOauthToken(code.code, appleOauthInfo)!! - } catch (e: HttpClientErrorException) { - throw InvalidLoginCodeException("[ERROR] 로그인 코드가 유효하지 않습니다.") - } - - // id_token이 없으면 에러 - if (appleOauthToken.idToken.isNullOrEmpty()) { - throw InvalidLoginCodeException("[ERROR] Apple ID token을 받지 못했습니다.") - } - - val appleLoggedInUser = oauthLoginService.processAppleLogin(appleOauthToken.idToken!!) - session.setAttribute("appleUser", appleLoggedInUser) - - return ResponseEntity - .ok() - .body(CustomResponse( - "success", - 200, - "애플 로그인 성공", - null - )) - } - @PostMapping("/users/login") fun upbrellaLogin(session: HttpSession): ResponseEntity> { val kakaoUser = session.getAttribute("kakaoUser") as? KakaoLoginResponse