编者按:本文转载自 ThinkJS 知乎专栏,作者王文健,奇舞团前端工程师。
在 WWDC19 大会上,苹果公司推出了一项有意思的内容,即 “Sign In with Apple”。这项由苹果提供的认证服务,可以让开发者允许用户使用 Apple Id 来登录他们的应用程序,Sign In with Apple使用OAuth登录授权标准。
本文将介绍使用苹果登录的整个流程,并演示如何用Node.js在Web端接入苹果第三方登录。
Sign in with Apple使用双重验证,简单说就是当你首次使用Apple登录一个设备时,在输入Apple id和密码之后,还需要在其他已登录的Apple设备上确认授权,并输入已登录设备上提供的验证码进行验证。
有了双重认证,只能通过您信任的设备(如 iPhone、iPad、Apple Watch 或 Mac)才能访问您的帐户。首次登录一台新设备时,您需要提供两种信息:您的密码和自动显示在您的受信任设备上的六位验证码。输入验证码后,您即确认您信任这台新设备。例如,如果您有一台 iPhone 并且要在新购买的 Mac 上首次登录您的帐户,您将收到提示信息,要求您输入密码和自动显示在您 iPhone 上的验证码。
登录一个Web网站,输入账号密码,apple设备弹出登录授权验证,输入验证码,即可登录。
首次登录会选择是否隐藏邮箱,选择隐藏将会使用apple提供的一个匿名邮箱而不是真实邮箱号。
当选择信任浏览器后,之后在此浏览器中登录只需要输入账号、密码即可。
在登录后用户可以随时在apple设备上取消apple id在该程序上的授权登录。
mac上safari浏览器上可直接验证登录。
也可以通过手机号等其他方式进行验证,apple设备开启双重认证,账户管理等一些常见使用问题可查此篇阅官方介绍Apple ID 的双重认证(https://support.apple.com/zh-cn/HT204915)
首先我们需要一个苹果开发者账号,进入https://developer.apple.com/account/#/welcome,点击底部加入苹果开发者计划,按里面流程注册账号即可,如下图。
值得注意的是,加入开发者计划是付费的,无论公司还是个人都是99美元。
具体注册流程不再赘述,可参考此篇文章苹果开发者账号申请和证书创建流程(https://www.jianshu.com/p/f10a10c6e8e3)
当我们拥有一个苹果开发者账号后,需要进行相关配置来获得我们在web端接入apple登录时,所需要的一些id和文件,并做一些相关验证,此过程非常繁琐,此篇文章对配置流程有很详细的讲解,可以点击查阅What the Heck is Sign In with Apple?(https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple)
当配置结束后我们将获得我们所需的两个文件、三个ID、和一个URL连接,如下(演示用,非正确)
redirectURI = 'https://abc.baidu.com/appleAuth' // 自己设置的重定向域名,可添加多个
webClientId = 'com.baidu.abc.signInWithApple'; // 设置的client_id,一般是域名的反写
teamId = 'JI87S9KI7D'; // 10个字符的team_id
keyId = 'KOI98S78J6'; // 获取的10个字符的密钥标识符
一个以.p8结尾的文本文件,里面是生成的密钥,用作生成JWT,作为请求Token时的参数之一
另一个apple-developer-domain-association.txt文本放在项目代码中,作为账号配置过程中验证用,保证浏览器url输入https://abc.baidu.com/.well-known/apple-developer-domain-association.txt时,能外网访问到此文本中的内容,完成后点击苹果开发者账号配置过程中的验证按钮(具体操作参考上面推荐的配置文章),通过后可进行正常开发调试。验证通过后可删除此文件。
正式开发(开始OAuth 2.0流程)
OAuth
正式开发前我们可以先了解下OAuth 2.0的标准,OAuth是一个关于授权的开放网络标准,apple登录正是使用了此标准,如果你了解此标准的授权流程,在下面的开发中会觉得很熟悉,OAuth流程大概如下:
用户访问客户端,后者将前者导向认证服务器。
用户选择是否给予客户端授权。
假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见
认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
更多关于OAuth的知识可点击查阅此篇文章。(http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)
苹果开发者文档(https://developer.apple.com/documentation)提供了两篇在Web端接入苹果登录相关的文档 ,如下:
一篇是前端开发文档Sign in with Apple JS(https://developer.apple.com/documentation/signinwithapplejs)
一篇是服务端开发文档Sign in with Apple REST API(https://developer.apple.com/documentation/signinwithapplerestapi),可点击链接查阅详细内容。
1. 进入登录授权页
前端
前端操作非常简单,就是显示一个登录按钮,点击可跳转到苹果指定的授权登录页,苹果提供了一个js文件,你可以引入上面这个js文件然后直接在html中写入以下代码,页面将会出现苹果提供的登录按钮,点击即可跳转到苹果授权登录页。
第一种,你需要在mate标签的content属性中写入相关配置账号
<html>
<head>
<meta name="appleid-signin-client-id" content="com.baidu.abc.signInWithApple">
<meta name="appleid-signin-scope" content="[SCOPES]">
<meta name="appleid-signin-redirect-uri" content="https://abc.baidu.com/appleAuth">
<meta name="appleid-signin-state" content="[STATE]">
head>
<body>
<div id="appleid-signin" data-color="black" data-border="true" data-type="sign in">div>
<script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js">script>
body>
html>
第二种,引入js文件后将得到AppleID对象,监听click点击事件,点击后直接执行AppleID.auth.init 方法,将配置信息以对象的形式传进去,自动跳转到授权页
<html>
<head>
head>
<body>
<script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js">script>
<div id="appleid-signin" data-color="black" data-border="true" data-type="sign in">div>
<script type="text/javascript">
AppleID.auth.init({
clientId : '[CLIENT_ID]',
scope : '[SCOPES]',
redirectURI: '[REDIRECT_URI]',
state : '[STATE]'
});
script>
body>
html>
官方文档对参数的定义如上图,具体链接(https://developer.apple.com/documentation/signinwithapplejs/incorporating_sign_in_with_apple_into_other_platforms)
client_id:获取的client_id,必传
redirect_uri:设置的重定向url,当用户同意授权后,会发起一个该URL的post请求,开发者需要在后台设置相应接口去接收他,服务端通过apple传来的code参数去请求身份令牌,必传。
scope:权限范围,name或者email,或者两个都设,只有设了权限范围,你才能在授权过程中得到相应的用户信息。
state:表示客户端的当前状态,可以指定任意值,会原封不动地返回这个值,你可以通过它做些验证,生成一个随机数,并存在服务端,当获取token时对比传回的 state 是否时同一个,来避免一些攻击。
这里面只有client_id,redirect_uri,是必须的,其他如果不设会自动设置默认值。
你可以使用官方提供的按钮,当然也可以不用,当你点击登录按钮后会实际会跳转到一下地址,你可以选择直接手动拼接跳转授权页地址。
https://appleid.apple.com/auth/authorize?client_id=[CLIENT_ID]&redirect_uri=[REDIRECT_URI]&response_type=[RESPONSE_TYPE]&scope=[SCOPES]&response_mode=[RESPONSE_MODE]&state=[STATE]
如果手动拼接的话 response_type 应设为 code, response_mode应设为form_post,
2. 接收授权码code,并向apple申请Token
当用户给予授权后,apple服务器将发起一个POST请求至当时设置的redirectURI,同时附上一个授权码code,id_token可用于刷新token,这里的id_token字段只有通过验证后才会有,首次请求并没有这个字段,首次验证通过后再次登录可直接通过解析这个id_token来获得用户唯一标识,这里首次登录,我们将只有code和state,如下图
下图是官方文档对请求参数的解释,具体链接(https://developer.apple.com/documentation/signinwithapplejs/incorporating_sign_in_with_apple_into_other_platforms),只有用户取消授权时才会返回唯一一个错误码`user_cancelled_authorize`
值得注意的是当用户首次登录时,apple将返回给我们user字段(如上图),里面有用户名和邮箱(或匿名邮箱),我们应该将用户信息保存在服务端,与最终获取的用户唯一标识相对应。
在首次登录过后我们将永远无法再次获取用户信息,只有用户手动取消appleId在该程序上的登录,并等待一段时间再次登录时才会重新发送用户信息,所以当我们首次请求时应及时把用户信息保存下来,如下图,具体链接(https://developer.apple.com/documentation/signinwithapplerestapi/authenticating_users_with_sign_in_with_apple)
接下来我们需要通过上步获取的授权码去获取身份令牌,这需要我们在服务端去发起一个请求,请求url与参数,如下图,具体链接(https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens)。
请求url为POST https://appleid.apple.com/auth/token
获取令牌我们需要传以下几个参数
grant_type:'authorization_code'为获取令牌
client_id:client_id
redirect_uri:redirect_uri
code:上一步获取到的授权码,code
client_secret:一个生成的JWT,如果不了解可自行查阅有关JWT的知识
刷新令牌我们需要传以下参数
grant_type:'refresh_token'为刷新令牌
client_id:client_id
client_secret:client_secret,
refresh_token:上一步获取到的id_token
在此过程中,最重要的就是client_secret参数,为生成JWT,官网文档对JWT生成的相关条件如下图,具体链接(https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens)
在Node代码中我们使用 Node 的jsonwebtoken库去生成jwt,代码如下。
规定生成的JWT最长期限为6个月,你可以手动生成 JWT ,用在项目里,但必须在将要过期前更新它,我们把生成 JWT 的代码写在程序里,每次都重新生成一个JWT。
// 生成JWT
const jwt = require('jsonwebtoken');
const fs = require('fs');
const path = require('path');
// apple开发者账号配置下载的AuthKey_XHGXCP8B9S.p8文件
const PRIVATEKEY = fs.readFileSync(path.join(__dirname, './AuthKey_XH******9S.txt'), {encoding: 'utf-8'});
const TEARM_ID = 'K5******G8';
const CLIENT_ID = 'com.baidu.abc.signInWithApple';
const KEY_ID = 'XH******9S';
async getClientSecret() {
const headers = {
alg: 'ES256',
kid: KEY_ID
};
const timeNow = Math.floor(Date.now() / 1000);
const claims = {
iss: TEARM_ID,
aud: 'https://appleid.apple.com',
sub: CLIENT_ID,
iat: timeNow,
exp: timeNow + 15777000
};
const token = jwt.sign(claims, PRIVATEKEY, {
algorithm: 'ES256',
header: headers
// expiresIn: '24h'
});
return token;
}
接下来我们需要在服务端写一个api接口去接收apple发起的post请求,拿到请求参数后在服务端发起/auth/token请求去请求access token,代码如下(thinkjs 编写)
const axios = require('axios');
const qs = require('qs');
const Base = require('./base.js');
export default class extends think.Controller {
// appleAuth接口
async appleAuthAction() {
const body = this.post();
// 获取token,刷新传grant_type:refresh_token与refresh_token
const params = {
grant_type: 'authorization_code', // refresh_token authorization_code
code: body.code,
redirect_uri: [REDIRECT_URI],
client_id: [CLIENT_ID],
client_secret: this.getClientSecret()
// refresh_token:body.id_token
};
const token = await this.authToken(params);
// verifyIdToken为解密获取的id_token信息
const jwtClaims = await this.verifyIdToken(token.data.id_token, [CLIENT_ID]);
this.success({
data: token.data,
verifyData: jwtClaims
});
}
// 发起请求
async authToken(params) {
return axios.request({
method: 'POST',
url: 'https://appleid.apple.com/auth/token',
data: qs.stringify(params),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
}
};
请求成功后将返回 token ,如下图
其中我们用到的verifyIdToken方法就是对该id_token解密,首先我们需要通过apple提供GET https://appleid.apple.com/auth/keys接口获取公钥,具体链接(https://developer.apple.com/documentation/signinwithapplerestapi/fetch_apple_s_public_key_for_verifying_token_signature)
然后我们用jwt.verify通过公钥解密id_token,代码如下
const NodeRSA = require('node-rsa');
// 获取公钥
async getApplePublicKey() {
let res = await axios.request({
method: "GET",
url: "https://appleid.apple.com/auth/keys",
})
let key = res.data.keys[0]
const pubKey = new NodeRSA();
pubKey.importKey({ n: Buffer.from(key.n, 'base64'), e: Buffer.from(key.e, 'base64') }, 'components-public');
return pubKey.exportKey(['public']);
};
// 通过公钥和RS256算法解密id_token
async verifyIdToken(id_token, client_id) {
const applePublicKey = await this.getApplePublicKey();
const jwtClaims = jwt.verify(idToken, applePublicKey, { algorithms: 'RS256' });
return jwtClaims;
};
解密后得到的verify.sub就是用户apple账号登录在该程序中的唯一标识,我们可以把它存到程序的数据库中与用户信息做映射,用于标识用户身份。
终于我们完成了整个 apple 第三方登录流程,得到了我们需要的用户唯一标识与用户信息,更加完善了我们项目的登录模块。
文中 demo 演示的具体代码已经上传到 Github 中,可直接下载运行体验,但未上传所有账号相关信息,你需要有一个 apple 开发者账号哦!https://github.com/wwenj/Sign-in-with-Apple-for-node
可在我们项目上体验apple登录哦!声享(https://ppt.baomitu.com/)
在通过授权码 code 申请 token 的过程中,apple服务器向我们的服务器发起的请求是通过开发者账号配置严格定义的,无法更改或附加其他参数,只有当时请求的 state 参数会被原封不动的返回回来,所以我们可以把自己需要带的参数转成 json ,一起放到state中,最后再解析出来使用。
配置的重定向URL是不允许配置127.0.0.1的,我们开发过程中可以通过配置本地 host ,将域ip指向本地。
即使用户在 apple 设备上停止 apple id 对该项目的授权,当用户再次登录时,该用户的唯一标识仍然不会改变。
What the Heck is Sign In with Apple
Sgin in with Apple NODE
Sign in with Apple JS
Sign in with Apple REST API
Sign In With Apple(一)
点击“阅读原文”查看链接地址
《奇舞周刊》是360公司专业前端团队「奇舞团」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。