关于手机端登录的代码,这里不做多余介绍,我们看下登录成功后, 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 拿到 useridentityTokenauthorizationCode 等相关信息。

1. 首先手机端先做一个验证

identityToken 是一个 JWT 格式的加密数据 (JWT相关知识介绍 JSON Web Token),我们通过如下代码拿到 headerpayload 中的数据

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]

headerpayload 均是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. 服务端像苹果请求验证

手机端需要提交 userauthorizationCodeidentityToken 字段信息(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 identifiercode 即为手机端获取到的 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 信息)进行解码,以获取 headerpayload

我这里是通过 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

How to configure Sign In with Apple