基於Nuxt實現Token過時自動刷新

功能概述

項目使用先後的分離的開發模式,後端使用Spring Security實現基於Jwt的用戶認證模式,數據交互使用Json格式。前端使用Nuxt框架實現服務端渲染(SSR)功能,使用Vuex實現登陸狀態存儲,使用@nuxtjs/axios插件加載數據。用戶登陸後就會一直處於登陸狀態,除非用戶主動登出或連續7天未訪問網站纔會要求從新登陸。前端

後端大致流程

  1. 用戶經過瀏覽器輸入帳號密碼進行登陸
  2. 後臺java程序認證成功後經過Http Header 返回 accessTokenrefreshTokentokenRefreshed(boolean)
  3. accessToken 有效期2小時,主要用於訪問須要認證受權的接口(若是有效期太長,有期限其用戶權限等信息發生變化後沒法及時反映到token中)
  4. refreshToken 有效期7天,其惟一的做用就是再accessToken過時後,用戶能夠在不用從新登陸的狀況下換取新的accessToken
  5. tookenRefreshed 告知客戶端Token是否已刷新,若是爲true客戶端必須存儲新的Token
  6. 後臺每次收到Jwt的認證請求時都會判斷accessToken是否快過時(此時的accessToken還未過時,仍是有效的,若是過時了就會發送認證失敗的Response給客戶端)。若是快過時了,將自動建立新的accessTokenrefreshToken放入Http Header中,隨本次請求的結果一塊兒返回給客戶端

前端方案

在介紹前端方案前,先簡單說下用戶訪問須要權限認證的兩種不一樣狀況:vue

一、用戶直接在瀏覽器地址欄中輸入連接或點擊一個普通 <a> 標籤的連接。
二、用戶點擊<nuxt-link>方式構建的連接java

第一種狀況的 Http 請求由瀏覽器自動構建,首先發送到部署Nuxt的Node服務器上(SSR的Server端),而後再Server端構建Nuxt及Vuex相關對象,此時是獲取不到保存再客戶端(瀏覽器)中Token信息的。瀏覽器在未收到響應以前,瀏覽器中沒有任何Nuxt或Vuex相關的實例對象(不能進行任何JS操做)。此時若是想攜帶保存在客戶端的Token信息,只能經過Cookie實現(瀏覽器在發送Http請求時會自動帶上客戶端的Cookie信息)ios

第二種狀況的路由跳轉是在客戶端進行的,真正發送HTTP請求通常都是在程序中經過Axios構建,而後再發送到部署Nuxt的Node服務端。所以,在發送請求前能夠方便獲取到VuexLocalstorageCookie等任何位置保存的Token信息,而後添加到Request中發送到Server端。vuex

這兩種狀況的主要區別在於,如何攜帶認證所需的Token。這兩種狀況是下面兩種方案都要考慮的,因爲第二種狀況限制少,主要考慮第一種狀況中的限制。axios

方案一 使用 Nuxt 提供的middleware功能實現

中間件(middleware)容許定義一個自定義函數運行在一個頁面或一組頁面渲染以前,所以,能夠在每次訪問頁面前都先判斷accessToken是否已過時,若是已過時,則刷新token。 middleware 的具體用法可參考官方文檔。後端

  1. 用戶在瀏覽器中執行登陸認證後,經過Axios的Response攔截器將獲取到的 accessTokenrefreshToken, 存儲在Vuex中。
  2. 使用vuex-persistedstate將Vuex中的Token信息持久化到Cookie中,且只能存在Cookie中。不然沒法解決上面第一種狀況中的限制。
  3. 建立一個refreshToken.js的 middleware 配置在須要Token認證的頁面(能夠全局配置,也能夠單獨配置某些頁面)
  4. 在Nuxt的頁面組件中提供的syncDatafetch方法中執行加載數據的請求

核心代碼以下:api

第一步:建立refreshToken中間件,並配置瀏覽器

// refreshToken.js

import { decode } from 'js-base64';
import {isEmpty} from "@/plugins/common-util";

// 距離token過時時間提早2分鐘刷新token,防止客戶端與服務端時間差
const DISTANCE_EXP_TIME = 2 * 60;

export default async function ({store, app, req}){

    //一、獲取cookie或vuex中的accessToken
    let accessToken = '';
    if(process.server){ //這種就是直接再瀏覽器中輸入url的,再服務端進行刷新token的狀況
        if(isEmpty(req.headers.Authorization)){
            let cookie = req.headers.cookie
            if(cookie != null && cookie !== '' && cookie){
                cookie = cookie.split('=')
                if(cookie.length === 2){
                    let cookieValue = JSON.parse(decodeURIComponent(cookie[1]))
                    accessToken = cookieValue.user.accessTokenStr;
                }
            }
        } 
   }else { //這種客戶端渲染的狀況瀏覽器中有完整的VUE VUEX之類的js對象,能夠直接獲取
      accessToken = store.state.user.accessTokenStr
   }
   
   //二、判斷是否須要刷新token
   if(needRefreshToken(accessToken)){
      
      //三、刷新token
      let bundle = await app.$userSecurity.refreshToken()
      // 此處和axios插件中任選一個地方更新token便可
      //store.commit('user/setToken', bundle);
   }else {
      console.log('--->> 不須要刷新token')
   }
}

// 判斷accessToken是否須要刷新
function needRefreshToken(accessToken){
    if(accessToken){
        let payload = accessToken.split('.')[1]
        payload = decode(payload)
        payload = JSON.parse(payload)
        let exp = payload.exp
        let time = Math.round(new Date().getTime()/1000)
        if((exp - time) <= DISTANCE_EXP_TIME){
            return true
        }
    } 
    return false
}
// nuxt.config.js中全局配置 refreshToke 中間件,
// 全局配置後每一個頁面組件渲染前都會執行 refreshToken中間件
router: {
    middleware: 'refreshToken'
}

第二步:再@nuxtjs/axios插件的Response攔截器中處理HttpResponse中攜帶的新token服務器

import {isEmpty} from "./common-util";
export default function ({ app, $axios, store, req, redirect, route }) {
     // 基本配置
     $axios.baseUrl = process.env.apiBasePath;
     $axios.defaults.timeout = 3000000
     $axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
     $axios.defaults.headers.ClientType = 'PC'
     
     // 請求回調
     $axios.onRequest(config => {
         console.log('send request to: ', config.url)
         setToken(req, $axios, store, config)
     })
     
     // 返回回調
     $axios.onResponse(response => {
         updateToken(response, store)
     })
     
     // 請求失敗時的默認操做
     $axios.onError(error => {

     })
}

/**
 * 給每一個請求頭中加入token
 */
 const setToken = function (req, $axios, store, config) {
     // SSR的Server端執行設置token
     if(process.server){

         let accessToken = store.state.user.accessTokenStr;
         if(accessToken && config.url !== 'refresh/token'){
         
            // 若是進入此處,應該是Server端執已經執行了刷新Token的操做(refreshToken中間件中執行的)
            // Server端的Vuex中已經有新的Token值,再Server端渲染完成後,
            // Nuxt會將Vuex中新的Token值隨着Response一塊兒傳遞到客戶端(瀏覽器中)
            config.headers.Authorization = accessToken
            
         }else if(isEmpty(config.headers.Authorization)){
             
             // 若是進入此處,應該就是在瀏覽器中輸入直接輸入了URL,瀏覽器構建了普通的HttpRequest
             // 直接發送到了Nuxt部署的NodeServer中,此時從Vuex中是獲取不到Token數據的
             // 所以Request Header中的Authorization 是空的,
             // 只有原始HttpRequest攜帶的Cookie中有Token數據
             let cookie = req.headers.cookie
             if(cookie != null && cookie !== '' && cookie){
                 cookie = cookie.split('=')
                 if(cookie.length === 2){
                     let cookieObj = JSON.parse(decodeURIComponent(cookie[1]))
                     // 若是是刷新token的請求,使用refreshToken,其餘的請求使用accessToken
                     let token = config.url === 'refresh/token' ? cookieObj.user.refreshTokenStr : cookieObj.user.accessTokenStr;
                     if(token){
                        config.headers.Authorization = token
                     }
                 } 
             } 
         } 
     }else { //SSR的客戶端設置token
     
         //console.log('----->> client 設置header', store.state.user.momentTokenStr)
         let token = config.url === 'refresh/token' ? store.state.user.refreshTokenStr : store.state.user.accessTokenStr
         if(token){
            config.headers.Authorization = token
         }
    }
}

/**
 * Request攔截器中執行
 * 將HttpResponse Header中攜帶的Token信息保存到Vuex中
 */
const updateToken = function (response, store) {

    let isRefreshed = response.headers.tokenrefreshed
    let accessToken = response.headers.authorization
    let refreshToken = response.headers.refreshtoken

    if(isRefreshed === "true" && !isEmpty(accessToken) && !isEmpty(refreshToken)){
        store.commit('user/setAccessToken', accessToken);
        store.commit('user/setRefreshToken', refreshToken);
    }else {
        console.log("---->>> 沒有重置token")
    }
}

第三步:持久化Vuex中的Token值到Cookie,此處使用vuex-persistedstate插件

// vuex-persist.js
import createPersistedState from "vuex-persistedstate";
import * as Cookies from "js-cookie";
import {isEmpty} from "@/plugins/common-util";

const KEY = 'youselfKey';

export default ({store}) => {
 
    // 因爲Server端相關操做致使Vuex中狀態發生變化後,nuxt會經過window.__NUXT__返回給瀏覽器(客戶端)
    // 所以在客戶端是能取到Vuex中變化後的值(此時的值是在內存中),
    // 先另存Server端中修改的過的值,不然在createPersistedState執行後會被覆蓋
    let serverSideAccessTokenStr = store.state.user.accessTokenStr
    let serverSideRefreshTokenStr = store.state.user.refreshTokenStr
    let serverSideMomentTokenStr = store.state.user.momentTokenStr

    // vuex-persistedstate插件的原理應該是監聽store的Commit操做,且因爲vuex-persistedstate插件只支持在客戶端運行
    // 所以,若是是在Server端進行刷新Token保存在Vuex中的操做,vuex-persistedstate是監聽不到的,
    // 即更新後的Token值不會被持久化到Cookie中,解決方法就是在客戶端從新Commit一下
    createPersistedState({
        key: KEY,
        paths: [
            'user.accessTokenStr',  // 前面加 user. 是由於accessTokenStr存在user模塊下
            'user.refreshTokenStr'
        ],
        storage: {
            getItem: (key) => Cookies.get(key),
            // secure: true 表示只有在https狀況下才會發送cookie,不要隨意加
            setItem: (key, value) => Cookies.set(key, value, { expires: 7/*, secure: true*/}),
            removeItem: (key) => Cookies.remove(key),
        }
    })(store)
    
    // 從新將Server中更新的Token值Commit一下,讓插件監聽到值的變化後進行自動保存
    if(!isEmpty(serverSideAccessTokenStr)){
        store.commit('user/setAccessToken', serverSideAccessTokenStr)
    } if(!isEmpty(serverSideRefreshTokenStr)){
        store.commit('user/setRefreshToken', serverSideRefreshTokenStr)
    } if(!isEmpty(serverSideMomentTokenStr)){
        store.commit('user/setRefreshToken', serverSideMomentTokenStr)
    }
}
// nuxt.config.js中配置 vuex-persist 插件,必定要配置成客戶端模式
plugins: [
    // ssr: false 是指定該插件只在客戶端運行
    {src: '~/plugins/vuex-persist', ssr: false},
    // 新的寫法
    //{src: '~/plugins/vuex-persist', mode: "client"}
],

第四步:頁面中使用Nuxt提供的生命週期函數asyncData或fetch中加載數據

<!-- 文章詳情頁,加載文章數據 -->
<template>
    ...
</template>
<script>
export default {
    async asyncData({app, params}) {

        // 此處是將全部獲取數據的接口分鐘到了Api模塊中
        // 原生寫法 this.$axios.$get(`articles/${id}`, {params: { extra: true }})
        let article = await app.$article.getArticleDetail(aid, true)
        return {
            article
        }
    }
}
</script>

方案一小結:因爲Nuxt的 middleware 只在Server端執行,所以,方案一隻能在Server端出現Token過時時自動刷新Token。若是是在客戶端(瀏覽器)中獲取數據時發生Token過時則不會自動刷新。全部,方案一不夠完美,沒有完全解決問題。

方案二 使用 Axios 的攔截器實現

方案二目前只是個簡單思路,因爲對@nuxtjs/axios 以及ES6的異步功能理解的不是很透徹,暫時未能實現。

大概思路就是經過 axios 的 Request 和 Response 攔截器來實現。因爲跟後端(java)接口交互獲取數據,都是經過axios插件完成的,所以每次發送請求時均可以攔截並執行響應邏輯。

用Request攔截器實現,其實就是將方案一種在 middleware 中判斷token過時的操做移到 Request 攔截器中,每次發送獲取數據的請求都先判斷Token是否過時,若是過時就行先刷新Token,而後再繼續本次的請求。根據目前的嘗試結果,沒法保證刷新token的請求執行完成後再執行本次請求。

用Response攔截器實現,是再攔截到後端響應的Token過時的錯誤後,先不返回,直接再攔截器中刷新token,從新執行本次請求後,將最新的Respon結果返回。根據目前嘗試的結果,只實現到刷新Token後從新執行本次請求,沒法將最新的請求結果返回給頁面中的調用處。

相關文章
相關標籤/搜索