后端代码就不贴了,基于.NET5 webapi+jwt+swagger 实现的Token验证接口。
本文目的
探讨一下双Token验证设计思路以及Nuxt前端请求拦截器的封装方法。
所需插件
项目中使用到了cookie-universal-nuxt
gayhub地址:cookie-universal-nuxt
前言
nuxt请求拦截器的封装比vue的封装要复杂一些。
主要难点在于封装请求拦截的时候要对服务端渲染和客户端渲染区别对待。
当请求走的是服务端渲染时,token传递都是在请求头中进行的。
当请求走的是客户端渲染时,token传递是直接取本机cookie。
期间遇到的问题
1.当使用服务端渲染时应该使用什么容器来存放Token
2.当代码执行在服务端时,拿到了刷新成功的Token,如何传递给客户端代码
3.$cookies.set已经设置成了刷新后的Token,为什么在request里拿到的还是旧的过期的Token
4.当一个页面不止一个请求,并且同时包含服务端渲染和客户端渲染的请求时,该如何队列执行。
5.当页面不止一个请求,当第一个请求访问Token过期,并且进入刷新状态,如何处理后续请求。
最后
封装nuxt请求拦截器期间遇到了太多坑,无奈之下看了cookie-universal-nuxt和js-cookie的源码才成此文。
下面是请求拦截器代码,注释已经够详细了,代码测试环境和线上环境运行均无BUG。
1 /** 2 * 请求拦截器 3 * 名词解释 4 * accessToken:访问令牌 5 * refreshToken:刷新令牌 6 * 设计方案:用户登录成功后服务端返回访问令牌和刷新令牌,访问令牌1分钟过期,刷新令牌1小时过期 7 * 访问令牌过期后,重新把访问令牌和刷新令牌传递给接口,接口返回新的访问令牌和刷新令牌 8 * 设计初衷: 9 * 当用户停留在某个页面超过一小时未做任何操作,则会强制让用户重新登录 10 * 当用户一直保持着对网页进行操作时,则能实现永久不需要退出而刷新Token 11 */ 12 export default ({ $cookies, store, redirect, $axios, req }) => { 13 // 从全局导出类中加载RequestUrl作为$axios的baseURL 14 $axios.defaults.baseURL = store.$GlobalHelper.GlobalRequestUrl 15 $axios.defaults.timeout = 3000 16 // 表示是否正在刷新Token 17 // js中的节流阀,相当于线程锁 18 // 当正在刷新Token时,后续所有继续发来的accessToken失效的请求都应该被缓存到数组requestsCache中,直到Token刷新结束再重发请求 19 let isRefreshingTokenLock = false 20 21 // 请求缓存数组 22 // 用来存放正在刷新Token时,后续所有的请求 23 let requestsCache = [] 24 25 // 拦截请求,将accessToken插入到请求头中 26 $axios.onRequest((config) => { 27 // 这里的get要注意 28 // 如果代码在服务端执行,则是从req.headers里面拿Cookie 29 // 如果代码在客户端执行,则是从本地拿Cookie 30 // 所以在使用这句代码之前,一定要确保判断是在服务端还是客户端执行,并且确保拿到的cookie是最新的 31 const accessToken = $cookies.get('accessToken') 32 accessToken && (config.headers.Authorization = accessToken) 33 }) 34 35 // 拦截返回,当accessToken过期,则利用refreshToken进行刷新,并且缓存所有accessToken失效的请求进行重发 36 $axios.onResponse(async (response) => { 37 // 如果响应的状态码不是401,代表accessToken并未失效,则直接将response成功结果返回 38 if (response.data.status !== 401) { return Promise.resolve(response) } 39 40 // 如果正在执行刷新Token操作 41 if (isRefreshingTokenLock === true) { 42 global.console.log('正在刷新Token') 43 // 创建Promise的函数对象 44 const promise = new Promise((resolve, reject) => { 45 requestsCache.push((newCookie) => { 46 resolve($axios(response.config)) 47 }) 48 }) 49 // 直接把异步对象返回,由于该对象还没有被执行resolve函数,所以调用方使用await等待的时会一直挂起,直到resolve被执行 50 return promise 51 } 52 53 // 401表示请求过期,如果访问Token过期,则应该使用刷新Token去重新获取访问Token和刷新Token 54 if (isRefreshingTokenLock === false) { 55 // 设置节流阀,表示正在执行刷新Token 56 isRefreshingTokenLock = true 57 // 从req请求头或者本地cookie里获取Token 58 const accessToken = $cookies.get('accessToken') 59 const refreshToken = $cookies.get('refreshToken') 60 // 如果请求令牌或刷新令牌不存则直接返回 61 if (!accessToken || !refreshToken) { 62 return Promise.resolve(response) 63 } 64 // 调用远程API刷新Token 65 const object = { accessToken, refreshToken } 66 const apiTokenResult = await $axios.post('/api/Token', object) 67 // 123表示刷新Token失败,如果刷新失败,将刷新失败的结果返回 68 if (apiTokenResult.data.status === 123) { 69 return Promise.resolve(apiTokenResult) 70 } 71 72 // 将返回的新cookie保存到客户端 73 // 因为如果不是ssr,$cookies.get是从本机拿cookie 74 $cookies.set('accessToken', apiTokenResult.data.response.accessToken) 75 $cookies.set('refreshToken', apiTokenResult.data.response.refreshToken) 76 77 if (process.server) { 78 // 将返回的新的cookie保存到req.headers 79 // 因为如果是ssr,$cookies.get是从req.headers拿cookie 80 const newCookie = 'accessToken=' + apiTokenResult.data.response.accessToken + ';' + 'refreshToken=' + apiTokenResult.data.response.refreshToken 81 req.headers.cookie = newCookie 82 } 83 // 设置节流阀,表示刷新Token执行完成 84 isRefreshingTokenLock = false 85 global.console.log('令牌刷新成功') 86 87 // 下面这一步最重要,这里是循环requestsCache数组,并且调用该数组中的所有函数 88 // 当函数被调用时,就会执行resolve通知Promise执行成功,调用方的await才会继续执行 89 requestsCache.forEach((value) => { 90 value() 91 }) 92 // 清空缓存 93 requestsCache = [] 94 95 return $axios(response.config) 96 } 97 }) 98 }