前端自動刷新令牌

前端自動刷新令牌

前言

1. 技術選型

咱們在實際項目中選用了JWT這種認證方式.前端

  1. 簡單瞭解JWTios

    JWTJson Web Token, 用戶登錄後, 將非私密用戶信息放置在token中攜帶給前端並加密, 以後每一筆請求攜帶token, 後端解密token便可取得用戶信息axios

    最大好處: 後端無狀態, 能夠平滑橫向擴張, 且token較難解密後端

    最大弊端: 後端較難控制token失效跨域

    更多詳情能夠在掘金進行搜索: JWT瀏覽器

  2. 是否須要前端刷新令牌?併發

    其實並非必定的測試

    • 當前端後端同域下, 選用Cookie+HttpOnly進行令牌傳遞, 可讓前端無需操做任何令牌, 當令牌過時後主動由後端刷新並放回Cookie便可
    • (咱們項目)當跨域狀況下, (實際測試中)後端在Set-Cookie時, Chrome瀏覽器會發生沒法正常處理Cookie的問題, 所以放棄Cookie傳遞, 使用請求頭攜帶token

      第一張圖是Chrome, 第二張圖是FireFox, 相同請求網站

    Chrome

    FireFox

    • 所以選用localStorage存儲token, 由前端處理放在請求頭中進行認證ui

    • 最後一個問題: 因爲token有效期短, 須要有人刷新token

    小插曲: 其實能夠交給後端刷新令牌, 當token過時後, 後端刷新而後放回請求頭, 前端主動根據返回請求頭進行更新便可. 問題在後端遇到併發時token會混亂.

    最終緣由由前端刷新: 吵不事後端, 只能接下需求_(:з」∠)_

  3. http請求插件 不過多介紹, 選用了axios

  4. axios使用介紹 主要使用了攔截器interceptors, 因爲使用了Promise, 能夠隨意在請求先後進行各類延時操做.

功能點介紹

  1. 登錄後將登錄token儲存

  2. axios請求前將全部token放入請求頭

  3. 退出登陸時清空token存儲

  4. 當任意接口返回401時, 嘗試刷新token, 若成功, 則更新token儲存, 若失敗則跳轉登錄

  5. 因爲併發的存在, 須要考慮如下狀況:

    • 發起一筆請求時, 若已經在嘗試刷新token, 將此請求攔截, 並在成功刷新後, 更新請求頭並從新發送
    • 當收到一筆401請求時, 若已經在嘗試刷新token, 將此請求攔截, 並在成功刷新後, 更新請求頭並從新發送
  6. 加載頁面時, 嘗試加載token

轉化爲代碼邏輯

  1. 登陸後,將token存儲進localStorage, 並更新全部axios實例的默認請求頭

  2. null

  3. 退出登陸時, 將localStoragetoken清除, 並清除全部axios實例的默認請求頭

  4. axios請求失敗且狀態碼爲401

    • 攔截該筆請求, 並註冊一個刷新Token完成事件
      • 當觸發token刷新成功時, 將該筆請求更新token, 而後從新發送
      • 當觸發token刷新失敗時, 將該筆請求返回失敗, 交由Promise:reject進一步處理
    • 在沒有其餘刷新token請求時, 嘗試刷新token, 並在刷新後觸發刷新Token完成事件, 若刷新成功, 則將token存儲進localStorage, 並更新全部axios實例的默認請求頭
  5. axios請求前, 檢查是否正在刷新token, 若正在刷新token, 註冊一個刷新Token完成事件

    • 當觸發token刷新成功時, 將該筆請求更新token, 而後繼續發送
    • 當觸發token刷新失敗時, 將該筆請求返回失敗, 拋棄請求
  6. 加載頁面時, 從localStorage中獲取token

實際代碼

  1. 私有全局變量

    • isRefreshing: Boolean 是否正在刷新token
    • RefreshEvent: EventEmitter 刷新事件分發器
    • instances: AxiosInstance[] 全部封裝好的axios實例,

      注意最好默認包含Axios即默認實例

    • REFRESH_URL: String 刷新URL
  2. 封裝方法

    • 設置並返回token
    function setRefreshToken(refreshToken) {
      if (refreshToken !== undefined) {
        // 若攜帶參數, 則塞入localStorage中更新
        localStorage.setItem('ompJwtRefreshToken', refreshToken)
      } else {
        // 若沒有攜帶參數, 則從localStorage中加載, 注意防範XSS攻擊
        refreshToken = (
          localStorage.getItem('ompJwtRefreshToken', refreshToken) || ''
        ).replace(/[^.\-_a-zA-Z0-9]/g, '')
      }
      // 這裏instances包含全部axios實例
      instances.forEach(instance => {
        // 設置默認請求頭
        instance.defaults.headers['x-client-refresh-token'] = refreshToken
      })
      return refreshToken
    }
    複製代碼
    • 刷新token

    插播小廣告: 個人掘金主頁

    function tryToRefreshToken() {
      // 約全局變量isRefreshing: 是否正在刷新token
      isRefreshing = true
      let refreshToken = localStorage.getItem('ompJwtRefreshToken') || ''
      refreshToken = refreshToken.replace(/[^.\-_a-zA-Z0-9]/g, '')
      // 沒有refreshToken是沒法刷新token的, 直接失敗
      // 這裏使用事件分發機制處理, 返回false表示刷新失敗
      if (!refreshToken) return RefreshEvent.emit('refreshEnd', false)
      // ^_^ 友好提示, 防止用戶覺得刷新中點擊按鈕沒有反應
      Notice.open({
        title: '正在主動刷新, 嘗試繼續登錄中……',
        duration: 0,
        name: 'refresh'
      })
      axios
        .get(REFRESH_URL)
        .then(res => {
          // 刷新成功
          Notice.close('refresh')
          Notice.open({
            title: '刷新成功, 將自動繼續您以前的操做~'
          })
          // 注意觸發事件
          RefreshEvent.emit('refreshEnd', true)
        })
        .catch(e => {
          // 刷新失敗
          RefreshEvent.emit('refreshEnd', false)
          Notice.close('refresh')
          Notice.open({
            title: '刷新失敗, 將自動爲您跳轉登陸頁'
          })
          router.push({name: LOGIN_PAGE})
          return Promise.reject(e)
        })
    }
    複製代碼
    • 請求前攔截器
    function preRequestInterceptor(config) {
      // 當正在刷新token時, 延時請求, 直到刷新完成
      if (isRefreshing && config.url !== REFRESH_URL) {
        // 經過返回Promise進行延遲操做
        return new Promise((resolve, reject) => {
          // 註冊事件
          RefreshEvent.once('refreshEnd', result => {
            // 注意resolve(config)才能繼續請求
            // 注意config中已經包含舊的token了, 而且不會自動刷新, 須要手動從新設置下
            if (result) {
                config.headers['x-client-refresh-token'] = setRefreshToken()
                resolve(config)
            }
            // 這裏建議reject封裝後的東西, 不然會出現reject形式不一致
            else reject(config)
          })
        })
      }
      return config
    }
    複製代碼
    • 請求後攔截器
    function errorDeal(error) {
      if (error && error.response) {
        switch (error.response.status) {
          // 通常會有其餘處理吧
          case 401:
            // 綁定事件
            // 重發事件避免重複處理(其實不會出現這種狀況)
            if (error.config._retry) return Promise.reject(error)
            // 先註冊事件!!! 再觸發重試, 不然可能會註冊失敗哦~
            const re = new Promise((resolve, reject) => {
              // 一樣註冊事件, 用於延時請求
              RefreshEvent.once('refreshEnd', result => {
                if (result) resolve(error.config)
                else reject(error)
              })
            }).then(config =>
              // 兩個注意點:
              // 1. 刷新token
              // 2. 請使用Axios.create({})出的實例, 避免此請求重複一次錯誤處理, 那樣的話就會有兩次錯誤處理
              config.headers['x-client-refresh-token'] = setRefreshToken()
              axiosRetry.request({
                ...config,
                // 綁定一些私有屬性方便大家使用
                _retry: true
              })
            )
            if (!isRefreshing) tryToRefreshToken()
            // 注意返回Promise
            return re
        }
      }
      return Promise.reject(error)
    }
    複製代碼
  3. 在各個地方觸發各類方法:

    • 私有全局
    // 定義變量喲~
    // 註冊刷新結束時間, 解除刷新態, 路由處理
    RefreshEvent.on('refreshEnd', result => {
      isRefreshing = false
      if (!result) {
        route.push({
          name: 'login'
        })
      }
    })
    // 綁定攔截器
    instances.forEach(instance => {
      instance.interceptors.request.use(preRequestInterceptor)
      instance.interceptors.response.use(undefined, errorDeal)
    })
    // 註冊重試實例(即不註冊攔截器)
    const axiosRetry = axios.create({})
    // 先觸發一下, 以便從localStorage中進行加載
    setAccessToken()
    setRefreshToken()
    複製代碼

寫在最後

分享一下工做時縹緲的想法, 沒有而後了.

若是文中出現錯誤, 還請提出喲, 我儘可能改~

插播小廣告: 個人掘金主頁

P.S. 座標: 南京, 性別: ♂, 聯繫方式: ‭41620F678‬

此文在掘金原創, 其餘網站請勿轉載.

相關文章
相關標籤/搜索