教你在 Node.js 项目中接入 Sign with Apple 第三方登录

5,211 阅读12分钟

写在前面

在 WWDC19 大会上,苹果公司推出了一项有意思的内容,即 “Sign In with Apple”。这项由苹果提供的认证服务,可以让开发者允许用户使用 Apple Id 来登录他们的应用程序,Sign In with Apple使用OAuth登录授权标准。

本文将介绍使用苹果登录的整个流程,并演示如何用NODE在Web端接入苹果三方登录。

Apple ID 的双重认证

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登录流程.GIF

Apple开发者账号

申请

配置

  • 当我们拥有一个苹果开发者账号后,需要进行相关配置来获得我们在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/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>

  • 前端操作非常简单,就是显示一个登录按钮,点击可跳转到苹果指定的授权登录页,苹果提供了一个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>

配置参数

官方文档对参数的定义如上图跳转去连接

  • 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:'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生成的相关条件如下图,可跳转去连接 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 开发者账号哦!github.com/wwenj/Sign-…

可在我们项目上体验apple登录哦,声享

补充

  • 在通过授权码 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(一)

原创声明

原创文章,转载请注明作者和文章原链接,关注公众号看更多文章哦!