Skip to content

Commit

Permalink
Merge pull request #218 from swm-standard/rin/swm-178
Browse files Browse the repository at this point in the history
Feat: Apple login 구현
  • Loading branch information
RinRinPARK authored Sep 13, 2024
2 parents 47263f9 + d5f9ae8 commit 0506fa6
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 0 deletions.
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.9.0-RC")
implementation("org.springframework.boot:spring-boot-starter-webflux")
testImplementation("com.navercorp.fixturemonkey:fixture-monkey-kotlin:1.0.25")
implementation("org.bouncycastle:bcpkix-jdk18on:1.75")
implementation("com.nimbusds:nimbus-jose-jwt:9.12")

// querydsl
implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.swm_standard.phote.dto.RenewAccessTokenResponse
import com.swm_standard.phote.dto.UserInfoResponse
import com.swm_standard.phote.service.GoogleAuthService
import com.swm_standard.phote.service.KaKaoAuthService
import com.swm_standard.phote.service.AppleAuthService
import com.swm_standard.phote.service.TokenService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
Expand All @@ -22,6 +23,7 @@ import java.util.UUID
class AuthController(
private val googleAuthService: GoogleAuthService,
private val kaKaoAuthService: KaKaoAuthService,
private val appleAuthService: AppleAuthService,
private val tokenService: TokenService,
) {
@Operation(summary = "google-login", description = "구글 로그인/회원가입")
Expand All @@ -48,6 +50,18 @@ class AuthController(
return BaseResponse(msg = message, data = userInfo)
}

@Operation(summary = "apple-login", description = "애플 로그인/회원가입")
@PostMapping("/apple-login")
fun appleLogin(
@RequestBody request: LoginRequest,
): BaseResponse<UserInfoResponse> {
val idToken = appleAuthService.getTokenFromApple(request)
val userInfo = appleAuthService.getUserInfoFromApple(idToken)

val message = if (userInfo.isMember == false) "회원가입 성공" else "로그인 성공"
return BaseResponse(msg = message, data = userInfo)
}

@Operation(summary = "renewAccessToken", description = "refreshToken으로 accessToken 갱신")
@PostMapping("/refreshtoken")
fun renewAccessToken(
Expand Down
153 changes: 153 additions & 0 deletions src/main/kotlin/com/swm_standard/phote/service/AppleAuthService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package com.swm_standard.phote.service

import com.fasterxml.jackson.databind.ObjectMapper
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.SignedJWT
import com.swm_standard.phote.common.authority.JwtTokenProvider
import com.swm_standard.phote.common.module.NicknameGenerator
import com.swm_standard.phote.common.module.ProfileImageGenerator
import com.swm_standard.phote.dto.LoginRequest
import com.swm_standard.phote.dto.UserInfoResponse
import com.swm_standard.phote.entity.Member
import com.swm_standard.phote.entity.Provider
import com.swm_standard.phote.repository.MemberRepository
import io.jsonwebtoken.JwsHeader
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.stereotype.Service
import org.springframework.util.LinkedMultiValueMap
import org.springframework.util.MultiValueMap
import org.springframework.web.client.RestTemplate
import java.security.PrivateKey
import java.security.Security
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.Date
import java.util.Base64

@Service
class AppleAuthService(
private val memberRepository: MemberRepository,
private val jwtTokenProvider: JwtTokenProvider,
private val tokenService: TokenService,
) {
@Value("\${APPLE_CLIENT_ID}")
lateinit var clientId: String

@Value("\${APPLE_KEY_ID}")
lateinit var keyId: String

@Value("\${APPLE_TEAM_ID}")
lateinit var teamId: String

@Value("\${APPLE_AUTH_URL}")
lateinit var authUrl: String

@Value("\${APPLE_K8_KEY}")
lateinit var k8Key: String

fun getTokenFromApple(request: LoginRequest): String {
val headers = HttpHeaders()
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8")

val body: MultiValueMap<String, String> = LinkedMultiValueMap()
body.add("client_id", clientId)
body.add("client_secret", generateClientSecret())
body.add("code", request.code)
body.add("grant_type", "authorization_code")
body.add("redirect_uri", request.redirectUri)

val appleTokenRequest = HttpEntity(body, headers)
val response =
RestTemplate().exchange(
"https://appleid.apple.com/auth/token",
HttpMethod.POST,
appleTokenRequest,
String::class.java,
)

val responseBody = response.body
val objectMapper = ObjectMapper()
val jsonNode = objectMapper.readTree(responseBody)

return jsonNode["id_token"].asText()
}

fun getUserInfoFromApple(token: String): UserInfoResponse {

val signedJWT: SignedJWT = SignedJWT.parse(token)
val getPayload: JWTClaimsSet = signedJWT.getJWTClaimsSet()

val email = getPayload.getStringClaim("email")

var member = memberRepository.findByEmail(email)

val isMember: Boolean

if (member == null) {
val nicknameGenerator = NicknameGenerator()

member =
Member(
name = nicknameGenerator.randomNickname(),
email = email,
image = ProfileImageGenerator().imageGenerator(),
provider = Provider.APPLE,
)

memberRepository.save(member)

isMember = false
} else {
isMember = true
}

val dto =
UserInfoResponse(
name = member.name,
email = member.email,
picture = member.image,
isMember = isMember,
userId = member.id,
accessToken = jwtTokenProvider.createToken(member.id),
)

dto.refreshToken = tokenService.generateRefreshToken(member.id)

return dto
}

private fun generateClientSecret(): String {
val expiration: LocalDateTime = LocalDateTime.now().plusMinutes(5)

return Jwts.builder()
.setHeaderParam(JwsHeader.KEY_ID, keyId)
.setIssuer(teamId)
.setAudience(authUrl)
.setSubject(clientId)
.setExpiration(Date.from(expiration.atZone(ZoneId.systemDefault()).toInstant()))
.setIssuedAt(Date())
.signWith(getPrivateKey(), SignatureAlgorithm.ES256)
.compact()
}

private fun getPrivateKey(): PrivateKey {
Security.addProvider(BouncyCastleProvider())
val converter = JcaPEMKeyConverter().setProvider("BC")

return try {
val privateKeyBytes = Base64.getDecoder().decode(k8Key)
val privateKeyInfo = PrivateKeyInfo.getInstance(privateKeyBytes)
converter.getPrivateKey(privateKeyInfo)
} catch (e: Exception) {
throw RuntimeException("private key생성 실패", e)
}
}
}

0 comments on commit 0506fa6

Please sign in to comment.