阅读 1314

iOS13 - Sign in with Apple 记录

由于公司项目 App 里面支持微信登陆,所以最近了解了下 iOS13 新增的 Sign in with Apple ,本文主要讲述自己的学习记录。

  • Apple审核介绍
    Apple 截图

以上为 Apple 在 2019年 9 月 12 日在官网上发布的最新消息。原文链接
翻译如下:( 译文来自 google 翻译)

  • 为方便用户使用已有的Apple ID登录您的应用和网站。 借助内置的隐私和安全功能,Sign in with Apple是帮助用户快速轻松地设置帐户,登录并与您的应用互动的好方法。 所有帐户均受到两因素身份验证的保护,以提供更高的安全性,Apple不会跟踪用户在您的应用程序或网站中的活动。
  • 我们更新了《 App Store审查指南 》,为何时需要应用程序使用Sign in with Apple提供了标准。 从今天开始,提交到App Store的新应用必须遵循这些准则。 现有应用程序和应用程序更新必须在2020年4月之前进行。我们还提供了有关在网络和其他平台上Sign in with Apple功能的新指南。

审核指南

以上截图为 Apple 审核指南中对于 Sign in with Apple 的介绍。 原文链接
翻译如下:( 译文来自 google 翻译)
4.8 使用Apple登录

仅使用第三方或社交登录服务(例如Facebook登录,Google登录,Twitter登录,LinkedIn登录,Amazon登录或微信登录)的应用来设置或验证用户的主帐户该应用程序还必须提供与Sign in with Apple作为等效选项。用户的主要帐户是他们在您的应用中建立的帐户,用于识别自己的身份,登录并访问您的功能和相关服务。

如果满足以下条件,则无需使用Apple登录:

  • 您的应用专门使用公司自己的帐户设置和登录系统。
  • 您的应用是教育,企业或商业应用,要求用户使用现有的教育或企业帐户登录。
  • 您的应用程序使用政府或行业支持的公民身份识别系统或电子ID来对用户进行身份验证。
  • 您的应用是特定第三方服务的客户端,要求用户直接登录其邮件,社交媒体或其他第三方帐户才能访问其内容。
  • 通过审核指南我们可以明确的知道:
  • Sign in with Apple截止时间为2020年4月之前
  • 仅使用三方登陆、且以第三方账户为主账户的App必须把Sign in with Apple作为选项之一,否则不能通过审核

也就是说,我们必须在 2020年4月之前 完成对 App 登陆的改造:即添加Sign in with Apple功能。下面讲一下集成流程

准备工作

  • 为 自己 App ID添加 Sign in with Apple

    登录开发者中心,进入Certificates, Identifiers & Profiles,点击自己项目App ID,添加Sign in with Apple,保存

  • 创建 Services IDs

点击左侧进入Identifiers,创建服务ID

选择 Services IDs,然后Continue

填写描述和服务 ID,服务 ID 填写规则如下面提示(我的 Demo BundleIDcom.hellohchen.signdemo, 所以直接加了一个.sign,可以按照自己的项目和提示一样写,如示例com.domainname.appname),然后 点击Configure

选择要实现Sign in with AppleAPP ID(只有 ID 添加了 Sign in with Apple才会在这里显示),然后 Save

点击Services IDs页面右上角Continue进入确认页,点击Register

  • 创建基于APP id的密钥,以实现使用Apple登录

进入证书主页面,点击左侧Keys,创建Key

填写 key Name,选中Sign in with Apple,然后Configure

选择要实现Sign in with AppleAPP ID,保存Save,然后Continue进入确认页,点击Register

按要求点击 Done
然后我们点击刚刚创建好的Key ,点击 Download

注意!!

  • 该文件只能下载一次,注意好保存措施 !

Xcode 项目配置

项目添加 Sign in with Apple

代码流程 (Swift为例)

  • 导入框架
import AuthenticationServices
复制代码
  • 添加Sign in with Apple登录按钮及按钮点击事件
可以使用系统的登录按钮:
let authorizationButton = ASAuthorizationAppleIDButton()
authorizationButton.addTarget(self, action: #selector(), for: .touchUpInside)

也可以自定义按钮:
let button = UIButton()
button.addTarget(self, action: #selector(), for: .touchUpInside)
复制代码
  • 实现代理
ASAuthorizationControllerDelegate、ASAuthorizationControllerPresentationContextProviding
复制代码

关于系统登录按钮

  1. 默认设置了圆角
  2. 当宽度不够显示 "Sign in with Apple" 时只有一个logo
  3. 可以自定义按钮替代(确认可以自定义,但不知道是否要加 "Sign in with Apple" 标签,还没试过等大佬结果)
  • 代码如下(为 LoginViewController 添加 Extension)
import UIKit
import AuthenticationServices

class LoginViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        if #available(iOS 13.0, *) {
            let sign1 = signSystemButton_Black()
            sign1.frame = CGRect(x: 100, y: 300, width: 120, height: 40)
            let sign2 = signSystemButton_White()
            sign2.frame = CGRect(x: 100, y: 400, width: 100, height: 40)
            let sign3 = signSystemButton_WhiteOutline()
            sign3.frame = CGRect(x: 100, y: 500, width: 120, height: 40)
            let sign4 = signCustomButton()
            sign4.frame = CGRect(x: 100, y: 600, width: 40, height: 40)
            
            view.addSubview(sign1)
            view.addSubview(sign2)
            view.addSubview(sign3)
            view.addSubview(sign4)
        }
        
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        if #available(iOS 13.0, *) {
            performExistingAccountSetupFlows()
        }
    }


}

@available(iOS 13.0, *)
extension LoginViewController: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
    
    /// 系统白色
    private func signSystemButton_White() -> UIView {
        let authorizationButton = ASAuthorizationAppleIDButton(type: .signIn, style: .white)
        authorizationButton.addTarget(self, action: #selector(handleAuthorizationAppleIDButtonPress), for: .touchUpInside)
        return authorizationButton
    }
    /// 系统黑色
    private func signSystemButton_Black() -> UIView {
        let authorizationButton = ASAuthorizationAppleIDButton(type: .signIn, style: .black)
        authorizationButton.addTarget(self, action: #selector(handleAuthorizationAppleIDButtonPress), for: .touchUpInside)
        return authorizationButton
    }
    /// 系统白色带边框
    private func signSystemButton_WhiteOutline() -> UIView {
        let authorizationButton = ASAuthorizationAppleIDButton(type: .signIn, style: .whiteOutline)
        authorizationButton.addTarget(self, action: #selector(handleAuthorizationAppleIDButtonPress), for: .touchUpInside)
        return authorizationButton
    }
    /// 自定义
    private func signCustomButton() -> UIView {
        let button = UIButton(type: .custom)
        button.setImage(UIImage(named: "login-apple"), for: .normal)
        button.addTarget(self, action: #selector(handleAuthorizationAppleIDButtonPress), for: .touchUpInside)
        return button
    }
    
    /// 登陆授权
    @objc private func handleAuthorizationAppleIDButtonPress() {
        let appleIDProvider = ASAuthorizationAppleIDProvider()
        let request = appleIDProvider.createRequest()
        request.requestedScopes = [.fullName, .email]
        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        authorizationController.delegate = self
        authorizationController.presentationContextProvider = self
        authorizationController.performRequests()
    }
    
    /// 如果找到现有的iCloud钥匙串凭证或Apple ID凭证,则提示用户。
    private func performExistingAccountSetupFlows() {
        // Prepare requests for both Apple ID and password providers.
        let requests = [ASAuthorizationAppleIDProvider().createRequest(),
                        ASAuthorizationPasswordProvider().createRequest()]
        
        // Create an authorization controller with the given requests.
        let authorizationController = ASAuthorizationController(authorizationRequests: requests)
        authorizationController.delegate = self
        authorizationController.presentationContextProvider = self
        authorizationController.performRequests()
    }
    
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        return view.window!
    }
    
    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        // Handle error.
    }
    
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
            
            let user = appleIDCredential.user
            let fullName = appleIDCredential.fullName
            let email = appleIDCredential.email
            let identityToken = appleIDCredential.identityToken
            let authorizationCode = appleIDCredential.authorizationCode
            
            // TODO: Create an account in your system.
            
            print("user:\(user)")
            print("fullName:\(String(describing: fullName))")
            print("email:\(String(describing: email))")
            print("identityToken:\(String(describing: identityToken))")
            print("authorizationCode:\(String(describing: authorizationCode))")
            
        } else if let passwordCredential = authorization.credential as? ASPasswordCredential {
            
            let username = passwordCredential.user
            let password = passwordCredential.password
            
            print("username:\(username)")
            print("password:\(password)")
            
        }
    }
}

复制代码

注意:

let user = appleIDCredential.user // 授权的用户唯一标识
let fullName = appleIDCredential.fullName // 授权的用户名称
let email = appleIDCredential.email // 授权的用户邮箱
let identityToken = appleIDCredential.identityToken // 授权用户的JWT凭证
let authorizationCode = appleIDCredential.authorizationCode // 授权码
复制代码

fullNameemail只有在第一次授权时有值,二次以及之后都是没有值的状态

第一次授权
第二次授权 到这里基本上 App 已经配置完成,下面介绍服务端工作

服务端验证

针对后端验证苹果提供了两种验证方式,一种是基于JWT的算法验证,另外一种是基于授权码的验证

  1. 基于JWT的算法验证
    • 使用到的Apple公钥接口:https://appleid.apple.com/auth/keys 链接
    • 详细接口文档说明参见:https://developer.apple.com/documentation/signinwithapplerestapi/fetch_apple_s_public_key_for_verifying_token_signature 链接
    • 接口返回值:
    {
        "keys": [
            {
                "kty": "RSA", // 按键类型参数设置。 必须将其设置为“ RSA”, String
                "kid": "AIDOPK1", // 从您的开发者帐户获取的10个字符的标识符密钥, String
                "use": "sig", // 公钥的预期用途, Sstring
                "alg": "RS256", // 用于加密令牌的加密算法, String
                "n": "lxrwmuYSAsTfn-lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4Fu_auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY-RNwCWdjNfEaY_esUPY3OVMrNDI15Ns13xspWS3q-13kdGv9jHI28P87RvMpjz_JCpQ5IM44oSyRnYtVJO-320SB8E2Bw92pmrenbp67KRUzTEVfGU4-obP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysyd_JhmqX5CAaT9Pgi0J8lU_pcl215oANqjy7Ob-VMhug9eGyxAWVfu_1u6QJKePlE-w", // RSA公钥的模数值, String
                "e": "AQAB" // RSA公钥的指数值, String
            }  
        ]
    }
    复制代码
    kid,为密钥id标识,签名算法采用的是RS256(RSA 256 + SHA 256),kty常量标识使用RSA签名算法,其公钥参数为n和e,其值采用了BASE64编码,使用时需要先解码

下面针对identityToken后端验证做简要说明:

  • identityToken参考样例:
// jwt 格式 
eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnNreW1pbmcuZGV2aWNlbW9uaXRvciIsImV4cCI6MTU2NTY2ODA4NiwiaWF0IjoxNTY1NjY3NDg2LCJzdWIiOiIwMDEyNDcuOTNiM2E3OTlhN2M4NGMwY2I0NmNkMDhmMTAwNzk3ZjIuMDcwNCIsImNfaGFzaCI6Ik9oMmFtOWVNTldWWTNkcTVKbUNsYmciLCJhdXRoX3RpbWUiOjE1NjU2Njc0ODZ9.e-pdwK4iKWErr_Gcpkzo8JNi_MWh7OMnA15FvyOXQxTx0GsXzFT3qE3DmXqAar96nx3EqsHI1Qgquqt2ogyj-lLijK_46ifckdqPjncTEGzVWkNTX8uhY7M867B6aUnmR7u-cf2HsmhXrvgsJLGp2TzCI3oTp-kskBOeCPMyTxzNURuYe8zabBlUy6FDNIPeZwZXZqU0Fr3riv2k1NkGx5MqFdUq3z5mNfmWbIAuU64Z3yKhaqwGd2tey1Xxs4hHa786OeYFF3n7G5h-4kQ4lf163G6I5BU0etCRSYVKqjq-OL-8z8dHNqvTJtAYanB3OHNWCHevJFHJ2nWOTT3sbw
 
// header 解码
{"kid":"AIDOPK1","alg":"RS256"} 其中kid对应上文说的密钥id
 
// claims 解码
{
    "iss":"https://appleid.apple.com",
    "aud":"com.skyming.devicemonitor",
    "exp":1565668086,"iat":1565667486,
    "sub":"001247.93b3a799a7c84c0cb46cd08f100797f2.0704",
    "c_hash":"Oh2am9eMNWVY3dq5JmClbg",
    "auth_time":1565667486
}
 
其中 iss标识是苹果签发的,aud是接收者的APP ID,该token的有效期是10分钟,sub就是用户的唯一标识
复制代码

如何验证?

首先通过identityToken中的header中的kid,然后结合苹果获取公钥的接口,拿到相应的n和e的值,然后通过下面这个方法构建RSA公钥

public RSAPublicKeySpec build(String n, String e) {  
   BigInteger modulus = new BigInteger(1, Base64.decodeBase64(n));
   BigInteger publicExponent = new BigInteger(1, Base64.decodeBase64(e));
   return new RSAPublicKeySpec(modulus, publicExponent);    
}

通过下面这个方法验证JWT的有效性
public 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 GlobalCode.SUCCESS;            
      }           
      return GlobalCode.THIRD_AUTH_CODE_INVALID;
   } catch (ExpiredJwtException e) { 
      log.error("apple identityToken expired", e);
      return GlobalCode.THIRD_AUTH_CODE_INVALID;
   } catch (Exception e) {
      log.error("apple identityToken illegal", e);
      return GlobalCode.FAIL_ILLEGAL_REQ;
   }
}

使用的JWT工具库为:
<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.1</version>
</dependency>
复制代码
  1. 基于授权码的后端验证
  • 首先需要了解如何构建client_secret,详细文档可以参考如下两个:
  • https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple 链接
  • https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens链接

首先说下client_secret的构建方法:

先在后台生成授权应用APP ID的密钥KEY文件,然后下载密钥文件格式样例:
 
-----BEGIN PRIVATE KEY-----
   BASE64编码后的密钥
-----END PRIVATE KEY-----
 
public  byte[] readKey() throws Exception {
    String temp = "密钥文件中间的编码字符串";
    return Base64.decodeBase64(temp);
}
 
构建client_secret关键代码:
 
String client_id = "..."; // 被授权的APP ID
Map<String, Object> header = new HashMap<String, Object>();
header.put("kid", "密钥id"); // 参考后台配置
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("iss", "team id"); // 参考后台配置 team id
long now = System.currentTimeMillis() / 1000;
claims.put("iat", now);
claims.put("exp", now + 86400 * 30); // 最长半年,单位秒
claims.put("aud", "https://appleid.apple.com"); // 默认值
claims.put("sub", client_id);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(readKey());
KeyFactory keyFactory = KeyFactory.getInstance("EC");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
String client_secret = Jwts.builder().setHeader(header).setClaims(claims).signWith(SignatureAlgorithm.ES256, privateKey).compact(); 
复制代码

如何验证?

String url = "https://appleid.apple.com/auth/token";
// POST 请求
HttpSynClient client = new HttpSynClient(5000, 5000, 5000, 20);
Map<String, String> form = new HashMap<String, String>();
form.put("client_id", client_id);
form.put("client_secret", client_secret);
form.put("code", code);form.put("grant_type","authorization_code");
form.put("redirect_uri", redirectUrl);
HttpResponse result = client.excutePost(url, form);
System.out.println(result);
复制代码

返回值样例:

{
    "access_token":"a0996b16cfb674c0eb0d29194c880455b.0.nsww.5fi5MVC-i3AVNhddrNg7Qw",
    "token_type":"Bearer",
    "expires_in":3600,
    "refresh_token":"r9ee922f1c8b048208037f78cd7dfc91a.0.nsww.KlV2TeFlTr7YDdZ0KtvEQQ",
    "id_token":"eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnNreW1pbmcuYXBwbGVsb2dpbmRlbW8iLCJleHAiOjE1NjU2NjU1OTQsImlhdCI6MTU2NTY2NDk5NCwic3ViIjoiMDAwMjY2LmRiZTg2NWIwYWE3MjRlMWM4ODM5MDIwOWI5YzdkNjk1LjAyNTYiLCJhdF9oYXNoIjoiR0ZmODhlX1ptc0pqQ2VkZzJXem85ZyIsImF1dGhfdGltZSI6MTU2NTY2NDk2M30.J6XFWmbr0a1hkJszAKM2wevJF57yZt-MoyZNI9QF76dHfJvAmFO9_RP9-tz4pN4ua3BuSJpUbwzT2xFD_rBjsNWkU-ZhuSAONdAnCtK2Vbc2AYEH9n7lB2PnOE1mX5HwY-dI9dqS9AdU4S_CjzTGnvFqC9H5pt6LVoCF4N9dFfQnh2w7jQrjTic_JvbgJT5m7vLzRx-eRnlxQIifEsHDbudzi3yg7XC9OL9QBiTyHdCQvRdsyRLrewJT6QZmi6kEWrV9E21WPC6qJMsaIfGik44UgPOnNnjdxKPzxUAa-Lo1HAzvHcAX5i047T01ltqvHbtsJEZxAB6okmwco78JQA"
}
复制代码

其中id_token是一个JWT,其中claims中的sub就是授权的用户唯一标识,该token也可以使用上述的验证方法进行有效性验证,另外授权code是有时效性的,且使用一次即失效

扩展资料

JWT:www.cnblogs.com/softidea/p/…

ECDSA 椭圆曲线签名,JDK 1.7 的第四个版本提供了对ECDSA的支持:blog.csdn.net/qq_35612816…

  1. 本文参考其他文章并加以完善【原文链接
  2. 因服务端代码能力欠缺,服务端实现记录自 CSDN文章【原文链接
  3. Demo:Apple官方示例代码
  4. 本文Demo
关注下面的标签,发现更多相似文章
评论