性能概述

我的项目应用前后的拆散的开发模式,后端应用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中,随本次申请的后果一起返回给客户端

前端计划

在介绍前端计划前,先简略说下用户拜访须要权限认证的两种不同状况:

1、用户间接在浏览器地址栏中输出链接或点击一个一般 <a> 标签的链接。
2、用户点击<nuxt-link>形式构建的链接

第一种状况的 Http 申请由浏览器主动构建,首先发送到部署Nuxt的Node服务器上(SSR的Server端),而后再Server端构建Nuxt及Vuex相干对象,此时是获取不到保留再客户端(浏览器)中Token信息的。浏览器在未收到响应之前,浏览器中没有任何Nuxt或Vuex相干的实例对象(不能进行任何JS操作)。此时如果想携带保留在客户端的Token信息,只能通过Cookie实现(浏览器在发送Http申请时会主动带上客户端的Cookie信息)

第二种状况的路由跳转是在客户端进行的,真正发送HTTP申请个别都是在程序中通过Axios构建,而后再发送到部署Nuxt的Node服务端。因而,在发送申请前能够不便获取到VuexLocalstorageCookie等任何地位保留的Token信息,而后增加到Request中发送到Server端。

这两种状况的次要区别在于,如何携带认证所需的Token。这两种状况是上面两种计划都要思考的,因为第二种状况限度少,次要思考第一种状况中的限度。

计划一 应用 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办法中执行加载数据的申请

外围代码如下:

第一步:创立refreshToken中间件,并配置

// refreshToken.jsimport { 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}){    //1、获取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   }      //2、判断是否须要刷新token   if(needRefreshToken(accessToken)){            //3、刷新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.jsimport 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后从新执行本次申请,无奈将最新的申请后果返回给页面中的调用处。