Sign in With Apple 从登陆到服务器验证
关于手机端登录的代码,这里不做多余介绍,我们看下登录成功后, Apple
返回给手机端的一些参数
open var user: String { get }
open var state: String? { get }
open var authorizedScopes: [ASAuthorization.Scope] { get }
open var authorizationCode: Data? { get }
open var identityToken: Data? { get }
open var email: String? { get }
open var fullName: PersonNameComponents? { get }
open var realUserStatus: ASUserDetectionStatus
这里初步登录成功,从 Apple
拿到 user
、 identityToken
、 authorizationCode
等相关信息。
1. 首先手机端先做一个验证
identityToken
是一个 JWT
格式的加密数据 (JWT相关知识介绍 JSON Web Token
),我们通过如下代码拿到 header
和 payload
中的数据
guard let _ = identityToken else { return }
let token = String(data: identityToken!, encoding: .utf8)
let arr = token?.components(separatedBy: ".")
let header = arr?[0]
let payload = arr?[1]
header
及 payload
均是base64编码的字符串,通过如下代码进行解码
注意:swift中需要用NSData进行转换,用Data进行转换会失败
// 这里为了方便,均采用 ! 强行解包
let headerData = NSData(base64EncodedString: payload!)
let decodeHeader = String(data: (headerData! as Data), encoding: .utf8)
let payloadData = NSData(base64EncodedString: payload!)
let decodePayload = String(data: (payloadData! as Data), encoding: .utf8)
得到结果如下
header:
{"kid":"AIDOPK1","alg":"RS256"}
payload:
{"iss":"https://appleid.apple.com","aud":"这个是你的app的bundle identifier","exp":1567482337,"iat":1567481737,"sub":"这个字段和上面获取的 user 字段是完全一样的","c_hash":"8KDzfalU5kygg5zxXiX7dA","auth_time":1567481737}
通过如上操作,我们可以获取到 payload
中的 sub
字段,然后与上面的 user
字段做一个对比,如果对比通过,则第一步完成
把 base64 data 转化成字符串之后,也可以去 JWT官网 把字符串粘贴进去,获取 header 及 payload 信息
python
decode base64 字符串示例代码如下:
import base64
token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
arr = token.split('.')
header = arr[0]
data = header.encode()
missing_padding = len(data) % 4
if missing_padding != 0:
data += b'='* (4 - missing_padding)
bStr = base64.decodebytes(data)
decode_header = bStr.decode()
print(decode_header) # 结果 {"alg": "HS256","typ": "JWT"}
2. 服务端像苹果请求验证
手机端需要提交 user
、authorizationCode
、 identityToken
字段信息(code和token字段苹果返回的是 base64 Data
形式,手机端可以先转换 base64 字符串之后在给服务器)到服务器。然后服务器通过 https://appleid.apple.com/auth/token
该接口,并拼接对应参数去验证,接口相关信息苹果有提供 Generate and validate tokens
。
下面着重介绍几个参数及其获取方法。首先先看下该接口需要的参数,如下
client_id: string (Required) (Authorization and Validation) The application identifier for your app
client_secret: string (Required) (Authorization and Validation) A secret generated as a JSON Web Token that uses the secret key generated by the WWDR portal.
code: string (Authorization) The authorization code received from your application’s user agent. The code is single use only and valid for five minutes.
grant_type: string (Required) (Authorization and Validation) The grant type that determines how the client interacts with the server. For authorization code validation, use authorization_code. For refresh token validation requests, use refresh_token.
refresh_token: string (Validation) The refresh token received during the authorization request.
redirect_uri: string (Authorization) The destination URI the code was originally sent to.
其中 client_id
为app的 bundle identifier
, code
即为手机端获取到的 authorizationCode
信息, grant_type
传入固定字符串 authorization_code
即可。还剩下一个必要参数 client_secret
那么这个参数相对麻烦点,需要我们自己生成。client_secret
参数是一个JWT,singature
部分使用非对称加密 RSASSA【RSA签名算法】 和 ECDSA【椭圆曲线数据签名算法】。
生成 client_secret
之前,我们需要做如下工作
- 获取 APP 的 bundleID
- 获取开发者账号的TeamID
- 创建 privateKey,获取到 Key ID 和 私钥
创建完之后把私钥下载下来,并保存好,注意,私钥只能下载一次。
拿到上面所有信息之后,可以通过如下代码生成 client_secret
,代码为 Ruby
代码,确保已安装ruby环境。
require "jwt"
key_file = "Path to the private key"
team_id = "Your Team ID"
client_id = "Your App Bundle ID"
key_id = "The Key ID of the private key"
validity_period = 180 # In days. Max 180 (6 months) according to Apple docs.
private_key = OpenSSL::PKey::EC.new IO.read key_file
token = JWT.encode(
{
iss: team_id,
iat: Time.now.to_i,
exp: Time.now.to_i + 86400 * validity_period,
aud: "https://appleid.apple.com",
sub: client_id
},
private_key,
"ES256",
header_fields=
{
kid: key_id
}
)
puts token
创建文件 secret_gen.rb
,把上面代码粘贴进去,执行 ruby secret_gen.rb
即可生成 client_secret
代码中这个 key_file
需要指定刚才下载的文件的地址
到这里, https://appleid.apple.com/auth/token
的三个必需参数已经全部获得,进行请求,获得请求结果如下
{
"access_token": "一个token,此处省略",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "一个token,此处省略",
"id_token": "结果是JWT,字符串形式,此处省略"
}
参数解释看这个文档
服务器拿到相应结果,其中 id_token
也是 JWT
数据,decode 出 payload
部分如下
其中
aud
部分与你的app的bundleID一致, sub
部分即与手机端获得的 user
一致,服务器端通过对比 sub
字段信息是否与手机端上传的 user
信息一致来确定是否成功登录。
关于Apple文档中的 Public Key
Apple
在服务器验证时候给的文档
有两个接口,一个是获取 Public Key
的,另一个便是验证token的。 Public Key
是一个固定的接口 https://appleid.apple.com/auth/keys
,直接调用便会获取到一个 JWK
,信息如下
{
"keys": [
{
"kty": "RSA",
"kid": "AIDOPK1",
"use": "sig",
"alg": "RS256",
"n": "lxrwmuYSAsTfn-lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4Fu_auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY-RNwCWdjNfEaY_esUPY3OVMrNDI15Ns13xspWS3q-13kdGv9jHI28P87RvMpjz_JCpQ5IM44oSyRnYtVJO-320SB8E2Bw92pmrenbp67KRUzTEVfGU4-obP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysyd_JhmqX5CAaT9Pgi0J8lU_pcl215oANqjy7Ob-VMhug9eGyxAWVfu_1u6QJKePlE-w",
"e": "AQAB"
}
]
}
我们可以通过这个 Public Key
去对手机端获取到的 identityToken
(JWT 信息)进行解码,以获取 header
及 payload
我这里是通过 python 进行验证的。
- 首先我们需要将
Public Key
转为pem
(可通过这个网址 进行转换) 转换结果如下,我们创建一个文件名为publicKey.pem
并把下面内容添加进去保存。
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlxrwmuYSAsTfn+lUu4go
ZSXBD9ackM9OJuwUVQHmbZo6GW4Fu/auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD
4eRtY+RNwCWdjNfEaY/esUPY3OVMrNDI15Ns13xspWS3q+13kdGv9jHI28P87RvM
pjz/JCpQ5IM44oSyRnYtVJO+320SB8E2Bw92pmrenbp67KRUzTEVfGU4+obP5RZ0
9OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysy
d/JhmqX5CAaT9Pgi0J8lU/pcl215oANqjy7Ob+VMhug9eGyxAWVfu/1u6QJKePlE
+wIDAQAB
-----END PUBLIC KEY-----
- 安装库
pip install authlib
(其他各种语言对应的库都可从JWT官网 查找获取)
脚本文件内容如下
from authlib.jose import jwt
identityToken = '这个是手机端获取到的identityToken'
# 这个publicKey.pem即刚才创建的文件,放在 py 文件同目录下即可
dec = jwt.decode(identityToken, open("publicKey.pem").read())
print(dec)
这里打印出decode信息如下
{
"iss": "https://appleid.apple.com",
"aud": "这个对应app的bundleid",
"exp": 1567494694,
"iat": 1567494094,
"sub": "这个字段和手机端获取的user信息相同",
"c_hash": "nRYP2wGXBGT0bIYWibx4Yg",
"auth_time": 1567494094
}
到这里我们可以看到,通过 Public Key
饶了一大圈decode出来的payload信息,和我们直接将 identityToken
转换为base64字符串,在将base64字符串解码成普通字符串得到的信息是一样的。同样需要比对 sub
字段是否与 user
字段内容相同。
绕一大圈去decode 相对于 直接转换,麻烦太多,但也可以从第一步去防止伪造信息登录,因为 identityToken
第三部分不正确的话,通过 Public Key
是解析不成功的。所以上面提到的第一步验证也可以放到服务器端去做。
原文: https://www.yuque.com/zhanglong/bb0s5d/cxbh7n
参考链接
So They’ve Signed in with Apple, Now What?
Adding Sign In with Apple to your app in under 5mins with ZERO code