diff --git a/growthookServer/build.gradle b/growthookServer/build.gradle index 0875644..7f17770 100644 --- a/growthookServer/build.gradle +++ b/growthookServer/build.gradle @@ -46,6 +46,8 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' + + implementation 'com.google.code.gson:gson:2.10.1' } tasks.named('bootBuildImage') { diff --git a/growthookServer/src/main/java/com/example/growthookserver/api/member/auth/dto/Request/AuthRequestDto.java b/growthookServer/src/main/java/com/example/growthookserver/api/member/auth/dto/Request/AuthRequestDto.java index 7b31afc..5c919d5 100644 --- a/growthookServer/src/main/java/com/example/growthookserver/api/member/auth/dto/Request/AuthRequestDto.java +++ b/growthookServer/src/main/java/com/example/growthookserver/api/member/auth/dto/Request/AuthRequestDto.java @@ -10,4 +10,5 @@ public class AuthRequestDto { private String socialPlatform; private String socialToken; + private String userName; } \ No newline at end of file diff --git a/growthookServer/src/main/java/com/example/growthookserver/api/member/auth/service/AppleAuthService.java b/growthookServer/src/main/java/com/example/growthookserver/api/member/auth/service/AppleAuthService.java new file mode 100644 index 0000000..5ce8093 --- /dev/null +++ b/growthookServer/src/main/java/com/example/growthookserver/api/member/auth/service/AppleAuthService.java @@ -0,0 +1,126 @@ +package com.example.growthookserver.api.member.auth.service; + +import com.example.growthookserver.api.member.auth.dto.SocialInfoDto; +import com.example.growthookserver.common.exception.BaseException; +import com.google.gson.*; +import com.example.growthookserver.common.exception.UnAuthorizedException; +import com.example.growthookserver.common.response.ErrorStatus; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.math.BigInteger; +import java.net.HttpURLConnection; +import java.net.URL; + +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.Base64; +import java.util.Objects; +import lombok.RequiredArgsConstructor; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AppleAuthService { + /** + * 1. apple로 부터 공개키 3개 가져옴 + * 2. 내가 클라에서 가져온 token String과 비교해서 써야할 공개키 확인 (kid,alg 값 같은 것) + * 3. 그 공개키 재료들로 공개키 만들고, 이 공개키로 JWT토큰 부분의 바디 부분의 decode하면 유저 정보 + */ + public SocialInfoDto login(String socialAccessToken, String userName) { + return getAppleSocialData(socialAccessToken, userName); + } + + private JsonArray getApplePublicKeys() { + StringBuffer result = new StringBuffer(); + try { + URL url = new URL("https://appleid.apple.com/auth/keys"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); + + String line = ""; + + while ((line = br.readLine()) != null) { + result.append(line); + } + JsonObject keys = (JsonObject) JsonParser.parseString(result.toString()); + return (JsonArray) keys.get("keys"); // 1. 공개키 가져오기 + } catch (IOException e) { + throw new UnAuthorizedException(ErrorStatus.FAILED_TO_VALIDATE_APPLE_LOGIN.getMessage()); + } + } + + private SocialInfoDto getAppleSocialData(String socialAccessToken, String userName) { + try { + JsonArray publicKeyList = getApplePublicKeys(); + PublicKey publicKey = makePublicKey(socialAccessToken, publicKeyList); + + Claims userInfo = Jwts.parserBuilder() + .setSigningKey(publicKey) + .build() + .parseClaimsJws(socialAccessToken.substring(7)) + .getBody(); + + JsonObject userInfoObject = (JsonObject) JsonParser.parseString(new Gson().toJson(userInfo)); + String appleId = userInfoObject.get("sub").getAsString(); + String email = userInfoObject.get("email").getAsString(); + + return new SocialInfoDto(appleId, userName, email, null); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new BaseException(HttpStatus.INTERNAL_SERVER_ERROR, "애플 계정 데이터 가공 실패"); + } + } + + private PublicKey makePublicKey(String identityToken, JsonArray publicKeyList) throws NoSuchAlgorithmException, InvalidKeySpecException { + JsonObject selectedObject = null; + + String[] decodeArray = identityToken.split("\\."); + String header = new String(Base64.getDecoder().decode(decodeArray[0].substring(7))); + + JsonElement kid = ((JsonObject) JsonParser.parseString(header)).get("kid"); + JsonElement alg = ((JsonObject) JsonParser.parseString(header)).get("alg"); + + for (JsonElement publicKey : publicKeyList) { + JsonObject publicKeyObject = publicKey.getAsJsonObject(); + JsonElement publicKid = publicKeyObject.get("kid"); + JsonElement publicAlg = publicKeyObject.get("alg"); + + if (Objects.equals(kid, publicKid) && Objects.equals(alg, publicAlg)) { + selectedObject = publicKeyObject; + break; + } + } + + if (selectedObject == null) { + throw new InvalidKeySpecException("공개키를 찾을 수 없습니다."); + } + + return getPublicKey(selectedObject); + } + + private PublicKey getPublicKey(JsonObject object) throws NoSuchAlgorithmException, InvalidKeySpecException { + String nStr = object.get("n").toString(); + String eStr = object.get("e").toString(); + + byte[] nBytes = Base64.getUrlDecoder().decode(nStr.substring(1, nStr.length() - 1)); + byte[] eBytes = Base64.getUrlDecoder().decode(eStr.substring(1, eStr.length() - 1)); + + BigInteger n = new BigInteger(1, nBytes); + BigInteger e = new BigInteger(1, eBytes); + + + RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); + return publicKey; + } + +} diff --git a/growthookServer/src/main/java/com/example/growthookserver/api/member/auth/service/Impl/AuthServiceImpl.java b/growthookServer/src/main/java/com/example/growthookserver/api/member/auth/service/Impl/AuthServiceImpl.java index 8c2afc5..18b4ff2 100644 --- a/growthookServer/src/main/java/com/example/growthookserver/api/member/auth/service/Impl/AuthServiceImpl.java +++ b/growthookServer/src/main/java/com/example/growthookserver/api/member/auth/service/Impl/AuthServiceImpl.java @@ -6,6 +6,7 @@ import com.example.growthookserver.api.member.auth.dto.Response.AuthTokenResponseDto; import com.example.growthookserver.api.member.auth.dto.SocialInfoDto; +import com.example.growthookserver.api.member.auth.service.AppleAuthService; import com.example.growthookserver.api.member.auth.service.AuthService; import com.example.growthookserver.api.member.auth.service.KakaoAuthService; @@ -28,6 +29,7 @@ public class AuthServiceImpl implements AuthService { private final JwtTokenProvider jwtTokenProvider; private final KakaoAuthService kakaoAuthService; + private final AppleAuthService appleAuthService; private final MemberRepository memberRepository; @Override @@ -41,7 +43,8 @@ public AuthResponseDto socialLogin(AuthRequestDto authRequestDto) throws NoSuchA try { SocialPlatform socialPlatform = SocialPlatform.valueOf(authRequestDto.getSocialPlatform()); - SocialInfoDto socialData = getSocialData(socialPlatform, authRequestDto.getSocialToken()); + SocialInfoDto socialData = getSocialData(socialPlatform, authRequestDto.getSocialToken(), + authRequestDto.getUserName()); String refreshToken = jwtTokenProvider.generateRefreshToken(); @@ -83,11 +86,13 @@ public AuthTokenResponseDto getNewToken(String accessToken, String refreshToken) return AuthTokenResponseDto.of(accessToken,refreshToken); } - private SocialInfoDto getSocialData(SocialPlatform socialPlatform, String socialAccessToken) throws NoSuchAlgorithmException, InvalidKeySpecException { + private SocialInfoDto getSocialData(SocialPlatform socialPlatform, String socialAccessToken, String userName) { switch (socialPlatform) { case KAKAO: return kakaoAuthService.login(socialAccessToken); + case APPLE: + return appleAuthService.login(socialAccessToken, userName); default: throw new IllegalArgumentException(ErrorStatus.ANOTHER_ACCESS_TOKEN.getMessage()); } diff --git a/growthookServer/src/main/java/com/example/growthookserver/common/response/ErrorStatus.java b/growthookServer/src/main/java/com/example/growthookserver/common/response/ErrorStatus.java index a63ca96..8c79e44 100644 --- a/growthookServer/src/main/java/com/example/growthookserver/common/response/ErrorStatus.java +++ b/growthookServer/src/main/java/com/example/growthookserver/common/response/ErrorStatus.java @@ -25,6 +25,7 @@ public enum ErrorStatus { KAKAO_UNAUTHORIZED_USER("카카오 로그인 실패. 만료되었거나 잘못된 카카오 토큰입니다."), SIGNIN_REQUIRED("access, refreshToken 모두 만료되었습니다. 재로그인이 필요합니다."), VALID_ACCESS_TOKEN("아직 유효한 accessToken 입니다."), + FAILED_TO_VALIDATE_APPLE_LOGIN("애플 로그인 실패"), /** * 404 NOT_FOUND @@ -40,7 +41,8 @@ public enum ErrorStatus { * 500 SERVER_ERROR */ INTERNAL_SERVER_ERROR("예상치 못한 서버 에러가 발생했습니다."), - BAD_GATEWAY_EXCEPTION("일시적인 에러가 발생하였습니다.\n잠시 후 다시 시도해주세요!"); + BAD_GATEWAY_EXCEPTION("일시적인 에러가 발생하였습니다.\n잠시 후 다시 시도해주세요!"), + ; private final String message;