项目使用先后的分离的开发模式,后端使用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后从新执行本次请求,没法将最新的请求结果返回给页面中的调用处。