Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] 애플 소셜 로그인 개발 #83

Merged
merged 1 commit into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions growthookServer/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
public class AuthRequestDto {
private String socialPlatform;
private String socialToken;
private String userName;
}
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand All @@ -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();

Expand Down Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;

Expand Down