Skip to content

Commit

Permalink
Merge pull request #36 from YAPP-Github/feature/#35
Browse files Browse the repository at this point in the history
feat: ์• ํ”Œ ๋กœ๊ทธ์ธ ์„œ๋ฒ„ ๊ธฐ๋Šฅ ๊ตฌํ˜„
  • Loading branch information
CChuYong authored Jul 18, 2024
2 parents 8f663b9 + 270b665 commit 853b025
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 1 deletion.
2 changes: 2 additions & 0 deletions user-service/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies {
// https://mvnrepository.com/artifact/com.mysql/mysql-connector-j
implementation("com.mysql:mysql-connector-j:8.4.0")


implementation("org.springframework:spring-jdbc")
implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:2.5.0")
implementation("io.asyncer:r2dbc-mysql:1.1.0")
Expand All @@ -43,6 +44,7 @@ dependencies {
implementation("io.jsonwebtoken:jjwt-api:$jjwtVersion")
runtimeOnly("io.jsonwebtoken:jjwt-impl:$jjwtVersion")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:$jjwtVersion")
implementation("com.nimbusds:nimbus-jose-jwt:9.37.2")

implementation("io.projectreactor.tools:blockhound:1.0.9.RELEASE")
implementation("io.micrometer:micrometer-tracing-bridge-otel:1.3.2")
Expand Down
7 changes: 7 additions & 0 deletions user-service/src/main/java/kr/mafoo/user/api/AuthApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import kr.mafoo.user.controller.dto.request.AppleLoginRequest;
import kr.mafoo.user.controller.dto.request.KakaoLoginRequest;
import kr.mafoo.user.controller.dto.request.TokenRefreshRequest;
import kr.mafoo.user.controller.dto.response.LoginResponse;
Expand All @@ -21,6 +22,12 @@ Mono<LoginResponse> loginWithKakao(
@RequestBody KakaoLoginRequest request
);

@Operation(summary = "์• ํ”Œ ๋กœ๊ทธ์ธ" , description = "์• ํ”Œ ์ธ๊ฐ€ ์ฝ”๋“œ๋กœ ๋กœ๊ทธ์ธ(ํ† ํฐ ๋ฐœํ–‰)ํ•ฉ๋‹ˆ๋‹ค.")
@PostMapping("/login/apple")
Mono<LoginResponse> loginWithApple(
@RequestBody AppleLoginRequest request
);

@Operation(summary = "ํ† ํฐ ๊ฐฑ์‹ ", description = "๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์œผ๋กœ ๊ธฐ์กด ํ† ํฐ์„ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค.")
@PostMapping("/refresh")
Mono<LoginResponse> loginWithRefreshToken(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package kr.mafoo.user.config.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;

@ConfigurationProperties(prefix = "app.oauth.apple")
@ConfigurationPropertiesBinding
public record AppleOAuthProperties(
String clientId
) {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kr.mafoo.user.controller;

import kr.mafoo.user.api.AuthApi;
import kr.mafoo.user.controller.dto.request.AppleLoginRequest;
import kr.mafoo.user.controller.dto.request.KakaoLoginRequest;
import kr.mafoo.user.controller.dto.request.TokenRefreshRequest;
import kr.mafoo.user.controller.dto.response.LoginResponse;
Expand All @@ -21,6 +22,13 @@ public Mono<LoginResponse> loginWithKakao(KakaoLoginRequest request) {
.map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken()));
}

@Override
public Mono<LoginResponse> loginWithApple(AppleLoginRequest request) {
return authService
.loginWithApple(request.identityToken())
.map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken()));
}

@Override
public Mono<LoginResponse> loginWithRefreshToken(TokenRefreshRequest request) {
return authService
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kr.mafoo.user.controller.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "์• ํ”Œ ๋กœ๊ทธ์ธ ์š”์ฒญ")
public record AppleLoginRequest(
@Schema(description = "์—‘์„ธ์Šค ์ฝ”๋“œ", example = "test")
String identityToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kr.mafoo.user.controller.dto.response;

public record AppleKeyListResponse(
AppleKeyResponse[] keys
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package kr.mafoo.user.controller.dto.response;

public record AppleKeyResponse(
String kty,
String kid,
String use,
String alg,
String n,
String e
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kr.mafoo.user.controller.dto.response;

public record AppleLoginInfo(
String id
) {
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kr.mafoo.user.enums;

public enum IdentityProvider {
KAKAO
KAKAO,
APPLE
}
62 changes: 62 additions & 0 deletions user-service/src/main/java/kr/mafoo/user/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
package kr.mafoo.user.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.jwk.JWK;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import kr.mafoo.user.config.properties.AppleOAuthProperties;
import kr.mafoo.user.config.properties.KakaoOAuthProperties;
import kr.mafoo.user.controller.dto.response.AppleKeyListResponse;
import kr.mafoo.user.controller.dto.response.AppleKeyResponse;
import kr.mafoo.user.controller.dto.response.AppleLoginInfo;
import kr.mafoo.user.controller.dto.response.KakaoLoginInfo;
import kr.mafoo.user.domain.AuthToken;
import kr.mafoo.user.domain.SocialMemberEntity;
import kr.mafoo.user.enums.IdentityProvider;
import kr.mafoo.user.exception.KakaoLoginFailedException;
import kr.mafoo.user.repository.SocialMemberRepository;
import kr.mafoo.user.util.NicknameGenerator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.security.Key;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.Base64;
import java.util.LinkedHashMap;

@RequiredArgsConstructor
Expand All @@ -22,6 +37,8 @@ public class AuthService {
private final MemberService memberService;
private final JWTTokenService jwtTokenService;
private final KakaoOAuthProperties kakaoOAuthProperties;
private final AppleOAuthProperties appleOAuthProperties;
private final ObjectMapper objectMapper;


public Mono<AuthToken> loginWithKakao(String code) {
Expand All @@ -35,6 +52,17 @@ public Mono<AuthToken> loginWithKakao(String code) {
));
}

public Mono<AuthToken> loginWithApple(String identityToken) {
return getApplePublicKeys()
.flatMap(keyObj -> getUserInfoWithAppleAccessToken(keyObj.keys(), identityToken))
.flatMap(appleLoginInfo -> getOrCreateMember(
IdentityProvider.APPLE,
appleLoginInfo.id(),
NicknameGenerator.generate(),
null
));
}

public Mono<AuthToken> loginWithRefreshToken(String refreshToken){
return Mono
.fromCallable(() -> jwtTokenService.extractUserIdFromRefreshToken(refreshToken))
Expand Down Expand Up @@ -100,4 +128,38 @@ private Mono<KakaoLoginInfo> getUserInfoWithKakaoToken(String kakaoToken){
(String) ((LinkedHashMap)map.get("properties")).get("profile_image")
));
}

private Mono<AppleKeyListResponse> getApplePublicKeys(){
return externalWebClient
.get()
.uri("https://appleid.apple.com/auth/keys")
.retrieve()
.bodyToMono(AppleKeyListResponse.class);
}

private Mono<AppleLoginInfo> getUserInfoWithAppleAccessToken(AppleKeyResponse[] keys, String identityToken) {
return Mono.fromCallable(() -> {
String[] tokenParts = identityToken.split("\\.");
String headerPart = new String(Base64.getDecoder().decode(tokenParts[0]));
JsonNode headerNode = objectMapper.readTree(headerPart);
String kid = headerNode.get("kid").asText();
AppleKeyResponse keyStr = Arrays.stream(keys)
.filter(k -> k.kid().equals(kid))
.findFirst()
.orElseThrow(() -> new RuntimeException("Apple Key not found"));

RSAPublicKey pubKey = JWK.parse(objectMapper.writeValueAsString(keyStr)).toRSAKey().toRSAPublicKey();
Jws<Claims> claims = Jwts.parser()
.verifyWith(pubKey)
.build()
.parseSignedClaims(identityToken);

String client = claims.getPayload().get("aud", String.class);
if (!client.equals(appleOAuthProperties.clientId())) {
throw new RuntimeException();
}

return new AppleLoginInfo(claims.getPayload().get("sub", String.class));
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package kr.mafoo.user.util;

import lombok.NoArgsConstructor;

import java.util.concurrent.ThreadLocalRandom;

@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
public class NicknameGenerator {
private static String[] prefixes = new String[] {
"๋ฉ‹์žˆ๋Š”", "๋ง›์žˆ๋Š”", "๋Œ€๋‹จํ•œ", "์—„์ฒญ๋‚œ", "์œ„๋Œ€ํ•œ", "ํ–‰๋ณตํ•œ", "์ฆ๊ฑฐ์šด", "์ƒˆ๋กœ์šด", "์ž‘์€", "์‹ ๊ธฐํ•œ", "์žฌ๋ฐŒ๋Š”", "๋†€๋ผ์šด"
};
private static String[] suffixes = new String[] { "์‹์‚ฌ",
"๊ณผ์ผ",
"์ฑ…",
"์ปคํ”ผ",
"์ˆ ",
"์Œ์•…",
"์˜ํ™”",
"์ฑ„ํŒ…",
"ํ†ตํ™”",
"์ฝ”๋”ฉ",
"ํœด์‹",
"์—ฌํ–‰",
"์šด๋™",
"๊ฒŒ์ž„",
"๋…์„œ",
"๊ณต๋ถ€",
"์ˆ™์ œ",
"๊ณผ์ œ",
"์ทจ๋ฏธ",
"์ทจ์—…",
"์—ฐ์• ",
"๊ฒฐํ˜ผ",
"ํˆฌ์ž",
"ํ”„๋กœ๊ทธ๋ž˜๋ฐ",
"๊ฐœ๋ฐœ",
"์ฝ”๋”ฉ",
"ํ…Œ์ŠคํŠธ",
"๋ณด์•ˆ" };

public static String generate() {
ThreadLocalRandom random = ThreadLocalRandom.current();
int prefixIndex = random.nextInt(prefixes.length);
int suffixIndex = random.nextInt(suffixes.length);
return prefixes[prefixIndex] + " " + suffixes[suffixIndex];
}

}
2 changes: 2 additions & 0 deletions user-service/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ app:
client-id: ${KAKAO_CLIENT_ID}
client-secret: ${KAKAO_CLIENT_SECRET}
redirect-uri: ${KAKAO_REDIRECT_URL}
apple:
client-id: ${APPLE_CLIENT_ID}
jwt:
verify-key: ${JWT_VERIFY_KEY}
expiration:
Expand Down

0 comments on commit 853b025

Please sign in to comment.