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
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM