浅析应用 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 的有效期进行测试
后端局部的源码在这里
前端局部的源码在这里
还有在线的体验地址