Sign in with Apple(苹果授权登陆)
用户是懒的,有快捷登录的情况下,没人会去输入邮箱,接收验证码并输入验证码来完成注册。
明明有一步完成操作的方式,又有几个用户会选择用几步去完成呢。
国内的 APP 基本都会加上微信登录,同理换到海外,如果你的 APP 面向是海外的用户,除 Facebook 登录以外,Apple 登录也同样重要。
APP端
关于手机端登录的代码,这里不做多余介绍,我们看下登录成功后, 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、email、fullName、authorizationCode、identityToken 。
- userID:授权的用户唯一标识
- email、fullName:授权的用户资料
- authorizationCode:授权code。(授权code是有时效性的,且使用一次即失效)
- identityToken:授权用户的JWT凭证
服务端
服务端校验有两种方式。
- 基于JWT的算法验证;
- 基于授权码的验证;
基于JWT的算法验证
APP上传userID
、identityToken
,服务端解析identityToken
获取用户唯一标识与userID
进行对比。
服务端解析identityToken
的步骤跟下面3、解码id_token
是一样的。
基于授权码的验证
- 首先需要得到
client_secret
。 - 根据APP上传上来的
authorizationCode
请求苹果服务器进行登录,获取id_token
。 - 解析
id_token
,获取用户唯一标识。
1、获取client_secret
参考:
https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple
https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
首先需要了解如何构建client_secret
。
client_secret
参数是一个 JWT,singature 部分使用非对称加密 RSASSA【RSA签名算法】 和 ECDSA【椭圆曲线数据签名算法】。
生成client_secret
之前:
- 获取 APP 的 bundleId。
- 获取开发者账户的 TeamID。
- 创建 privateKey,获取到 Key ID 和 私钥。
- 创建完之后把私钥下载下来,并保存好,注意,私钥只能下载一次。
- 生成
client_secret
。可以通过如下代码生成 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
key_file:私钥路径。
- 创建文件
secret_gen.rb
,把上面代码粘贴进去,执行ruby secret_gen.rb
即可生成client_secret
。
2、进行登录
服务器接收到 APP 提交的信息后,根据authorizationCode
获取id_token
,然后再获取苹果用户的唯一标识 userId。
所需参数:
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
为 APP 获取到的 authorizationCode。grant_type
传固定字符串authorization_code
。client_secret
是前面生成的,需注意是有时效性的,半年过期。
发起请求,返回的结果为:
{
"access_token": "一个token,此处省略",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "一个token,此处省略",
"id_token": "结果是JWT,字符串形式,此处省略"
}
参数解释,官方文档:https://developer.apple.com/documentation/sign_in_with_apple/tokenresponse
3、解码id_token
解码步骤:
- 先要获取
Public Key
; - 根据获取
Public Key
去进行解码。
获取 Public Key。https://appleid.apple.com/auth/keys
结果:
{
"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 后,对id_token
进行解析。
// claims 解码
{
"iss":"https://appleid.apple.com", // 苹果签发的标识
"aud":"com.skyming.devicemonitor", // 接收者的APP ID
"exp":1565668086,
"iat":1565667486,
"sub":"001247.93b3a799a7c84c0cb46cd08f100797f2.0704", //用户的唯一标识
"c_hash":"Oh2am9eMNWVY3dq5JmClbg",
"auth_time":1565667486
}
- iss:苹果签发的标识;
- aud:与你的 app 的 bundleID 一致;
- sub:与手机端获得的 user 一致;
服务器端通过对比 sub 字段信息是否与手机端上传的 user 信息一致来确定是否成功登录。
源码
所需依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
代码:
import cn.hutool.core.codec.Base64;
import cn.hutool.core.lang.Dict;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class AppleUtil {
public static void main(String[] args) {
String identityToken = "eyJraWQiOiJXNldjT0tCIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnVtZW94LnFpYmxhIiwiZXhwIjoxNjcyMzY5NDUwLCJpYXQiOjE2NzIyODMwNTAsInN1YiI6IjAwMTMwNy5mN2NkNWFkYmVlMDY0ZDc0YjI4ZjI2OWE3MmUzNjU4Mi4wNTMwIiwiY19oYXNoIjoiV3VBS1lycUhlcUxiSS12cnRLRkF4ZyIsImVtYWlsIjoiMjgwMDM1NzY4QHFxLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImF1dGhfdGltZSI6MTY3MjI4MzA1MCwibm9uY2Vfc3VwcG9ydGVkIjp0cnVlfQ.TUMsZXG8qjA2EjisrKnyksukDY0vbQnDVXsgl1y7hFrbOlu0pyUkm4FfJHB-OAg1FOKFA5Nn7zGLezgGbsgmmSfnP5piitZMtQkNEZ-5FNuXJ1KRHNJexQePUrEpKrUMTVLoi23WDPZOofNlYuyvRnnkQaO2jG4XZ94IHbwrOpbrPYfNNMF9oaRG2w8RV125TN2Bh2nLHP_Xm7Fd5RQDEJVNTBkSkIJ1xbHlsEemrcEd-SPDtp2jvAliiMNyK5FlsHgShuUeAeW2nmyrx0hUanAymjUUckCCOJsdawia7ps_uzdmjWvbDgvUtuIzZVf-kevWC_dqzwEcDjjlkG1R2Q";
AppleUtil.test(identityToken);
}
public static void test(String identityToken) {
String clientId = "com.xxx.xxxx";
// 解析id_token
Map<String, JSONObject> json = parserIdentityToken(identityToken);
JSONObject header = json.get("header");
String kid = header.getStr("kid");
// 根据kid,获取相应的公钥
PublicKey publicKey = getPublicKey(kid);
if (publicKey == null) {
log.error("[苹果登录失败 publicKey获取失败 clientId<{}> kid<{}>]", clientId, kid);
}
getAppleUserId(clientId, publicKey, identityToken);
}
private static Map<String, Object> getAppleUserId(String appleClientId, PublicKey publicKey, String identityToken) {
String appleIssuerUrl = "https://appleid.apple.com";
JwtParser jwtParser = Jwts.parser().requireIssuer(appleIssuerUrl).requireAudience(appleClientId).setSigningKey(publicKey);
Jws<Claims> claim = jwtParser.parseClaimsJws(identityToken);
if (claim != null) {
Map<String, Object> result = claim.getBody();
String iss = result.get("iss").toString();
String sub = result.get("sub").toString();
String aud = result.get("aud").toString();
Long exp = Long.parseLong(result.get("exp").toString()) * 1000;
log.info("[苹果登录iss<{}> sub<{}> aud<{}> exp<{}>]", iss, sub, aud, exp);
if (appleIssuerUrl.equals(iss) && appleClientId.equals(aud)) {
log.error("[苹果账号登录成功 appleClientId<{}> userId<{}>]", appleClientId, sub);
return new Dict().set("userId", sub);
}
}
return null;
}
/**
* 解析id_token
*
* @param identityToken
* @return
*/
private static Map<String, JSONObject> parserIdentityToken(String identityToken) {
Map<String, JSONObject> map = new HashMap<>(2);
String[] arr = identityToken.split("\\.");
String deHeader = Base64.decodeStr(arr[0]);
JSONObject header = JSONUtil.parseObj(deHeader);
String dePayload = Base64.decodeStr(arr[1]);
JSONObject payload = JSONUtil.parseObj(dePayload);
map.put("header", header);
map.put("payload", payload);
return map;
}
/**
* 获取苹果的公钥
*
* @param kid
* @return
*/
private static PublicKey getPublicKey(String kid) {
Map<String, Map<String, String>> map = getAppleKeys();
try {
String n = map.get(kid).get("n");
String e = map.get(kid).get("e");
BigInteger modulus = new BigInteger(1, Base64.decode(n));
BigInteger publicExponent = new BigInteger(1, Base64.decode(e));
RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, publicExponent);
//目前kty均为 "RSA"
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(spec);
} catch (Exception e) {
System.err.println("getPublicKey error:{}" + e);
}
return null;
}
/**
* 请求苹果公钥信息
*
* @return
*/
private static Map<String, Map<String, String>> getAppleKeys() {
Map<String, Map<String, String>> result = new HashMap<>(2);
String keys = HttpUtil.get("https://appleid.apple.com/auth/keys");
JSONObject keysObject = JSONUtil.parseObj(keys);
JSONArray jsonArray = keysObject.getJSONArray("keys");
for (int i = 0; i < jsonArray.size(); i++) {
Map<String, String> m = new HashMap<>();
m.put("n", jsonArray.getJSONObject(i).getStr("n"));
m.put("e", jsonArray.getJSONObject(i).getStr("e"));
result.put(jsonArray.getJSONObject(i).getStr("kid"), m);
}
return result;
}
}
文章
- 6
- 0
-
分享