背景
要在App Store 新上架的App要在4月份前适配iOS 13,其中App如果使用了第三方登录的,就要同时支持AppleId 登录,也是一种第三方登录.
先放一个时序图:
第一步,客户端和AppleServer交互后请求后端
客户端向苹果服务器请求,拿到用户的信息和identityToken.主要返回数据如下:
- user: 用户唯一ID,在一个开发者账号下的APP获取到的是一样的,类似微信开发API中的openid;
- identityToken: 「JWT」格式的token,用于验证信息合法性。
- email: 用户邮箱(可能为空)
- fullName: 昵称等信息
- realUserStatus: 是否是“真实用户”,可用于反作弊,对抗黑灰产. (0为黑户,1为不确定,2为正常用户)
拿到信息后调用接口,把信息传给后端,但这样有个重要的问题就是不能保证安全性,无法判断请求是否是伪造的。这个时候就要使用identityToken了。
注意:当第一次认证成功之后,将不会再返回email,fullName等信息,可以在设置->Apple ID->密码与安全性->使用您AppleID的App 中删除对应的APP。
第二步,后端校验identityToken合法性
identityTokenString实际上是JWT(JSON Web Token)格式的文件,JWT文件由三部分组成:
这三部分由”.”分割,其中Header和Payload是经过base64编码的。
Header base64解码之后示例:
1 2 3 4
| { "kid":"eXaunmL", "alg":"RS256" }
|
Payload base64解码之后示例:
1 2 3 4 5 6 7 8 9 10
| { "iss":"https://appleid.apple.com", "aud":"com.easeapi.www", "exp":1584588402, "iat":1584587802, "sub":"022409.17avbbaf112941e5a722788e7f3880f4.4565", "c_hash":"DmuPZ_bX1Tr6AGFW3rDYbQ", "auth_time":1584587802, "nonce_supported":true }
|
而Signature部分就是对Header及Payload两部分内容按指定算法进行签名,大致逻辑如下:
1 2 3
| Signature = signature(base64UrlEncode(Header) + "." + base64UrlEncode(Payload), secretKey) #signature代表具体的加密算法; #secretKey为密钥;
|
具体到identityToken,Apple目前采用的是RS256的非对称加密算法:
Apple会使用私钥(也即为上面的secretKey)对Header及Payload加密,获取Signature;
将Header,Payload及Signature信息包装为JWT格式文件,即是identityToken;
那么,我们如何才能验证拿到的identityToken是否合法呢,这就要用到Apple提供的公钥了。公钥获取地址:https://appleid.apple.com/auth/keys
直接用GET请求就会得等一个数组Keys,也就是 JWK 列表。这也就意味着客户端向服务器提交的 identityToken 可能是用 keys 里面的特定某个 JWK 来进行加密的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| { "keys":[ { "kty":"RSA", "kid":"86D88Kf", "use":"sig", "alg":"RS256", "n":"iGaLqP6y-SJCCBq5Hv6pGDbG_SQ11MNjH7rWHcCFYz4hGwHC4lcSurTlV8u3avoVNM8jXevG1Iu1SY11qInqUvjJur--hghr1b56OPJu6H1iKulSxGjEIyDP6c5BdE1uwprYyr4IO9th8fOwCPygjLFrh44XEGbDIFeImwvBAGOhmMB2AD1n1KviyNsH0bEB7phQtiLk-ILjv1bORSRl8AK677-1T8isGfHKXGZ_ZGtStDe7Lu0Ihp8zoUt59kx2o9uWpROkzF56ypresiIl4WprClRCjz8x6cPZXU2qNWhu71TQvUFwvIvbkE1oYaJMb0jcOTmBRZA2QuYw-zHLwQ", "e":"AQAB" }, { "kty":"RSA", "kid":"eXaunmL", "use":"sig", "alg":"RS256", "n":"4dGQ7bQK8LgILOdLsYzfZjkEAoQeVC_aqyc8GC6RX7dq_KvRAQAWPvkam8VQv4GK5T4ogklEKEvj5ISBamdDNq1n52TpxQwI2EqxSk7I9fKPKhRt4F8-2yETlYvye-2s6NeWJim0KBtOVrk0gWvEDgd6WOqJl_yt5WBISvILNyVg1qAAM8JeX6dRPosahRVDjA52G2X-Tip84wqwyRpUlq2ybzcLh3zyhCitBOebiRWDQfG26EH9lTlJhll-p_Dg8vAXxJLIJ4SNLcqgFeZe4OfHLgdzMvxXZJnPp_VgmkcpUdRotazKZumj6dBPcXI_XID4Z4Z3OM1KrZPJNdUhxw", "e":"AQAB" } ] }
|
接下来就需要我们确定当前的 identityToken 到底是使用哪个 JWK 来加密的,这样做可以避免批量生成证书,提升性能。找到keys数组中的kid和我们刚刚从Header中解析出来的kid相同的那个对象.那个就我们要用的publicKey.但是现在的publicKey格式不是我们要的PEM文件格式.所以要把数据再转一下格式.
1 2 3 4 5 6 7
| // pem公钥 -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8kGa1pSjbSYZVebtTRBLxBz5H 4i2p/llLCrEeQhta5kaQu/RnvuER4W8oDH3+3iuIYW4VQAzyqFpwuzjkDI+17t5t 0tyazyZ8JXw+KgXTxldMPEL95+qVhgXvwtihXC1c5oGbRlEDvDF6Sa53rcFVsYJ4 ehde/zUxo6UvS7UrBQIDAQAB -----END PUBLIC KEY-----
|
我这里用的是jwk-to-pem
包做的.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const jwkToPem = require('jwk-to-pem'); async getPublicKey(kid) { let publicKey; const result = await this.ctx.curl('https://appleid.apple.com/auth/keys', { method: 'GET', dataAsQueryString: true, dataType: 'json', }); for (const key of result.data.keys) { if (key.kid === kid) { publicKey = key; break; } } const pem = jwkToPem(publicKey); return pem; }
|
获取PEM后再用JWT校验identityToken就可以了
1 2 3 4
| let header = identityToken.split('.')[0]; header = JSON.parse(Base64.decode(header)); const publicKey = await this.getPublicKey(header.kid); const data = this.ctx.app.jwt.verify(identityToken, publicKey);
|
第三步,业务逻辑处理
使用user或者校验identityToken得到的sub作为唯一标识注册用户,这里最好还要缓存一下email,fullName字段,以免因为网络等原因在校验成功后没有注册用户,导致丢失数据.
参考文档:
大伟不是戴维 blog
segmentfault