OAuth2.0與前端無感知token重新整理實現
前言
OAuth是一個關於授權(authorization)的開放網路標準,在全世界得到廣泛的應用。Facebook、Twitter和Google等各種線上服務都提供了基於OAuth規範的認證機制。
OAuth一般用於面向第三方大範圍公開的API中的認證工作。換言之,假設帶有使用者註冊功能的線上服務A(例如騰訊qq)對外公開了API,線上服務B(例如百度網盤)便可使用這些線上服務A的API提供的各種功能。這種情況下,當某個已在qq裡註冊的使用者需要百度網盤的線上服務時,網盤的線上服務就會希望訪問qq來使用該使用者資訊。這時,判斷是否允許網盤使用該使用者在qq裡註冊的資訊的機制就是OAuth。
OAuth
OAuth的關鍵是,在使用百度網盤的線上服務時,使用者無需再次輸入qq的密碼。為了實現這一機制,認證過程中會通過qq提供的Web頁面,讓使用者確認是否允許訪問向百度網盤的線上服務提供qq賬戶資訊。如果尚未登入qq,則需要使用者輸入密碼,這一過程也是隻在qq裡完成登入,並不會把密碼傳送給百度網盤的線上服務。
如果通過OAuth訪問成功,網盤就可以從qq中獲取一個名為access token的令牌。通過該token,便可訪問qq中使用者允許訪問的資訊。
OAuth最主要的優點在於它是一種被廣泛認可的認證機制,並且已經實現了標準化。
OAuth2.0的認證流程
Grant Type | 作用 |
---|---|
Authorization Code | 適用於在服務端進行大量處理的web應用 |
Implicit | 適用於智慧手機應用及使用JavaScript客戶端進行大量處理的應用 |
Resource Owner Password Credential | 適用於不使用服務端的應用 |
Client Credentials | 適用於不以使用者為單位來進行認證的應用 |
其中Resource Owner Password Credential模式就是不存在網站B,客戶端直接從使用者那裡得到密碼,並從伺服器A那裡獲取access token。這一授權模式就能夠應用在公司內部所開發的客戶端應用中。
使用Resource Owner Password Credential模式進行認證時,在訪問API時需要將引數以application/x-www-form-urlencoded的形式(也就是表單的形式),進行UTF-8字元編碼後向伺服器傳送
鍵值(key) | 內容 |
---|---|
grant_type | 字串password。表示使用了Resource Owner Password Credential |
username | 登入的使用者名稱 |
password | 登入的密碼 |
scope | 指定允許訪問的許可權範圍 |
最後的scope一欄用來指定允許訪問的許可權範圍。許可權範圍的名稱可以由線上服務獨自定義,可以使用除空格、雙引號、反斜槓以外的所有ASCII文字字元。通過使用scope,就能在外部服務(線上服務B)獲取token的同時對允許訪問的範圍進行限制,還能向用戶顯示“該服務會訪問以下資訊”等提示。雖然scope不是必選項,但還是建議事先定義好。
示例:
POST /v1/oauth2/token HTTP/1.1 Host: api.example.com Authorization: Basic XXXXXXXXXXXXXXXXXXXXXXXXX Content-Type: application/x-www-form-urlencoded grant_type=password&username=zhang&password=zhang&scope=api
示例請求中還附加Authorization首部,稱為客戶端認證(Client Authorization)。它用來描述需要訪問的服務(即線上服務B)是誰。
在應用登入線上服務時,這些服務就會向其發行Client ID和Client Secret視為使用者名稱/密碼,並以Basic認證的形式經Base64編碼後放入Authorization首部。Client ID和Client Secret可以任意使用,伺服器端可以依據這些資訊識別出當前訪問的服務的應用身份。比如服務端對各個應用訪問API的次數進行限制時,或者希望遮蔽一些未經授權的應用時,就可以使用Client ID和Client Secret。
當正確的資訊送達伺服器後,伺服器端便會返回如下JSON格式的響應:
HTTP/1.1 200 OK Content-Type: application/json Cache-Control: no-store Pargma: no-cache { "access_token": 'zskldjflsdjflksjdflkjsd' "token_type": "bearer", "expires_in": 2629743, "refresh_token": 'ajsldkjflskdfjldfg' }
token_type中的bearer是RFC6750中定義的OAuth2.0所用的token型別。access_token是以後訪問時所需的access token。在以後訪問API時,只需附帶傳送該token資訊即可。這時無需再次傳送ClientID和ClientSecret資訊了。因為各個不同的客戶端都會從伺服器端得到特定的access token,即使之後沒有ClientID,服務端也同樣可以用access token資訊來識別應用身份。
根據RFC6750的定義,客戶端有3種方法將bearer token資訊傳送給伺服器端:
- 新增到請求資訊的首部
- 新增到請求訊息體
- 以查詢引數的形式新增到URL中
1、將token資訊新增到請求訊息的首部時,客戶端要用到Authorization首部,並按下面的形式指定token的內容:
GET /v1/users/ HTTP/1.1
Host: api.example.com
Authorization: Bearer zskldjflsdjflksjdflkjsd
2、token資訊新增到請求訊息體中,則需要將請求訊息裡的Content-Type設定為application/x-www-form-urlencoded,並用access_token來命名訊息體裡的引數,然後附加上tokan資訊
POST /v1/users HTTP/1.1 Host: api.example.com Context-Type: application/x-www-form-urlencoded access_token=zskldjflsdjflksjdflkjsd
3、以查詢引數的形式新增token引數時,可以在名為access_token的查詢引數後指定token資訊。
GET /v1/users?access_token=zskldjflsdjflksjdflkjsd HTTP/1.1 Host: api.example.com
access token的有效期和更新
客戶端在獲得access token的同時也會在響應資訊中得到一個名為expires_in的資料,它表示當前獲得的access token會在多少秒以後過期。當超過該指定的秒數後,access token便會過期。當access token過期後,如果客戶端依然用它訪問服務,服務端就會返回invalid_token的錯誤或401錯誤碼。
HTTP/1.1 401 Unauthorized Content-Type: application/json Cache-Control: no-store Pragma: no-cache { "error": "invaild_token" }
當發生invalid_token錯誤時,客戶端需要使用refresh token再次向服務端申請access token。這裡的refresh token時客戶端再次申請access token時需要的另一個令牌資訊,它可以和access token一併獲得。
在重新整理access token的請求裡,客戶端可以在grent_type引數裡指定refresh_token,並和refresh_token一起傳送給伺服器端。
POST /v1/oauth2/token HTTP/1.1 Host: api.example.com Authorization: Bearer zskldjflsdjflksjdflkjsd Content-Type: application/x-www-form-urlencoded grent_type=refresh_token&refresh_tokne=ajsldkjflskdfjldfg
封裝Axios實現無感重新整理token
- utils/oauth.js
const TokenKey = 'access_token' const ExpiresKey = 'expires_in' const TokenTypeKey = 'bearer' const RefreshTokenKey = 'refresh_token' export function getToken () { return localStorage.getItem(TokenTypeKey) + ' ' + localStorage.getItem(TokenKey) } export function getRefreshToken () { return localStorage.getItem(RefreshTokenKey) } export function setToken (data) { const ExpiresTime = new Date().getTime() + data.expires_in * 1000 localStorage.setItem(TokenKey, data.access_token) localStorage.setItem(ExpiresKey, ExpiresTime) localStorage.setItem(TokenTypeKey, data.token_type) localStorage.setItem(RefreshTokenKey, data.refresh_token) } export function removeToken () { localStorage.removeItem(TokenKey) localStorage.removeItem(ExpiresKey) localStorage.removeItem(TokenTypeKey) localStorage.removeItem(RefreshTokenKey) }
- request.js
import axios from 'axios' import Vue from 'vue' import { removeToken } from '@/utils/oauth' const server = axios.create({ baseURL: baseUrl, withCredentials: true }) // 用於記錄是否正在重新整理token,以免同時重新整理 window.tokenLock = false function refreshToken () { if (!window.tokenLock) { server.put('/oauth/refresh').then(({data}) => { const ExpiresTime = new Date().getTime() + data.expires_in * 1000 localStorage.setItem('access_token', data.access_token) localStorage.setItem('expires_in', ExpiresTime) localStorage.setItem('token_type', data.token_type) localStorage.setItem('refresh_token', data.refresh_token) }) window.tokenLock = true } } server.interceptors.request.use(req => { req.headers['Authorization'] = `${localStorage.getItem('token_type')} ${localStorage.getItem('access_token')}` return req }, error => { return Promise.reject(error) }) server.interceptors.response.use(rep => { // 如果距離過期時間還有10分鐘就使用refresh_token重新整理token const expiresTimeStamp = new Date(Number(localStorage.getItem('expires_in'))).getTime() - new Date().getTime() if (expiresTimeStamp < 10 * 60 * 1000 && expiresTimeStamp > 0) { if (rep.config.url.indexOf('current') < 0) { refreshToken() } } return rep }, error => { if (error.response.status === 401) { // 401錯誤:token失效或登入失敗 // 如果是在登入頁報錯的話直接顯示報錯資訊,否則清除token if (location.href.indexOf('login') > 0) { Vue.prototype.$notify.error({ title: '錯誤', message: error.response.data.message }) return } removeToken() location.reload() } return Promise.reject(error) }) export default server
作者: zhangwinwin
連結:OAuth2.0與前端無感知token重新整理實現
來源:github