Sign-in-with-Apple-NODEweb端接入苹果第三方登录

6次阅读

共计 7672 个字符,预计需要花费 20 分钟才能阅读完成。

写在前面

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

本文将介绍使用苹果登录的整个流程,并演示如何用 NODE 在 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/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:表示客户端的当前状态,可以指定任意值,会原封不动地返回这个值,你可以通过它做些验证,防止一些攻击。

这里面只有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]

2. 接收授权码 code,并想 apple 申请 Token

当用户给予授权后,apple 服务器将发起一个 POST 请求至当时设置的 redirectURI,同时附上一个授权码codeid_token 用于刷新 token,首次登录将只有 codestate,如下图


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

* 值得注意的是当用户 首次登录 时,apple 将返回给我们 user 字段(如上图),里面有用户名和邮箱(或匿名邮箱),我们应该将用户信息保存在服务端,与最终获取的用户唯一 token 相对应。

在首次登录过后我们将永远无法再次获取用户信息,只有用户手动取消 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
  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';
  
  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,代码如下

// 获取 access token
const express = require('express')
const app = express()
const bodyParser = require('body-parser')
const axios = require('axios')
const qs = require('qs');

// 获取 access token
app.post('/appleAuth', bodyParser.urlencoded({ extended: false}), (req, res) => {
    // 获取 token,刷新传 grant_type:refresh_token 与 refresh_token
    const params = {
        grant_type: 'authorization_code', // refresh_token authorization_code
        code: req.body.code,
        redirect_uri: apple.url,
        client_id: apple.client_id,
        client_secret: getClientSecret(),
        // refresh_token:req.body.id_token
    }
    axios.request({
        method: "POST",
        url: "https://appleid.apple.com/auth/token",
        data: qs.stringify(params),
        headers: {'Content-Type': 'application/x-www-form-urlencoded',}
    }).then(response => {
        // verifyIdToken 为解密获取的 id_token 信息
        verifyIdToken(response.data.id_token,apple.client_id).then((jwtClaims)=>{
            return res.json({
                message: 'success',
                data: response.data,
                verifyData:jwtClaims
            })
        })
    }).catch(error => {return res.status(500).json({
            message: '错误',
            error: error.response.data
        })
    })
})

请求成功后将返回 access token,如下图
<!––>

其中我们用到的 verifyIdToken 方法就是对该 id_token 解密,首先我们需要通过 apple 提供 GET https://appleid.apple.com/auth/keys 接口获取公钥,跳转去链接

然后我们用 jwt.verify 通过公钥解密id_token,代码如下

const NodeRSA = require('node-rsa');
// 获取公钥
const getApplePublicKey = async () => {
    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
const verifyIdToken = async (id_token, client_id) => {const applePublicKey = await 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 登录哦,声享

相关链接

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(一)

正文完
 0