教你在 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流程大概如下:

  1. 用户访问客户端,后者将前者导向认证服务器。
  2. 用户选择是否给予客户端授权。
  3. 假设用户给予授权,认证服务器将用户导向客户端事先指定的”重定向URI”(redirection URI),同时附上一个授权码。
  4. 客户端收到授权码,附上早先的”重定向URI”,向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见
  5. 认证服务器核对了授权码和重定向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_idredirect_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 ,同时附上一个授权码 codeid_token 可用于刷新token,这里的 id_token 字段只有通过验证后才会有,首次请求并没有这个字段,首次验证通过后再次登录可直接通过解析这个 id_token 来获得用户唯一标识,这里首次登录,我们将只有 codestate ,如下图

下图是官方文档对请求参数的解释
跳转去连接 ,只有用户取消授权时才会返回唯一一个错误码
user_cancelled_authorize

*值得注意的是当用户 首次登录 时,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