EEYatHo 앱 깎는 이야기

JPA, Swift ) Apple 로그인 클라-서버 2중 인증 - EEYatHo iOS 본문

iOS, Swift/Feature

JPA, Swift ) Apple 로그인 클라-서버 2중 인증 - EEYatHo iOS

EEYatHo 2021. 2. 27. 21:01
반응형

 

카카오, 구글 로그인 iOS 서버 2중 인증에 이어서 애플이다.

 

[ 카카오 로그인 iOS 서버 2중 인증 글 바로가기 ]

 

iOS, Swift, SNS로그인 서버 2중 인증(카카오 로그인 iOS-서버 2중 인증)

열심히 iOS + 서버개발 인턴 하는 중. 시니어 개발자분이 말씀하시길! iOS에서 SNS 로그인으로 토큰을 받고, 서버에 넘겨주면, (토큰의 종류는 SNS마다 다름) 서버는 받은 토큰으로 SNS 서버에 유효성

eeyatho.tistory.com

[ 구글 로그인 iOS 서버 2중 인증 글 바로가기 ]

 

구글 Google 로그인 iOS 서버 2중 인증

카카오 로그인 iOS 서버 2중 인증에 이어서 구글이다. [ 카카오 로그인 iOS 서버 2중 인증 글 바로가기 ] eeyatho.tistory.com/21 iOS, Swift, SNS로그인 서버 2중 인증(카카오 로그인 iOS-서버 2중 인증) 열심히..

eeyatho.tistory.com


[ 3. 애플 ]

카카오랑 구글을 기분좋게 구현하고 애플을 알아봤었는데

애플은 좀 많이 까다로워서 짜증났던 기억이 난다..

 

애플이 구현해둔 AuthenticationServices 라이브러리를 이용.

delegate로 authorization.credential 를 받아오는데,

authorization.credential.identityToken 을 이용하면 된다.

/// 애플 로그인 버튼 클릭 -> Delegate로 이어짐
@objc func tabAppleLogin() {
	if #available(iOS 13.0, *) {
		let appleIDProvider = ASAuthorizationAppleIDProvider()
		let request = appleIDProvider.createRequest()
		request.requestedScopes = [.fullName, .email]
		let authorizationController = ASAuthorizationController(authorizationRequests: [request])

		authorizationController.delegate = self
		authorizationController.presentationContextProvider = self
		authorizationController.performRequests()
	} else {
		ToastManager.showToast(message: "애플 로그인은 iOS 13.0 이상부터 가능합니다.", isTopWindow: false)
	}
}
// 애플 로그인 Delegate
@available(iOS 13.0, *)
extension LoginViewController: ASAuthorizationControllerDelegate,
                               ASAuthorizationControllerPresentationContextProviding {
    // Apple ID 연동 성공 시
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        switch authorization.credential {
        // Apple ID
        case let appleIDCredential as ASAuthorizationAppleIDCredential:
            // 이름
            let name = appleIDCredential.fullName?.description ?? ""
            // accessToken (Data -> 아스키 인코딩 -> 스트링)
            let accessToken = String(data: appleIDCredential.identityToken!, encoding: .ascii) ?? ""
            
            // 로그인 리퀘스트 생성
            let signInRequest = SignInRequest(name: name, accessToken: accessToken, socialType: .apple, uuid: "string")
            // 로그인 리퀘스트 발싸
            NetworkManager.singIn(request: signInRequest) { [weak self] result in
                self?.signInResultProcessor(request: signInRequest, result: result)
            }
        default:
            break
        }
    }
        
    // Apple ID 연동 실패 시
    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        print(error)
    }
    
    // context를 어디에 띄울지
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        return self.view.window!
    }
}

iOS 측에서 authorization.credential.identityToken 을 서버에 보내는 코드다.

 

( 이를 이용하여 서버는 애플의 프로필 정보를 가져올건데,

이 때 이메일만 가져올 수 있지 이름을 못가져온다....

그런데 우리 서비스 회원으로 등록할 때 이름이 필요하기 때문에

지금 이름을 보낼 수 밖에 없다...ㅜㅜ )

 

( uuid는 이전 글들과 동일한 이유로 필요없다. )

 


이를 수신한 서버는 이렇게 처리한다.

( 애플은 카카오, 구글처럼 특정 스트링을 OPEN API에 던져주는 간단한 방식이 아니다 )

( 좀 많이 더럽다 )

 

간단히 설명하자면, 

1. 애플은 OPEN API로 3개의 공개키의 재료들을 넘겨준다.

 

2. iOS에서 받았던 authorization.credential.identityToken 은 JWT토큰이라는 것이며,

JWT토큰에서 말하는 헤더를 뜯어서, 3개 중 어떤 공개키를 사용해야할지 확인한다.

 

3. 알맞는 공개키 재료를 사용해서 공개키를 만들고,

이 공개키로 JWT토큰의 바디부분을 디코딩하면 유저 프로필을 얻을 수 있다.

else if (socialType.equals("apple")) {
  // 애플 accessToken -> 구글 토큰 유효성 확인 및 프로필 정보 얻기
  try {
    // 애플 accessToken의 header를 Base64, UTF-8 디코딩해서, 알맞는 key 재료를 알기위한 kid, alg 알아내기
    String headerOfAccessToken = accessToken.substring(0, accessToken.indexOf("."));
    JsonUtil jsonUtil = new JsonUtil();
    Map<String, String> header = jsonUtil.convert(new String(Base64.getDecoder().decode(headerOfAccessToken), "UTF-8"), Map.class);

    // 애플한테서 key들의 재료들 얻기 + 알맞는 key의 재료 찾기
    ApplePublicKeyResponse keys = ApplePublicKeyResponse.getApplePublicKeys();
    MaterialsOfApplePublicKey key = keys.getMatchedKeyBy(header.get("kid"), header.get("alg"))
    .orElseThrow(() -> new NullPointerException("Failed get public key from apple's id server."));

    // 알맞는 key 재료로 부터, RS256 ( SHA-256 + RSA ) 암호화방식에서 사용하는 n, e 구하기
    byte[] nBytes = Base64.getUrlDecoder().decode(key.getN());
    byte[] eBytes = Base64.getUrlDecoder().decode(key.getE());
    BigInteger n = new BigInteger(1, nBytes);
    BigInteger e = new BigInteger(1, eBytes);

    // n, e를 이용하여 public key 만들기
    RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e);
    KeyFactory keyFactory = KeyFactory.getInstance(key.getKty());
    PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);

    // 만들어진 public key로 accessToken의 body를 디코딩해서 유효한 유저 프로필 얻기
    Claims userInfo = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(accessToken).getBody();

    // 유저 프로필로 SocialDTO 만들어서 리턴
    // 애플은 서버인증시 이름을 알 수 없기때문에 request에 이름 추가..ㅠ
    return SocialDTO.builder().name(name).email(userInfo.get("email", String.class)).build();
  } catch (UnsupportedEncodingException e ) { // UTF-8 디코딩 에러
  } catch (NoSuchAlgorithmException e) { // getUnstance 에러
  } catch (InvalidKeySpecException e) { // generatePublic 에러
  }
} else { ... }

 

여기서 사용하는 ApplePublicKeyResponse 클래스는 아래와 같다.

public class ApplePublicKeyResponse {
    // 애플은 3개의 공개키를 줌
    private List<MaterialsOfApplePublicKey> keys;

    // 3개의 공개키 중 사용할 키 찾기
    public Optional<MaterialsOfApplePublicKey> getMatchedKeyBy(String kid, String alg) {
        return this.keys.stream()
                .filter(key -> key.getKid().equals(kid) && key.getAlg().equals(alg))
                .findFirst();
    }

    static public ApplePublicKeyResponse getApplePublicKeys() {
        // URI 준비
        URI url = URI.create("https://appleid.apple.com/auth/keys");

        // Http Request 발싸
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, String.class);

        // Json 파싱
        JsonUtil jsonUtil = new JsonUtil();
        ApplePublicKeyResponse result = jsonUtil.convert(response.getBody().toString(), ApplePublicKeyResponse.class);
        return result;
    }
}


이렇게 얻은 유효한 유저 정보로,

추후에 우리 서비스의 회원을 만든다.

 

Comments