浅析应用 JWT 的正确姿态
在很长的一段时间里,我都没有正确的应用 jwt,意识到这个问题之后,把我最实在的思考和总结拿进去和大家分享下,欢送一起探讨
现状
先说下我以前是怎么应用的:
- 登录胜利后,将 userId 放进 payload 生成 jwt(有效期 8 小时),而后把 jwt 发送到前端,前端存储下来
- 前端每一次拜访 API 需在 header 中携带 jwt
- 后端先解析 jwt,拿到 userId 后,数据库查问此用户的权限列表(用户-角色-权限)
- 拿到用户的权限列表后,和以后接口所需的权限进行匹配,匹配胜利返回数据,失败返回 401
jwt 规范
先来查查规范是怎么样的,首先参考了jwt.io上面对应用场景的阐明:
- Authorization 受权
- Information Exchange 信息替换
对于下面的信息,我集体的了解是两个方面:
- 容许用户拜访路由、服务和资源,在我这里就是接口所需的权限,也能够 SSO 登录,我这里目前不须要
- 能够确定以后用户的身份,在我这里就是 userId 了
优化
当初的用法有以下缺点:
- 每次调用接口都须要进行数据库查问权限(用户-角色-权限),浪费资源
- 登录胜利 8 小时后,即便用户始终在不停的应用零碎,但 jwt 还是会生效,须要从新登录
第一点好说,把权限列表也放进 payload,解析结束间接和接口所需权限进行比照。
第二点,把有效期缩短到一个星期,一个月?然而依然会产生正在用着用着 jwt 就生效了,须要从新登录的状况,工夫设置的太长也不平安,因为 jwt 自身就是无状态的,而且权限变更了怎么办,难道要等很久才失效吗,这么看来必须要刷新 jwt 了。
对应的优化点就来了:
- 把权限列表放进 payload,不必每次都去数据库查问
- 让用户无感的刷新 jwt
刷新 jwt 计划
这里参考了 stackoverflow 下面的探讨:
- jwt-refresh-token-flow
- JWT (JSON Web Token) automatic prolongation of expiration
而后我确定了我的刷新流程:
- 登录胜利后颁发两个 token:accessToken 有效期 1 小时,refreshToken 有效期 1 天
- accessToken 生效后返回 401,前端通过 refreshToken 获取新的 accessToken 和新的 refreshToken
- refreshToken 生效后返回 403,须要从新登录
也就是说,登录胜利后,在 refreshToken 有效期内,都能够持续操作,并且顺延有效期,再也不会呈现用着用着忽然须要从新登录的状况了。这俩有效期能够自行调整,我这里思考的是 accessToken 最好不能太长,不然调整权限后失效期太短。
后端调整
新增用来刷新 token 的接口,大部分逻辑和登录是一样的,验证 refreshToken 后,返回新的 accessToken 和新的 refreshToken
前端调整
次要的难点在前端局部,前端的刷新逻辑:
- 登录胜利后在前端存储 accessToken 和 refreshToken,当前的每一次调用 API 都须要携带 accessToken
- 用户一小时后持续操作后端返回 401,此时 accessToken 生效,把这一阶段的所有申请都缓存下来
- 应用 refreshToken 获取新的 accessToken 和新的 refreshToken
- 应用新的 accessToken 从新发动方才所有缓存下来的申请
- 一天之后用户再次操作,后端返回 401,此时 accessToken 生效,把这一阶段的所有申请都缓存下来
- 应用 refreshToken 获取,后端返回 403,跳转到登录页从新登录
此处须要思考的是并发申请,须要把 accessToken 生效后期间所有的申请都缓存下来,并且在获取到无效 accessToken 后持续所有未实现的申请
目前我应用的是 axios,应用的拦截器,在此贴出局部外围代码:
// 响应拦截器axios.interceptors.response.use( response => { const data = response.data; // 没有code然而http状态为200示意内部申请胜利 if (!data.code && response.status === 200) return data; // 依据返回的code值来做不同的解决(和后端的公有约定) switch (data.code) { case 200: return data; default: } // 若不是正确的返回code,且曾经登录,就抛出谬误 throw data; }, err => { // 这里是返回 http 状态码不为 200和304 时候的错误处理 if (err && err.response) { switch (err.response.status) { case 400: err.message = '申请谬误'; break; case 401: // accesstoken 谬误 if (router.currentRoute.path === '/login') { break; } // 判断是否有 refreshToken const root = useRootStore(); if (!root.refreshToken) { logout(); break; } // 进入刷新 token 流程 // 本次申请的所有配置信息,蕴含了 url、method、data、header 等信息 const config = err?.config; const requestPromise = new Promise(resolve => { addRequestList(() => { // 留神这里的createRequest函数执行的时候是在resolve开始执行的时候,并且返回一个新的Promise,这个新的Promise会代替接口调用的那个 resolve(createRequest(config)); }); }); refreshTokenRequest(); // 这里很重要,因为本次申请 401 了,要返回给调用接口的办法返回一个新的申请 return requestPromise; case 403: // 403 这里阐明刷新token失败,登录曾经到期,须要从新登录 // 10 秒后革除所有缓存的申请 setTimeout(() => { clearTempRequestList(); }, 10000); logout(); break; default: } } return Promise.reject(err); });
刷新局部的逻辑代码:
import axios from 'axios';import http from './index';import { useRootStore } from '@/store/root';// 长期的申请函数列表const tempRequestList = [];// 发动刷新token的标记位,避免反复刷新申请let isRefreshing = false;// 1min 内刷新过token标记位// 为了避免并发的时候,刷新申请结束,tempRequestList也曾经清空,之后仍有申请返回403,造成反复刷新let refreshTokenWithin1Minute = false;const refreshTokenRequest = () => { if (isRefreshing) { return; } if (refreshTokenWithin1Minute) { for (const request of tempRequestList) { request(); } tempRequestList.length = 0; return; } isRefreshing = true; refreshTokenWithin1Minute = true; const root = useRootStore(); // 应用刷新token申请新的accesstoken和刷新token const params = { refreshToken: root.refreshToken }; http.post('/api/v1/refresh-token', params).then(({ data }) => { root.updateAccessToken(data.token); root.updateRefreshToken(data.refreshToken); root.updateUserId(data.userId); for (const request of tempRequestList) { request(); } // 1 min 后革除标记位 setTimeout(() => { refreshTokenWithin1Minute = false; }, 60000); tempRequestList.length = 0; isRefreshing = false; });};const addRequestList = request => { tempRequestList.push(request);};const clearTempRequestList = () => { tempRequestList.length = 0;};const createRequest = config => { // 这里必须更新 header 中的 AccessToken const root = useRootStore(); config.headers['Authorization'] = 'Bearer ' + root.accessToken; return axios(config);};export { refreshTokenRequest, createRequest, addRequestList, clearTempRequestList };
源码
前后端有提供源码,能够利用源码调整两个 token 的有效期进行测试
后端局部的源码在这里
前端局部的源码在这里
还有在线的体验地址