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

写在前面

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

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

Apple ID 的双重认证

Apple ID 的双重认证
Sign in with Apple使用双重验证,简单说就是当你首次使用Apple登陆一个设备时,在输入Apple id和密码以后,还须要在其余已登陆的Apple设备上确认受权,并输入已登陆设备上提供的 验证码进行验证。

工做原理

有了双重认证,只能经过您信任的设备(如 iPhone、iPad、Apple Watch 或 Mac)才能访问您的账户。首次登陆一台新设备时,您须要提供两种信息:您的密码和自动显示在您的受信任设备上的六位验证码。输入验证码后,您即确认您信任这台新设备。例如,若是您有一台 iPhone 而且要在新购买的 Mac 上首次登陆您的账户,您将收到提示信息,要求您输入密码和自动显示在您 iPhone 上的验证码。前端

因为只输入密码再也不可以访问您的账户,所以双重认证显著加强了 Apple ID 以及全部经过 Apple 储存的我的信息的安全性。java

登陆后,系统将不会再次要求您在这台设备上输入验证码,除非您彻底退出登陆账户、抹掉设备数据或出于安全缘由而须要更改密码。当您在 Web 上登陆时,能够选择信任您的浏览器,这样当您下次从这台电脑登陆时,系统就不会要求您输入验证码。node

登陆流程

  • 登陆一个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?ios

  • 当配置结束后咱们将得到咱们所需的两个文件、三个ID、和一个URL链接,以下(演示用,非正确)git

    redirectURI = 'https://abc.baidu.com/appleAuth' // 本身设置的重定向域名,可添加多个
    webClientId = 'com.baidu.abc.signInWithApple';  // 设置的client_id,通常是域名的反写
    teamId = 'JI87S9KI7D';  // 10个字符的team_id
    keyId = 'KOI98S78J6';  // 获取的10个字符的密钥标识符
    复制代码
  • 一个以.p8结尾的文本文件,里面是生成的密钥,用做生成JWT,做为请求Token时的参数之一github

  • 另外一个apple-developer-domain-association.txt文本放在项目代码中,做为帐号配置过程当中验证用,保证浏览器url输入https://abc.baidu.com/.well-known/apple-developer-domain-association.txt时,能外网访问到此文本中的内容,完成后点击苹果开发者帐号配置过程当中的验证按钮(具体操做参考上面推荐的配置文章),经过后可进行正常开发调试。验证经过后可删除此文件。 web

正式开发(开始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(一)

相关文章
相关标签/搜索