Skip to content

Commit

Permalink
[FEAT]애플 소셜 로그인 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
Hong0329 committed Feb 13, 2024
1 parent 7cc022b commit 07c3385
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 17 deletions.
3 changes: 3 additions & 0 deletions DontBeServer/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ dependencies {
implementation 'com.slack.api:slack-app-backend:1.28.0'
implementation 'com.slack.api:slack-api-model:1.28.0'

//* Apple Social Login
implementation 'com.google.code.gson:gson:2.10.1'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.dontbe.www.DontBeServer.api.auth;

public enum SocialPlatform {
KAKAO
KAKAO,
APPLE
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@
public class AuthRequestDto {
@NotNull
private String socialPlatform;

private String userName;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.dontbe.www.DontBeServer.api.auth.service;

import com.dontbe.www.DontBeServer.api.auth.dto.SocialInfoDto;
import com.dontbe.www.DontBeServer.common.exception.BaseException;
import com.dontbe.www.DontBeServer.common.exception.UnAuthorizedException;
import com.dontbe.www.DontBeServer.common.response.ErrorStatus;
import com.google.gson.*;

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 {
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, null, email);
} 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 @@ -4,9 +4,8 @@
import com.dontbe.www.DontBeServer.api.auth.dto.SocialInfoDto;
import com.dontbe.www.DontBeServer.api.auth.dto.response.AuthResponseDto;
import com.dontbe.www.DontBeServer.api.auth.dto.response.AuthTokenResponseDto;
import com.dontbe.www.DontBeServer.api.auth.dto.*;
import com.dontbe.www.DontBeServer.api.auth.dto.request.AuthRequestDto;
import com.dontbe.www.DontBeServer.api.auth.dto.response.*;
import com.dontbe.www.DontBeServer.api.auth.service.AppleAuthService;
import com.dontbe.www.DontBeServer.api.auth.service.AuthService;
import com.dontbe.www.DontBeServer.api.auth.service.KakaoAuthService;
import com.dontbe.www.DontBeServer.api.member.domain.Member;
Expand All @@ -32,14 +31,15 @@ public class AuthServiceImpl implements AuthService {

private final JwtTokenProvider jwtTokenProvider;
private final KakaoAuthService kakaoAuthService;
private final AppleAuthService appleAuthService;
private final MemberRepository memberRepository;

@Override
@Transactional
public AuthResponseDto socialLogin(String socialAccessToken, AuthRequestDto authRequestDto) throws NoSuchAlgorithmException, InvalidKeySpecException {

val socialPlatform = SocialPlatform.valueOf(authRequestDto.getSocialPlatform());
SocialInfoDto socialData = getSocialData(socialPlatform, socialAccessToken);
val socialPlatform = SocialPlatform.valueOf(authRequestDto.getSocialPlatform());
SocialInfoDto socialData = getSocialData(socialPlatform, socialAccessToken, authRequestDto.getUserName());
String refreshToken = jwtTokenProvider.generateRefreshToken();
Boolean isExistUser = isMemberBySocialId(socialData.getId());

Expand Down Expand Up @@ -97,19 +97,15 @@ private boolean isMemberBySocialId(String socialId) {
return memberRepository.existsBySocialId(socialId);
}

private SocialInfoDto getSocialData(SocialPlatform socialPlatform, String socialAccessToken) throws NoSuchAlgorithmException, InvalidKeySpecException {
private SocialInfoDto getSocialData(SocialPlatform socialPlatform, String socialAccessToken, String userName) {

return switch (socialPlatform) {
case KAKAO -> kakaoAuthService.login(socialAccessToken);
default -> {
switch (socialPlatform) {
case KAKAO:
return kakaoAuthService.login(socialAccessToken);
case APPLE:
return appleAuthService.login(socialAccessToken, userName);
default:
throw new IllegalArgumentException(ErrorStatus.ANOTHER_ACCESS_TOKEN.getMessage());
}
};
// switch (socialPlatform) {
// case KAKAO:
// return kakaoAuthService.login(socialAccessToken);
// default:
// throw new IllegalArgumentException(ErrorStatus.ANOTHER_ACCESS_TOKEN.getMessage());
// }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public enum ErrorStatus {
UNAUTHORIZED_MEMBER("권한이 없는 유저입니다."),
UNAUTHORIZED_TOKEN("유효하지 않은 토큰입니다."),
KAKAO_UNAUTHORIZED_USER("카카오 로그인 실패. 만료되었거나 잘못된 카카오 토큰입니다."),
FAILED_TO_VALIDATE_APPLE_LOGIN("애플 로그인 실패. 만료되었거나 잘못된 애플 토큰입니다."),
SIGNIN_REQUIRED("access, refreshToken 모두 만료되었습니다. 재로그인이 필요합니다."),
VALID_ACCESS_TOKEN("아직 유효한 accessToken 입니다."),

Expand Down

0 comments on commit 07c3385

Please sign in to comment.