教你在 Node.js 项目中接入 Sign with Apple 第三方登录
写在前面
在 WWDC19 大会上,苹果公司推出了一项有意思的内容,即 “Sign In with Apple”。这项由苹果提供的认证服务,可以让开发者允许用户使用 Apple Id 来登录他们的应用程序,Sign In with Apple使用OAuth登录授权标准。
本文将介绍使用苹果登录的整个流程,并演示如何用Node.js在Web端接入苹果三方登录。
Apple ID 的双重认证
Sign in with Apple使用双重验证,简单说就是当你首次使用Apple登录一个设备时,在输入Apple id和密码之后,还需要在其他已登录的Apple设备上确认授权,并输入已登录设备上提供的 验证码 进行验证。
工作原理
有了双重认证,只能通过您信任的设备(如 iPhone、iPad、Apple Watch 或 Mac)才能访问您的帐户。首次登录一台新设备时,您需要提供两种信息:您的密码和自动显示在您的受信任设备上的六位验证码。输入验证码后,您即确认您信任这台新设备。例如,如果您有一台 iPhone 并且要在新购买的 Mac 上首次登录您的帐户,您将收到提示信息,要求您输入密码和自动显示在您 iPhone 上的验证码。
由于只输入密码不再能够访问您的帐户,因此双重认证显著增强了 Apple ID 以及所有通过 Apple 储存的个人信息的安全性。
登录后,系统将不会再次要求您在这台设备上输入验证码,除非您完全退出登录帐户、抹掉设备数据或出于安全原因而需要更改密码。当您在 Web 上登录时,可以选择信任您的浏览器,这样当您下次从这台电脑登录时,系统就不会要求您输入验证码。
登录流程
- 登录一个Web网站,输入账号密码,apple设备弹出登录授权验证,输入验证码,即可登录。
- 首次登录会选择是否隐藏邮箱,选择隐藏将会使用apple提供的一个匿名邮箱而不是真实邮箱号。
- 当选择 信任浏览器后 ,之后在此浏览器中登录只需要输入账号、密码即可。
- 在登录后用户可以随时在apple设备上取消apple id在该程序上的授权登录。
- mac上safari浏览器上可直接验证登录。
- 也可以通过手机号等其他方式进行验证,apple设备开启双重认证,账户管理等一些常见使用问题可查此篇阅官方介绍 Apple ID 的双重认证
Apple开发者账号
申请
- 首先我们需要一个苹果开发者账号,进入 https://developer.apple.com/account/#/welcome ,点击底部加入 苹果开发者计划 ,按里面流程注册账号即可,如下图。
- 值得注意的是,加入开发者计划是 付费 的,无论公司还是个人都是99美元。
- 具体注册流程不再赘述,可参考此篇文章[苹果开发者账号申请和证书创建流程
]( https://www.jianshu.com/p/f10…
配置
- 当我们拥有一个苹果开发者账号后,需要进行相关配置来获得我们在web端接入apple登录时,所需要的一些id和文件,并做一些相关验证,此过程非常繁琐,此篇文章对配置流程有很详细的讲解,可以点击查阅 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的知识可 点击查阅此篇文章。
苹果开发者文档 提供了两篇在Web端接入苹果登录相关的文档 ,如下,一篇是前端开发文档 Sign in with Apple JS ,一篇是服务端开发文档 Sign in with Apple REST API ,可点击链接查阅详细内容。
1. 进入登录授权页
`<script type="text/javascript" src=" https://appleid.cdn-apple.com… ;>
`
- 前端操作非常简单,就是显示一个登录按钮,点击可跳转到苹果指定的授权登录页,苹果提供了一个js文件,你可以引入上面这个js文件然后直接在html中写入以下代码,页面将会出现苹果提供的登录按钮,点击即可跳转到苹果授权登录页。
- 第一种,你需要在
mate
标签的content
属性中写入相关配置账号
- 第二种,引入js文件后将得到AppleID对象,监听click点击事件,点击后直接执行
AppleID.auth.init
方法,将配置信息以对象的形式传进去,自动跳转到授权页
AppleID.auth.init({ clientId : '[CLIENT_ID]', scope : '[SCOPES]', redirectURI: '[REDIRECT_URI]', state : '[STATE]' });
官方文档对参数的定义如上图 跳转去连接
- 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
,如下图
*值得注意的是当用户 首次登录 时,apple将返回给我们 user
字段(如上图),里面有用户名和邮箱(或匿名邮箱),我们应该将用户信息保存在服务端,与最终获取的用户唯一标识相对应。
在首次登录过后我们将永远无法再次获取用户信息,只有用户手动取消appleId在该程序上的登录,并等待一段时间再次登录时才会重新发送用户信息,所以当我们首次请求时应及时把用户信息保存下来,如下图, 跳转去链接
接下来我们需要通过上步获取的授权码去获取身份令牌, 这需要我们在服务端去发起一个请求 ,请求url与参数,如下图, 跳转去链接 。
请求url为 POST https://appleid.apple.com/auth/token
获取令牌我们需要传以下几个参数
grant_type client_id redirect_uri code client_secret
刷新令牌我们需要传以下参数
grant_type client_id client_secret refresh_token
在此过程中,最重要的就是 client_secret
参数,为生成JWT,官网文档对JWT生成的相关条件如下图,可 跳转去连接
在 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
接口获取公钥, 跳转去链接
然后我们用 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登录哦, 声享
补充
127.0.0.1