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

寫在前面

在 WWDC19 大會上,蘋果公司推出了一項有意思的內容,即 「Sign In with Apple」。這項由蘋果提供的認證服務,可讓開發者容許用戶使用 Apple Id 來登陸他們的應用程序,Sign In with Apple使用OAuth登陸受權標準。 javascript

本文將介紹使用蘋果登陸的整個流程,並演示如何用Node.js在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開發者帳號

申請

  • 首先咱們須要一個蘋果開發者帳號,進入https://developer.apple.com/account/#/welcome,點擊底部加入蘋果開發者計劃,按裏面流程註冊帳號便可,以下圖。
  • 值得注意的是,加入開發者計劃是付費的,不管公司仍是我的都是99美圓。
  • 具體註冊流程再也不贅述,可參考此篇文章[蘋果開發者帳號申請和證書建立流程

](https://www.jianshu.com/p/f10...
ios

配置

  • 當咱們擁有一個蘋果開發者帳號後,須要進行相關配置來得到咱們在web端接入apple登陸時,所須要的一些id和文件,並作一些相關驗證,此過程很是繁瑣,此篇文章對配置流程有很詳細的講解,能夠點擊查閱What the Heck is Sign In with Apple?
  • 當配置結束後咱們將得到咱們所需的兩個文件、三個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時的參數之一
  • 另外一個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流程大概以下:github

  1. 用戶訪問客戶端,後者將前者導向認證服務器。
  2. 用戶選擇是否給予客戶端受權。
  3. 假設用戶給予受權,認證服務器將用戶導向客戶端事先指定的"重定向URI"(redirection URI),同時附上一個受權碼。
  4. 客戶端收到受權碼,附上早先的"重定向URI",向認證服務器申請令牌。這一步是在客戶端的後臺的服務器上完成的,對用戶不可見
  5. 認證服務器覈對了受權碼和重定向URI,確認無誤後,向客戶端發送訪問令牌(access token)和更新令牌(refresh token)。

更多關於OAuth的知識可點擊查閱此篇文章。web

蘋果開發者文檔提供了兩篇在Web端接入蘋果登陸相關的文檔 ,以下,一篇是前端開發文檔Sign in with Apple JS ,一篇是服務端開發文檔Sign in with Apple REST API ,可點擊連接查閱詳細內容。算法

1. 進入登陸受權頁

前端
`<script type="text/javascript" src="https://appleid.cdn-apple.com...;></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 開發者帳號哦!https://github.com/wwenj/Sign-in-with-Apple-for-node

可在咱們項目上體驗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(一)

相關文章
相關標籤/搜索