Sign in with Apple NODE,web端接入蘋果第三方登錄

寫在前面

在 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開發者賬號

申請

  • 首先我們需要一個蘋果開發者賬號,進入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(一)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章