功能概述项目使用前后分离开发模式,后端使用SpringSecurity实现基于Jwt的用户认证方式,数据交互使用Json格式。前端使用Nuxt框架实现服务端渲染(SSR)功能,使用Vuex实现登录状态存储,使用@nuxtjs/axios插件加载数据。用户登录后,将一直处于登录状态,除非用户主动退出或连续7天不访问网站,否则将被要求重新登录。后台的大致流程。用户通过浏览器输入账号密码登录后台java程序。认证成功后通过HttpHeader返回accessToken、refreshToken、tokenRefreshed(boolean)。,用户权限等信息发生变化不能及时反映在token中时有时间限制)refreshToken有效期为7天,唯一作用是accessToken过期后,用户可以换取新的accessToken无需重新登录。Refreshed通知客户端Token是否已经刷新,如果为真,则客户端必须存储一个新的Token。后台每次收到Jwt认证请求,都会判断accessToken是否即将过期(此时accessToken还没有过期,仍然有效,如果过期,会发送认证失败的响应给客户端).如果即将过期,会自动创建一个新的accessToken和refreshToken,放入HttpHeader中,连同本次请求的结果一起返回给客户端。不同情况:1、用户直接在浏览器地址栏输入链接或点击普通标签中的链接。2.用户点击构建的链接。第一种情况,Http请求由浏览器自动构建,首先发送到部署Nuxt的Node服务器(SSR的服务器端),然后在服务器端构建Nuxt和Vuex相关对象,此时获取不到客户端(浏览器)保存的Token信息。在浏览器收到响应之前,浏览器中没有Nuxt或Vuex相关的实例对象(无法进行JS操作)。这时如果要携带客户端保存的Token信息,只能通过Cookie来实现(浏览器在发送Http请求时会自动带上客户端的Cookie信息)。第二种情况,路由跳转是在客户端进行的。发送HTTP请求一般是在程序中通过axios构建,然后发送到部署Nuxt的Node服务器。因此,在发送请求之前,可以很方便的获取Vuex、Localstorage、Cookie等任意位置保存的Token信息,然后添加到Request中发送给Server。两种情况的主要区别在于如何携带认证所需的Token。下面两种方案都要考虑这两种情况。由于第二种情况的限制较少,所以主要考虑第一种情况的限制。解决方案1??使用Nuxt提供的中间件函数实现中间件(middleware)允许定义一个自定义函数在渲染一个页面或一组页面之前运行。因此可以在每次访问页面前判断accessToken是否过期。如果过期,请刷新令牌。中间件的具体使用请参考官方文档。用户在浏览器中进行登录认证后,获取到的accessToken和refreshToken通过axios的Response拦截器存储到Vuex中。使用vuex-persistedstate将Vuex中的Token信息持久化到Cookie中,并且只能存在于Cookie中。否则无法解决上述第一种情况的限制。创建一个refreshToken.js中间件,并配置在需要Token认证的页面上执行Nuxt页面组件提供的syncData或fetch方法中加载数据的请求(可全局配置,也可单独配置)。核心代码如下:第一步:创建refreshToken中间件并配置//refreshToken.jsimport{decode}from'js-base64';import{isEmpty}from"@/plugins/common-util";//刷新2分钟在token过期时间token之前,防止客户端和服务端的时间差constDISTANCE_EXP_TIME=2*60;导出默认异步函数({store,app,req}){//1.vuex中获取cookie或accessTokenletaccessToken='';if(process.server){//这是在浏览器直接输入url,在服务端刷新token的情况if(isEmpty(req.headers.Authorization)){letcookie=req.headers.cookieif(cookie!=null&&cookie!==''&&cookie){cookie=cookie.split('=')if(cookie.length===2){letcookieValue=JSON.parse(decodeURIComponent(cookie[1]))accessToken=cookieValue.user.accessTokenStr;}}}}else{//这种客户端渲染的情况下,浏览器中有完整的VUE、VUEX等js对象,可以直接获取accessToken=store.state.user.accessTokenStr}//2.判断是否需要刷新tokenif(needRefreshToken(accessToken)){//3.刷新令牌letbundle=awaitapp.$userSecurity.refreshToken()//here你可以在axios插件的任何地方更新令牌//store.commit('user/setToken',bundle);}else{console.log('--->>Noneedtorefreshthetoken')}}//判断accessToken是否需要刷新functionneedRefreshToken(accessToken){if(accessToken){letpayload=accessToken.split('.')[1]payload=decode(payload)payload=JSON.parse(payload)letexp=payload.explettime=Math.round(newDate().getTime()/1000)if((exp-time)<=DISTANCE_EXP_TIME){returntrue}}returnfalse}//全局配置refreshToke在nuxt.config.js中Components,//全局配置后,每个页面组件在渲染前都会执行refreshToken中间件router:{middleware:'refreshToken'}第二步:在@nuxtjs/axios插件的Response拦截器中处理HttpResponse携带的新tokenimport{isEmpty}来自“./common-util”;导出默认函数({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('sendrequestto:',config.url)setToken(req,$axios,store,config)})//返回回调$axios.onResponse(response=>{updateToken(response,store)})//请求失败时的默认动作$axios.onError(error=>{})}/***为每个请求头添加一个token*/constsetToken=function(req,$axios,store,config){//SSR服务器执行设置tokenif(process.server){letaccessToken=store.state.user.accessTokenStr;if(accessToken&&config.url!=='refresh/token'){//这里输入的话,应该是服务端执行了刷新Token的操作(在refreshToken中间件中执行)//Vue在服务端边x中有一个新的Token值,在服务器端渲染后,//Nuxt会将Vuex中的新Token值连同Response一起传递给客户端(在浏览器中)config.headers.Authorization=accessToken}elseif(isEmpty(config.headers.Authorization)){//这里如果输入,应该直接在浏览器输入url,浏览器会构建一个普通的HttpRequest//直接发送给N??uxt部署的NodeServer,此时time无法从Vuex获取Token数据//所以RequestHeader中的Authorization为空,//只有原来的HttpRequest在Cookie中携带了Token数据letcookie=req.headers.cookieif(cookie!=null&&cookie!==''&&cookie){cookie=cookie.split('=')if(cookie.length===2){letcookieObj=JSON.parse(decodeURIComponent(cookie[1]))//如果刷新Token请求,使用refreshToken,其他请求使用accessTokenlettoken=config.url==='refresh/token'?cookieObj.user.refreshTokenStr:cookieObj.user.accessTokenStr;如果(令牌){config.headers.Authorization=token}}}}}else{//SSR客户端设置token//console.log('----->>客户端设置header',store.state.user.momentTokenStr)lettoken=config.url==='刷新/令牌'?store.state.user.refreshTokenStr:store.state.user.accessTokenStrif(token){config.headers.Authorization=token}}}/***请求拦截在浏览器中执行*保存HttpResponseHeader中携带的Token信息到Vuex*/constupdateToken=function(response,store){letisRefreshed=response.headers.tokenrefreshedletaccessToken=response.headers.authorizationletrefreshToken=response.headers.refreshtokenif(isRefreshed==="true"&&!isEmpty(accessToken)&&!isEmpty(refreshToken)){store.commit('user/setAccessToken',accessToken);store.commit('user/setRefreshToken',refreshToken);}else{控制台e.log("---->>>didnotresettoken")}}第三步:将Vuex中的Token值持久化到Cookie中,这里使用vuex-persistedstate插件//vuex-persist.jsimportcreatePersistedStatefrom"vuex-persistedstate";import*asCookiesfrom"js-cookie";import{isEmpty}from"@/plugins/common-util";constKEY='youselfKey';exportdefault({store})=>{//之后Vuex中的状态由于服务端的相关操作发生了变化,nuxt会通过window.__NUXT__返回给浏览器(客户端)//因此,在客户端可以获取到Vuex中变化的值(此时的值为inmemory),//将修改后的值先保存在服务器上,否则createPersistedState执行后会被覆盖.user.momentTokenStr//vuex-persistedstate插件的原理应该是监听Commitostore的peration,并且由于vuex-persistedstate插件只支持在client端运行//所以,如果是在server端刷新,Token存储在Vuex中,vuex-persistedstate的运行无法监控,//即更新后的Token值不会持久化到cookie中,解决方法是在客户端重新提交createPersistedState({key:KEY,paths:['user.accessTokenStr',//在前面添加user因为accessTokenStr存在于用户模块'user.refreshTokenStr'],storage:{getItem:(key)=>Cookies.get(key),//secure:true表示仅在这种情况下的httpscookie只会在下次发送后,不要添加setItem:(key,value)=>Cookies.set(key,value,{expires:7/*,secure:true*/}),removeItem:(键)=>饼干。remove(key),}})(store)//在服务端再次提交更新后的Token值,让插件在监听到值变化后自动保存if(!isEmpty(serverSideAccessTokenStr)){store.commit('user/setAccessToken',serverSideAccessTokenStr)}if(!isEmpty(serverSideRefreshTokenStr)){store.commit('user/setRefreshToken',serverSideRefreshTokenStr)}if(!isEmpty(serverSideMomentTokenStr)){store.commit('user/setRefreshTokenStr',serverenSideTokenStr)MomentTok}}//在nuxt.config.js中配置vuex-persist插件,必须配置为客户端模式plugins:[//ssr:false指定插件只在客户端运行{src:'~/plugins/vuex-persist',ssr:false},//新的写法//{src:'~/plugins/vuex-persist',mode:"client"}],第四步:使用Nuxt提供的生命周期函数asyncData或者fetch在页面加载数据template>...方案总结:由于Nuxt的中间件只在服务端执行,方案一只能在服务端token过期时自动刷新token。如果在客户端(浏览器)获取数据时token过期,则不会自动刷新。因此,第一种方案并不完美,没有彻底解决问题。方案二使用axios拦截器实现方案二目前只是一个简单的思路。由于对@nuxtjs/axios的理解不完全以及ES6的异步功能,暂时没有实现。大致思路是通过axios的Request和Response拦截器来实现。由于通过axios插件与后台(java)接口交互获取数据,所以每次发送请求时都可以拦截并执行响应逻辑。用Request拦截器来实现,其实就是把中间件中判断token过期的操作移到了Request拦截器中。每次发送获取数据的请求时,首先判断Token是否过期。如果过期了,先刷新Token,然后Continue这个请求。根据目前的试用结果,无法保证在执行此请求之前执行刷新令牌的请求。用Response拦截器实现,拦截到后端响应的Token过期错误后,不先返回,直接在拦截器中刷新token,重新执行请求后返回最新的Response结果。从目前尝试的结果来看,只能在刷新Token后重新执行本次请求,而不能将最新的请求结果返回给页面中的调用处。