記一次OAuth2.0使用者鑑權

吃瓜的禿頭蘇打發表於2020-03-28

前言

我們平時登入不同的平臺,總會有使用到Token的場景,比如用github賬戶登入掘金,這個時候我們肯定不會把自己的github賬號密碼給掘金, OAuth 就是這樣一套機制,用於各種免密授權登入場景,在便利的同時保證安全性,其實就是向平臺申請token授權。筆者最近在用spotify API二次開發自己的應用,所以也碰到使用者鑑權登入的場景,踩坑蠻多,也學到很多,所以整理下來,希望能幫助到大家?。

簡要的授權流程

  • 使用者開啟客戶端以後,客戶端要求使用者給予授權(掘金登入選項:github登入)。
  • 使用者同意給予客戶端授權(我點選github登入,登入通過)。
  • 客戶端使用上一步獲得的授權,向認證伺服器申請令牌(掘金申請 access_token)。
  • 認證伺服器對客戶端進行認證以後,確認無誤,同意發放令牌(github給他 access_token)。
  • 客戶端使用令牌,向資源伺服器申請獲取資源(掘金拿著access_token 去換資源)。
  • 資源伺服器確認令牌無誤,同意向客戶端開放資源(github校驗通行)。

掘金和github簡要舉例,幫助大家理解,接下來我用Spotify的鑑權詳細舉例吧

授權碼模式(authorization code)

你的網站需要獲得伺服器的授權,要現在對應的開發者申請賬號、祕鑰、重定向地址

  • client_id:我們去spotify那裡註冊的
  • client Secret: soptify給我們的祕鑰
  • redirect_uri:認證伺服器把客戶端重定向去的一個 url(本地開發一般是loacalhost)
  • response_type:需要你給我一個 code
  • state:任意值,規範規請求和返回時都是一樣的值

首先示範一下如何在spotify develope申請開發者身份

這裡針對申請spotify開發者身份的流程介紹,圖文較多,不敢興趣的可以劃過,下面有我對OAth鑑權的流程介紹。

記一次OAuth2.0使用者鑑權

  • 登入後點選create a client id,生成一個專用的client_idclient_secret

記一次OAuth2.0使用者鑑權

  • 同時設定Redirect URIs,這是通過鑑權後重定向的地址,埠上執行你的應用,一定要填寫準確。

記一次OAuth2.0使用者鑑權

  • Scopes 許可權選擇,許可權選擇在進行授權申請之前,要先確定這個應用需要哪些許可權,確定好了再到授權過程中通過後端引數進行宣告。

    Spotify對許可權進行了詳細的分類,全部的許可權如下:

記一次OAuth2.0使用者鑑權
soptify官網的授權流程

Authentication 授權

授權的最終目的是獲取一個名為access_token的值,然後用這個access_token去獲取各種個樣的API資訊。

Spotify為了嚴格區分不同的用途和許可權,把這個access_token的獲取方法分為了三種流程,各自的許可權、存活期都不同。

三種流程特點如下:

  • Authorization Code Flow: 標準方法,可重新整理token
  • Client Credentials Flow: app級token,不可獲取使用者行為。
  • Implicit Grant Flow: 臨時授權。可獲取使用者行為,不可重新整理。存活期短。

如果沒有使用者的點選授權,那麼只能使用後兩者的授權模式。在使用者點選授權後,才能拿到使用者的行為種子做個性化開發,這也是推薦的開發者授權模式,接下來就是Authorization Code授權流程:

  • /authorize傳送GET請求,請求頭包括client_idredirect_uri
  • /authorize,accounts判斷是有效client_idredirect_uri,client_secret(這裡注意必須和你申請應用填的完全一致)後,Spotify彈出頁面,使用者手動登入並點選允許授權
  • Spotify把頁面跳轉至自己設定的callback網址,並明文傳輸一個Code
  • Code碼向/token傳送POST請求,並在header中包括一個動態生成並base64編碼的Authorization字串,格式為Authorization: Basic *<base64 encoded client_id:client_secret>*
  • 從Spotify獲得訪問令牌access_token和更新令牌refresh_token(雙令牌)
  • 拿到授權碼acess_token後,每次向瀏覽器請求資源,請求頭都會加上Authorization欄位
  • access_token過期後就不能再用了,這時請求伺服器會返回401狀態碼,這個時候就要用refresh token來更新token:用refresh_token/token傳送POST請求,獲得新的access_token

聽起來有些彎彎繞繞?,其實本質上就是根據OAuth 2.0 標準協議,把服務商的資源,來授權給第三方應用訪問,常見於單點登入場景(SSO)

理論差不多是上面這些,還有看不懂的小夥伴可以搜有一下OAuth2.0協議,接下來我們開始寫程式碼了~?

編碼

首先我們的應用程式要獲得client_idclient_secret,肯定不能明文寫在程式碼裡,我們可以在本地終端寫入臨時的環境變數,通過node程式模組process讀取到id和祕鑰。

process 物件是一個 global (全域性變數),提供有關資訊,控制當前 Node.js 程式。作為一個物件,它對於 Node.js 應用程式始終是可用的,故無需使用 require()

Node程式怎麼寫入環境變數呢?這裡介紹兩種臨時環境變數的配置方法

配置Node環境變數

  • Windows配置 臨時cmd 檢視環境變數,新增環境變數,刪除環境變數
#檢視環境變數
set SPOTIFY_CLIENT_ID
#如果不存在則新增環境變數 
set SPOTIFY_CLIENT_ID=XXXX
set SPOTIFY_CLIENT_SECRET=YYYY
#環境變數追加值 set 變數名=%變數名%;變數內容 (你的應用路徑名)
set path=%path%;C:\web;C:\Tools 
#需要刪除環境變數,直接=後面不寫入值 
set SPOTIFY_CLIENT_ID=
set SPOTIFY_CLIENT_SECRET=
複製程式碼

Window cmd環境圖在這

記一次OAuth2.0使用者鑑權
臨時 (powershell) 檢視環境變數,新增環境變數,刪除環境變數

#檢視是否存在 
$env:SPOTIFY_CLIENT_ID  
#如果不存在則新增環境變數  
$env:SPOTIFY_CLIENT_ID="XXX" 
$env:SPOTIFY_CLIENT_SECRET="YYY" 
#環境變數追加值 
$env:path=$env:path + ";C:\web;C:\Tools"  
#刪除環境變數 del env:SPOTIFY_CLIENT_ID  
#顯示所有的環境變數 ls env:
複製程式碼

powershell圖在這

記一次OAuth2.0使用者鑑權
這裡需要特別注意的是雖然通過 CMD 和 Powershell 都能修改環境變數,在不同終端的環境變數是不能共享的,(筆者在這裡踩了很久坑TT)即你在 CMD 可以設定 SPOTIFY_CLIENT_ID="XXX",同時也可以在 Powershell 中設定 SPOTIFY_CLIENT_ID="YYY"如果你只在cmd裡設定,Node環境裡是讀不到的!? 。並且,上面的環境設定只是臨時的,只針對當前執行視窗的環境有效。當終端執行視窗關閉以後,相關設定都會丟失。

設定好後就可以在node中通過process.env.REDIRECT_URI讀取到賬戶、祕鑰資訊,接下來就是在後端寫鑑權需要的各種介面了

先上一張官網的授權流程圖,感受一下授權流程?

記一次OAuth2.0使用者鑑權

登入

  • /login登入
let redirect_uri = 
  process.env.REDIRECT_URI || 
  'http://localhost:8888/callback'

app.get('/login', function(req, res) {
  res.redirect('https://accounts.spotify.com/authorize?' +
    querystring.stringify({
      response_type: 'code',
      client_id: process.env.SPOTIFY_CLIENT_ID,
      scope: 'user-read-private user-read-email user-read-recently-played user-top-read user-follow-read user-follow-modify playlist-read-private playlist-read-collaborative playlist-modify-public',
      expires_in: 3600,
      redirect_uri
    }))
})
複製程式碼

這裡的scope看著寫,需要什麼許可權就配置什麼,許可權越多,能訪問的使用者資源就越多,使用者可以點選授權登入後可以在官網連結刪除授權。

access_token

  • callback
app.get('/callback', function(req, res) {
  let code = req.query.code || null
  let authOptions = {
    url: 'https://accounts.spotify.com/api/token',
    form: {
      code: code,
      redirect_uri,
      grant_type: 'authorization_code',
      expires_in: 3600
    },
    headers: {
      'Authorization': 'Basic ' + (new Buffer(
        process.env.SPOTIFY_CLIENT_ID + ':' + process.env.SPOTIFY_CLIENT_SECRET
      ).toString('base64'))
    },
    json: true
  }
  request.post(authOptions, function(error, response, body) {
    if(!error && response.statusCode === 200){
      var access_token = body.access_token
      var expires_in = body.expires_in
      let uri = process.env.FRONTEND_URI || 'http://localhost:3000'
      res.redirect(uri + '?access_token=' + access_token +'?expires_in=' + expires_in)
    } else {
      res.redirect(`/#${querystring.stringify({ error: 'invalid_token' })}`);
    }
  })
})
複製程式碼

若引數無誤,響應資料包格式:

若引數無誤,伺服器將返回一段JSON文字,包含以下引數:

  • access_token:要獲取的Access Token。
  • expires_in:Access Token的有效期,以秒為單位。
  • refresh_token:用於重新整理Access Token 的 Refresh Token。
  • scope:Access Token最終的訪問範圍,即使用者實際授予的許可權列表(使用者在授權頁面時,有可能會取消掉某些請求的許可權)。
  • cookies: 讓瀏覽器記住客戶端,包含session_id

這個時候就會自動跳轉到最開始設定好的redirect_url,這個時候這個url上執行著我的React應用,有了鑑權之後就可以愉快的使用使用者資訊,訪問呼叫spotify的API啦~

超時重新整理 refresh_token

refresh_token顧名思義,refresh_token就是起到重新整理token的作用,避免使用者再次點選授權進行驗證,那麼refresh_token是怎麼和access_token聯合起來使用的呢?

如果我們遇到了access_token過期了,那麼我們需要使用refresh_token去獲取一個新的access_token,聽起來很簡單,那如果refresh_token也過期了呢?這時使用者需要重新登入嗎?官網給出的解決方案很簡單,使用**refresh_token來擴充套件access_token的有效性。就是協調雙令牌

正確的做法即是兩個令牌都有自己的過期時間,因為access_token是需要使用refresh_token重新整理獲取,所以refresh_token設定的過期時間要比access_token時間長,那麼如何避免refresh_token過期呢?辦法就是我們使用refresh_token重新整理了access_token後,將POST請求傳送到Accounts服務/api/token端點,grant_type註明是 refresh_token那麼Spotify將返回新的access_token。也返回新的refresh_token,這樣兩個令牌的時間又得到了延長。這就保證了使用者在一個規定時間段只要訪問了應用,就可以享受無感知的重新整理體驗。

app.get('/refresh_token', function(req, res) {
  // requesting access token from refresh token
  const refresh_token = req.query.refresh_token;
  let authOptions = {
    url: 'https://accounts.spotify.com/api/token',
    headers: {
      'Authorization': `Basic` + (new Buffer(
          process.env.SPOTIFY_CLIENT_ID + ':' + process.env.SPOTIFY_CLIENT_SECRET
        ).toString('base64')),
    },
    form: {
      grant_type: 'refresh_token',
      refresh_token,
    },
    json: true,
  };

  request.post(authOptions, function(error, response, body) {
    if (!error && response.statusCode === 200) {
      var access_token = body.access_token;
      res.send({ access_token });
    }
  });
});
複製程式碼

axios發具體請求

因為每一次向服務端發請求,請求頭都需要帶上authorization
所以在我們的前端應用通過
window.location.href.match(/access_token=([^&]*)/)拿到token
window.location.href.match(/expires_in=([\w]*)/)[1]拿到過期時間

... 剩下需要的引數自己匹配 然後axios配置

 headers: {
    'Authorization': `Bearer ${access_token}`,
  }
複製程式碼

就可以發請求拿到資料啦~ ?

動圖演示具體流程?

記一次OAuth2.0使用者鑑權

最後

這就是我的一次OAuth2鑑權登入的完整記錄啦,(找了蠻多資料發現敘述完整流程的很少,踩了蠻多坑,所以自己動手寫了個完整的 ~)

於是乎我們就可以愉快的藉助spotify的資料二次開發啦╰(* °▽°* )╯,很感謝看到這裡的掘友,希望本文能讓你收穫些東西,本文以spotify的開放API舉例,不玩spotify也沒關係,相信大家也能從本文了解一下OAth2的鑑權流程,碼字不易,希望你喜歡?

本文部分截圖來自

spotify developer官網

相關文章