美文网首页
Sign in with Apple

Sign in with Apple

作者: 岑吾 | 来源:发表于2022-04-16 06:15 被阅读0次

    1、前言

    “通过 Apple 登录”让用户能用自己的 Apple ID 轻松登录您的 app 和网站。用户不必填写表单、验证电子邮件地址和选择新密码,就可以使用“通过 Apple 登录”设置帐户并立即开始使用您的 app。所有帐户都通过双重认证受到保护,具有极高的安全性,Apple 亦不会跟踪用户在您的 app 或网站中的活动。

    2、客户端

    在xcode工程中添加Sign in with Apple

    2.1 在项目属性找到Signing & Capabilities,选择添加Capability

    2.2 在弹出的添加页面,输入sign找到Sign in with Apple

    2.3 将Sign in with Apple添加成功

    2.4 添加AuthenticationServices.Framework

    2.5 然后在登录按钮点击事件调用代码登录

    -(void)LoginApple{
    //
        if (@available(iOS 13.0, *)) {
            ASAuthorizationAppleIDProvider *appleIDProvider = [[ASAuthorizationAppleIDProvider alloc] init];
            ASAuthorizationAppleIDRequest *appleIDRequest = [appleIDProvider createRequest];
            appleIDRequest.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
            ASAuthorizationController *authorizationController = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[appleIDRequest]];
            authorizationController.delegate = appController;
            authorizationController.presentationContextProvider = appController;
            [authorizationController performRequests];
        }else{
            NSLog(@"error");
        }
    }
    

    2.6 回调事件

    在回调类中添加 <ASAuthorizationControllerDelegate,ASAuthorizationControllerPresentationContextProviding>

    /// 授权成功回调
    - (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)){
        if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
            ASAuthorizationAppleIDCredential *appleIDCredential = authorization.credential;
            NSData *identityToken = appleIDCredential.identityToken;
            
            // 注意:使用过授权的,可能获取不到以下三个参数,要做判空处理
            NSString *nickname = appleIDCredential.fullName.nickname;
            if(nickname == nullptr ){
                if(appleIDCredential.fullName.familyName != nullptr &&  appleIDCredential.fullName.givenName != nullptr){
                    nickname = [NSString stringWithFormat:@"%@%@",appleIDCredential.fullName.familyName, appleIDCredential.fullName.givenName];
                }else if(appleIDCredential.fullName.familyName != nullptr){
                    nickname = appleIDCredential.fullName.familyName;
                }else if(appleIDCredential.fullName.givenName != nullptr){
                    nickname = appleIDCredential.fullName.givenName;
                }
            }
            
            if(nickname == nullptr){
                nickname = @"";
            }
            
            // 服务器验证需要使用的参数
            NSString *identityTokenStr = [[NSString alloc] initWithData:identityToken encoding:NSUTF8StringEncoding];
            
            // TODO, 将用户名和 token 传给服务器做验证。
            // 注意: 非第一次授权,nickname可能为空。
    
    
        }else if ([authorization.credential isKindOfClass:[ASPasswordCredential class]]){
            // 这个获取的是iCloud记录的账号密码,需要输入框支持iOS 12 记录账号密码的新特性,如果不支持,可以忽略
            ASPasswordCredential *passwordCredential = authorization.credential;
            NSString *user = passwordCredential.user;
            NSString *password = passwordCredential.password;
            
        }else{
            NSLog(@"授权信息不符");
        }
    }
    
    /// 授权失败回调
    - (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)){
        NSLog(@"Handle error:%@", error);
        NSString *errorMsg = nil;
        switch (error.code) {
            case ASAuthorizationErrorCanceled:
                errorMsg = @"用户取消了授权请求";
                break;
            case ASAuthorizationErrorFailed:
                errorMsg = @"授权请求失败";
                break;
            case ASAuthorizationErrorInvalidResponse:
                errorMsg = @"授权请求响应无效";
                break;
            case ASAuthorizationErrorNotHandled:
                errorMsg = @"未能处理授权请求";
                break;
            case ASAuthorizationErrorUnknown:
                errorMsg = @"授权请求失败未知原因";
                break;
            default:
                break;
        }
        NSLog(@"errorMsg = %@", errorMsg);
    }
    
    /// 设置展示内容给用户的Window
    - (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller API_AVAILABLE(ios(13.0)){
        return [UIApplication sharedApplication].windows.lastObject;
    }
    

    3、服务端

    服务端使用客户端登录成功后获取的identityTokenStr值来做验证。identityTokenStr是基于JWT的算法验证。

    eyJraWQiOiJZdXlYb1kiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmh6ZGNtLnl5eWxjIiwiZXhwIjoxNjQ5ODg2NDc1LCJpYXQiOjE2NDk4MDAwNzUsInN1YiI6IjAwMTE5NC4zZjExMjE3OGFkYTI0NDU2YjllYTRjMDBhMTZlZmIyYS4yMTQ3IiwiY19oYXNoIjoiekQ0THpRelYxTjY3TmdlX3ZsTjhTQSIsImVtYWlsIjoibXFoNnF0a2s4eUBwcml2YXRlcmVsYXkuYXBwbGVpZC5jb20iLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJpc19wcml2YXRlX2VtYWlsIjoidHJ1ZSIsImF1dGhfdGltZSI6MTY0OTgwMDA3NSwibm9uY2Vfc3VwcG9ydGVkIjp0cnVlfQ.zmnDnSxuo9m95ly3tT_ZqO4ZKoRnYBFdgMWGwclbWvSim7pDKbnabvBP8F2pZ_A2O1KbsQyvzSRge6mDAJbFfuyTF2rC1nI2Ghm4oAl1_hZ39UskaM30L0exFrSjltTdpidnlho1SZNFauZy3IOh84IhyKrvXp7QbzmDXZI8_eiXBu7L50S7uEqTUWv3k7V-jUK6etOWg-7uuOadLAyQIuKx-MQ-Pr44IlHRw76abmaZzwWewc0CDGtmZBSlL3VdEdHbetl8FVlDRCUp-kQz0f6K4KKfDOEQNCU0JhIgEemlMX0q-0HW81GBuyPylCvGPMkDeIwP19oSxpiJMHkb1A
    

    JWT格式(以.点号分隔):

    • header: 包括了key id 与加密算法

    • payload:
        1. iss: 签发机构,苹果
        2. aud: 接收者,目标app
        3. exp: 过期时间
        4. iat: 签发时间
        5. sub: 用户id
        6. c_hash: 一个哈希数列
        7. auth_time: 签名时间

    • signature: 用于验证JWT的签名

    header

    {
      "kid": "YuyXoY",
      "alg": "RS256"
    }
    

    payload

    {
      "iss": "https://appleid.apple.com",
      "aud": "com.hzdcm.yyylc",
      "exp": 1649886475,
      "iat": 1649800075,
      "sub": "001194.3f112178ada24456b9ea4c00a16efb2a.2147",
      "c_hash": "zD4LzQzV1N67Nge_vlN8SA",
      "email": "mqh6qtkk8y@privaterelay.appleid.com",
      "email_verified": "true",
      "is_private_email": "true",
      "auth_time": 1649800075,
      "nonce_supported": true
    }
    

    token验证原理

    因为identityToken使用非对称加密 RSASSA【RSA签名算法】 和 ECDSA【椭圆曲线数据签名算法】,当验证签名的时候,先从https://appleid.apple.com/auth/keys获取公钥,再利用公钥来解密Singature,当解密内容与base64UrlEncode(header) + "." + base64UrlEncode(payload)的内容完全一样的时候,表示验证通过。

    Java服务器验证代码

    package com.game.data.platform;
    
    import com.arch.common.JsonUtil;
    import com.arch.common.StringUtil;
    import com.arch.log.Log;
    import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    import com.fasterxml.jackson.core.type.TypeReference;
    import com.fasterxml.jackson.databind.JsonNode;
    import io.jsonwebtoken.*;
    import org.apache.commons.codec.binary.Base64;
    import org.apache.http.client.methods.CloseableHttpResponse;
    import org.apache.http.client.methods.HttpGet;
    import org.apache.http.impl.client.CloseableHttpClient;
    import org.apache.http.impl.client.HttpClients;
    
    import java.math.BigInteger;
    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.HashMap;
    import java.util.List;
    import java.util.Map;
    
    /**
     * 苹果账号登录验证
     */
    public class PlatformApple {
        @JsonIgnoreProperties(ignoreUnknown = true)
        public static class Keys{
            public String kid;
            public String use;
            public String alg;
            public String n;
            public String e;
        }
    
        // 缓存公钥
        public static Map<String,Keys> keysMap = new HashMap<>();
    
        // 验证Token
        public static boolean checkToken(String identityToken) {
            if (StringUtil.isNullOrEmpty(identityToken)) {
                return false;
            }
    
            try {
                String []jwt = identityToken.split("\\.");
                if (jwt.length != 3){
                    return false;
                }
    
                String header  = new String(Base64.decodeBase64(jwt[0]),"utf-8");
                String token  = new String(Base64.decodeBase64(jwt[1]),"utf-8");
    
                JsonNode jsonNode = JsonUtil.readTree(header );
                Keys keys = getKeys(jsonNode.get("kid").asText(""), jsonNode.get("alg").asText(""));
    
                JsonNode wNode = JsonUtil.readTree(token );
                String audience = wNode.get("aud").asText("");
                String subject = wNode.get("sub").asText("");
                PublicKey publicKeySpec = build(keys.n, keys.e);
                int result = verify(publicKeySpec, token,audience, subject);
                if (result == 0){
                    return true;
                }
            }catch (Exception e){
            }
    
            return false;
        }
    
        // 获取公钥
        public static Keys getKeys(String kid, String alg){
            String key = kid+"."+alg;
            Keys keys = keysMap.get(key);
            if (keys != null){
                return keys;
            }
    
            final String url = "https://appleid.apple.com/auth/keys";
            try{
                CloseableHttpClient httpclient = HttpClients.createDefault();
                HttpGet httpget = new HttpGet(url);
                try (CloseableHttpResponse response =  httpclient.execute(httpget)) {
                    if (response.getStatusLine().getStatusCode() == 200) {
                        JsonNode result = JsonUtil.readTree(response.getEntity().getContent());
                        JsonNode ks = result.get("keys");
                        if (ks != null){
                            String kstr = ks.toString();
                            List<Keys> list = JsonUtil.deserialize(new TypeReference<List<Keys>>() {}, kstr);
                            for (Keys k : list){
                                keysMap.put(k.kid+"."+k.alg, k);
                            }
    
                            return keysMap.get(key);
                        }
                    }
                }
            }catch (Exception e) {
                Log.error(e);
            }
    
            return null;
        }
    
        // 构建key
        public static PublicKey  build(String n, String e) throws NoSuchAlgorithmException, InvalidKeySpecException {
            BigInteger modulus = new BigInteger(1, Base64.decodeBase64(n));
            BigInteger publicExponent = new BigInteger(1, Base64.decodeBase64(e));
            RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, publicExponent);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PublicKey publicKey = keyFactory.generatePublic(keySpec);
            return publicKey;
        }
    
        // 验证
        public static int verify(PublicKey key, String jwt, String audience, String subject) {
            JwtParser jwtParser = Jwts.parser().setSigningKey(key);
            jwtParser.requireIssuer("https://appleid.apple.com");
            jwtParser.requireAudience(audience);
            jwtParser.requireSubject(subject);
            try {
                Jws<Claims> claim = jwtParser.parseClaimsJws(jwt);
                if (claim != null && claim.getBody().containsKey("auth_time")) {
                    return 0;
                }
                return 1;
            } catch (ExpiredJwtException e) {
                Log.error("apple identityToken expired", e);
                return 2;
            } catch (Exception e) {
                Log.error("apple identityToken illegal", e);
                return 3;
            }
        }
    }
    

    然后,直接调用PlatformApple.checkToken(identityToken) 来验证

    相关文章

      网友评论

          本文标题:Sign in with Apple

          本文链接:https://www.haomeiwen.com/subject/tfrqertx.html