新博客地址(shankusu.me)

原文转载自

http://cwqqq.com/2020/09/27/apple_login_api_server_side

apple帐号登录服务器端接入

最近有新产品要提交App Store,发现APP审核增加了接入apple帐号登录的要求。所以,借此机会研究下apple帐号登录,做一个分享。

Sign In With Apple

这是苹果推出一套标准接口,用户通过端上的Apple ID登录第三方APP。官方网址 https://developer.apple.com/documentation/sign_in_with_apple

时序图如下:

接入过程分两步:
1、客户端拉起用户登陆,获取授权码及用户信息
2、服务端验证

客户端拉起apple授权页面,等用户授权登录后,可以取到用户的 authorizationCode 、 identityToken 、user 等。

客户端的请求结果如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

#pragma mark - ASAuthorizationControllerDelegate

- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0))

{

    if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]])       {

        ASAuthorizationAppleIDCredential *credential = authorization.credential;

        NSString *state = credential.state;

        NSString *user = credential.user;

        NSPersonNameComponents *fullName = credential.fullName;

        NSString *email = credential.email;

        NSString *authorizationCode = [[NSString alloc] initWithData:credential.authorizationCode encoding:NSUTF8StringEncoding];

        NSString *identityToken = [[NSString alloc] initWithData:credential.identityToken encoding:NSUTF8StringEncoding];

        ASUserDetectionStatus realUserStatus = credential.realUserStatus;

         

        NSLog(@"user: %@", user);

        NSLog(@"fullName: %@", fullName);

        NSLog(@"email: %@", email);

        NSLog(@"authorizationCode: %@", authorizationCode);

        NSLog(@"identityToken: %@", identityToken);

        NSLog(@"realUserStatus: %@", @(realUserStatus));

    }

}

这些信息具体含义和用途:
∙ 用户ID: user,苹果用户唯一标识,该值在同一个开发者账号下的所有 App 下是一样的。
∙ 验证数据: identityToken , authorizationCode ,用于服务端验证授权请求的合法性。
∙ 用户信息: fullName , email,包括全名、邮箱等。
∙ 真实用户标志: realUserStatus ,用于判断当前账号是否是一个真实用户,取值有:unsupported、unknown、likelyReal。

其实,根据 identityToken ,解析后可以得到用户信息。但是,数据从客户端取到,然后转发给服务端,这个过程中,数据存在被修改的可能性。

有两种验证用户的方法:
1、 解析出用户数据,再利用 Apple 公钥验证 identityToken
2、 根据 authorizationCode 从 Apple ID server 重新拿到 identityToken, 再解析出用户数据
当然,方法2拿到的 identityToken 也可以用公钥验证

解析验证 identity Token

从网上找了一个苹果服务器返回的 identityToken ,数据如下:

eyJraWQiOiI4NkQ4OEtmIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmNoYW5nZGFvLnR0c2Nob29sIiwiZXhwIjoxNTg5MTg1Mjg1LCJpYXQiOjE1ODkxODQ2ODUsInN1YiI6IjAwMTk0MC43YTExNDFhYTAwMWM0NjllYTE1NjNjNmJhZTk5YzM3ZC4wMzA3IiwiY19oYXNoIjoiN1gzc2x2dHVBU0kwYmFSbU0wVGFrQSIsImVtYWlsIjoiYXEzMmsydnpjd0Bwcml2YXRlcmVsYXkuYXBwbGVpZC5jb20iLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJpc19wcml2YXRlX2VtYWlsIjoidHJ1ZSIsImF1dGhfdGltZSI6MTU4OTE4NDY4NSwibm9uY2Vfc3VwcG9ydGVkIjp0cnVlfQ.S9wCOt6EeOoRrSMq4kUkPgJPyP1ruMXEcEZeeQEd1CDpcyVWLI8nTOqrl-l0sWYR-5nl2-1iJyiu77fRv8T7dBoV0EHT7GgM1l7qhnWsI9I8V-56rA9ArdJrLIBJbxu7j-xzQhZb6PZ5MSxPZ6WqZay0RpP9JiQ23ybssWQsMnqzvVZkye0iNtBGT1LnfT80XNxmj8L2uJZY08mXjjWWsYY_h0_IRvqOLyaW99w-F8T9KuDkWz2Z-DJX_tiKC0DOT03ypBv82H0v_v-8lFlp4rNRSB82CdgfYwEWElU7zKZfaHJOxT3wOvRXNpbj6_hENPdbtG2ozgdg2oVEiamz0g

这是一个 JWT 数据串,先说下公钥验证的方法。

首先,要取到苹果的公钥,公钥目前是以 JWK 的形式对外发布,需要自己转成pem,获取的URL为 https://appleid.apple.com/auth/keys,数据如下:

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"

    }

  ]

}

实际上,这里是两个JWK数据,我们需要转成对应的证书pem文件。
字段的含义如下:

algThe encryption algorithm used to encrypt the token.
eThe exponent value for the RSA public key.
kidA 10-character identifier key, obtained from your developer account.
ktyThe key type parameter setting. This must be set to “RSA”.
nThe modulus value for the RSA public key.
useThe intended use for the public key.

其中,e、n以及kid使用了大端字节序表示,再通过base64url编码

这里,我封装了JWK转pem的方法(目前只支持 RSA公钥,以后看需要拓展。)
RSA公钥需要 n、e字段,接口如下:

1

2

3

4

local jwt = require "jwt"

local ok, pem = jwt.jwk_to_pem(modulus, exponent)

print(">> jwt.jwk_to_pem(modulus, exponent)")

print(pem)

执行结果如下:

1

2

3

4

5

6

7

8

9

10

>> jwt.jwk_to_pem(modulus, exponent)

-----BEGIN PUBLIC KEY-----

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiGaLqP6y+SJCCBq5Hv6p

GDbG/SQ11MNjH7rWHcCFYz4hGwHC4lcSurTlV8u3avoVNM8jXevG1Iu1SY11qInq

UvjJur++hghr1b56OPJu6H1iKulSxGjEIyDP6c5BdE1uwprYyr4IO9th8fOwCPyg

jLFrh44XEGbDIFeImwvBAGOhmMB2AD1n1KviyNsH0bEB7phQtiLk+ILjv1bORSRl

8AK677+1T8isGfHKXGZ/ZGtStDe7Lu0Ihp8zoUt59kx2o9uWpROkzF56ypresiIl

4WprClRCjz8x6cPZXU2qNWhu71TQvUFwvIvbkE1oYaJMb0jcOTmBRZA2QuYw+zHL

wQIDAQAB

-----END PUBLIC KEY-----

然后,利用公钥 pem 验证 identityToken(利用第三方库libjwt实现),方法如下

1

2

3

4

local jwt = require "jwt"

local ok, token_str = jwt.jwt_decode(jwt_str, pem)

print(">> jwt.jwt_decode(jwt_str, pem)")

print(token_str)

执行结果如下:

1

2

>> jwt.jwt_decode(jwt_str, pem)

{"alg":"RS256","kid":"86D88Kf","typ":"JWT"}.{"aud":"com.xxx.xxx","auth_time":1589184685,"c_hash":"7X3slvtuASI0baRmM0TakA","email":"aq32k2vzcw@privaterelay.appleid.com","email_verified":"true","exp":1589185285,"iat":1589184685,"is_private_email":"true","iss":"https://appleid.apple.com","nonce_supported":true,"sub":"001940.7a1141aa001c469ea1563c6bae99c37d.0307"}

如果不想公钥验证,直接解析 identityToken的方法如下

1

2

3

4

local jwt = require "jwt"

local ok, token_str = jwt.jwt_decode(jwt_str)

print(">> jwt.jwt_decode(jwt_str)")

print(token_str)

执行结果如下:

1

2

>> jwt.jwt_decode(jwt_str)

{"alg":"none","kid":"86D88Kf"}.{"aud":"com.xxx.xxx","auth_time":1589184685,"c_hash":"7X3slvtuASI0baRmM0TakA","email":"aq32k2vzcw@privaterelay.appleid.com","email_verified":"true","exp":1589185285,"iat":1589184685,"is_private_email":"true","iss":"https://appleid.apple.com","nonce_supported":true,"sub":"001940.7a1141aa001c469ea1563c6bae99c37d.0307"}

说下这几个字段:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

{

  "alg":"none",

  "kid":"86D88Kf"

}

{

 "aud":"com.xxx.xxx",

 "auth_time":1589184685,

 "c_hash":"7X3slvtuASI0baRmM0TakA",

 "email":"aq32k2vzcw@privaterelay.appleid.com",

 "email_verified":"true",

 "exp":1589185285,

 "iat":1589184685,

 "is_private_email":"true",

 "iss":"https://appleid.apple.com",

 "nonce_supported":true,

 "sub":"001940.7a1141aa001c469ea1563c6bae99c37d.0307"

}

aud为app id,也就是请求的client_id, sub为用户在该app下的唯一id, iat为token的创建时间,exp为token的有效期,email为邮箱地址,其他字段的含义可以看这里

另外,说下c_hash, at_hash, s_hash:
c_hash : code 的hash值
at_hash : access_token 的hash值
s_hash : state 的hash值
我没有做过验证,在apple官网没有找到答案。网上找到OAuth 2.0开放协议对这块有说明 https://openid.net/specs/openid-connect-core-1_0.html#TokenSubstitution

1

2

3

4

5

$code = "c37906543364e6b";

$hash_var = hash("sha256", $code);

$first_16_bytes = substr($hash_var, 0, 16);

$b64_var = base64_encode($first_16_bytes);

echo rtrim(strtr($b64_var, '+/', '-_'), '=');

服务端获取 identity Token

利用 authorizationCode 获取 identityToken 的方法:(官方文档 https//developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens)
验证的过程是HTTPS验证,API如下:
POST https://appleid.apple.com/auth/token
请求字段说明:
client_id:这个是app id
client_secret: 密钥信息,使用 JWT编码,详见后文
code: 授权码,传客户端取到的 authorizationCode
grant_type: 此时固定写 authorization_code
其他非必要字段,就不做说明。

其中,需要特别讲下 client_secret,是一个json数据,例子如下:

1

2

3

4

5

6

7

8

9

10

11

{

    "alg": "ES256",

    "kid": "ABC123DEFG"

}

{

    "iss": "DEF123GHIJ",

    "iat": 1437179036,

    "exp": 1493298100,

    "aud": "https://appleid.apple.com",

    "sub": "com.mytest.app"

}

其中,iss为team id,iat为当前时间戳,exp为失效时间戳,aud固定,sub为app包名; kid为开发者帐号后台申请private key时系统附带生成的key id,猜测是apple用以确定解密对应的public key
这个字段需要使用 JWT 编码,转成 JWT数据串。
这里,我封装了jwt,如下:

1

2

>> jwt.jwt_encode(header, payload, pri_pem, 'es256')

eyJhbGciOiJFUzI1NiIsImtpZCI6ImFiY2RlZmciLCJ0eXAiOiJKV1QifQ.eyJhZG1pbiI6dHJ1ZSwiaWF0IjoxNTE2MjM5MDIyLCJuYW1lIjoiSm9obiBEb2UiLCJzdWIiOiIxMjM0NTY3ODkwIn0.QDzbIkVpLZ1Uf6OwnrabKnz9xH3WJ_nLoiUZlT37IiVu3aXEMCfZkE3LlDUo14JUE6iBHo1B_jG91zwZOz7oZA

JWT是一个基于JSON结构化的编码标准(RFC 7519),它定义了一种紧凑的、自包含的方式,可在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。(jwt.io可获取实现算法)
JWT由三部分组成,之间用圆点(.)连接。这三部分分别是:Header.Payload.Signature

Header头部域,使用Base64编码,会消除等号{
“alg”: “HS256”,
“typ”: “JWT”
}
Payload数据域,使用 Base64编码,会消除等号{
“sub”: “1234567890”,
“name”: “John Doe”,
“iat”: 1516239022
}
Signature签名,对Header.Payload进行签名,支持多种算法

优点:支持多种签名算法,可兼顾安全性和效率,避免数据伪造
缺点:数据都是使用base64_encode简单编码,不能传输敏感信息。

需要注意的是,验证接口 /auth/token 返回的数据,字段如下:

access_token(Reserved for future use) A token used to access allowed data. Currently, no data set has been defined for access.
expires_inThe amount of time, in seconds, before the access token expires.
id_tokenA JSON Web Token that contains the user’s identity information.
refresh_tokenThe refresh token used to regenerate new access tokens. Store this token securely on your server.
token_typeThe type of access token. It will always be bearer.

关键字段 id_token,就是前面提到的 identity Token, 即用户信息,同样使用jwt编码,参照上文解开。

最后语

文章到这里就结束了,最后分享文中提到的 lua jwt库, 地址 https://github.com/chenweiqi/lua_jwt
主要逻辑是c开发的,lua只是实现扩展,有遇到bug喊我!

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐