項目使用先後的分離的開發模式,後端使用Spring Security實現基於Jwt
的用戶認證模式,數據交互使用Json
格式。前端使用Nuxt框架實現服務端渲染(SSR
)功能,使用Vuex
實現登陸狀態存儲,使用@nuxtjs/axios
插件加載數據。用戶登陸後就會一直處於登陸狀態,除非用戶主動登出或連續7天未訪問網站纔會要求從新登陸。前端
accessToken
、refreshToken
、tokenRefreshed(boolean)
accessToken
有效期2小時,主要用於訪問須要認證受權的接口(若是有效期太長,有期限其用戶權限等信息發生變化後沒法及時反映到token中)refreshToken
有效期7天,其惟一的做用就是再accessToken過時後,用戶能夠在不用從新登陸的狀況下換取新的accessToken
tookenRefreshed
告知客戶端Token是否已刷新,若是爲true客戶端必須存儲新的TokenJwt
的認證請求時都會判斷accessToken
是否快過時(此時的accessToken
還未過時,仍是有效的,若是過時了就會發送認證失敗的Response給客戶端)。若是快過時了,將自動建立新的accessToken
、refreshToken
放入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服務端。所以,在發送請求前能夠方便獲取到Vuex
、Localstorage
、Cookie
等任何位置保存的Token信息,而後添加到Request
中發送到Server端。vuex
這兩種狀況的主要區別在於,如何攜帶認證所需的Token
。這兩種狀況是下面兩種方案都要考慮的,因爲第二種狀況限制少,主要考慮第一種狀況中的限制。axios
middleware
功能實現中間件(middleware)容許定義一個自定義函數運行在一個頁面或一組頁面渲染以前,所以,能夠在每次訪問頁面前都先判斷accessToken
是否已過時,若是已過時,則刷新token。 middleware 的具體用法可參考官方文檔。後端
Axios
的Response攔截器將獲取到的 accessToken
、refreshToken
, 存儲在Vuex中。vuex-persistedstate
將Vuex中的Token信息持久化到Cookie
中,且只能存在Cookie
中。不然沒法解決上面第一種狀況中的限制。refreshToken.js
的 middleware 配置在須要Token認證的頁面(能夠全局配置,也能夠單獨配置某些頁面)syncData
或fetch
方法中執行加載數據的請求核心代碼以下: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過時則不會自動刷新。全部,方案一不夠完美,沒有完全解決問題。
方案二目前只是個簡單思路,因爲對@nuxtjs/axios 以及ES6的異步功能理解的不是很透徹,暫時未能實現。
大概思路就是經過 axios 的 Request 和 Response 攔截器來實現。因爲跟後端(java)接口交互獲取數據,都是經過axios插件完成的,所以每次發送請求時均可以攔截並執行響應邏輯。
用Request攔截器實現,其實就是將方案一種在 middleware 中判斷token過時的操做移到 Request 攔截器中,每次發送獲取數據的請求都先判斷Token是否過時,若是過時就行先刷新Token,而後再繼續本次的請求。根據目前的嘗試結果,沒法保證刷新token的請求執行完成後再執行本次請求。
用Response攔截器實現,是再攔截到後端響應的Token過時的錯誤後,先不返回,直接再攔截器中刷新token,從新執行本次請求後,將最新的Respon結果返回。根據目前嘗試的結果,只實現到刷新Token後從新執行本次請求,沒法將最新的請求結果返回給頁面中的調用處。