无感刷新
应用场景
sh
1. 正在看学习资料,突然告诉你token过期了,该重新登录了。一下回到登录页了,这是大家都不喜欢的。
基本原理
sh
1. 利用双token accessToken和refreshTokne 长短token
2. 在发送请求的时候通过在请求头中添加短token(accessToken)
3. 长token在响应返回短token过期后,用来发送请求刷新token的
实现流程
sh
1. 登录成功后颁发长短token
2. 短token放在请求头中
3. 长token用在短token失效,后端返回短token过期信息后,
在响应拦截中执行长token验证的请求,发送给后端,
后端会验证长token有没有过期。
- 没过期: 返回新的长短token,
- 过期: 返回长token过期了的信息,前端会返回登录页
总结
实现了
sh
1. 无感刷新
2. 避免多次请求, 长token方法 添加控制器 避免发送多次长token验证
3. 触发短token失效的请求权,会暂存请求,后续token更新完成 继续执行
无感刷新
避免多次刷新
暂存请求 将()=> { resolve(执行的请求) } 箭头函数进行存储
前端 - Vue
常量文件
js
// /config/constants.js
export const ACCESS_TOKEN = 'accessToken'; // 短token字段
export const REFRESH_TOKEN = 'refreshToken'; // 长token字段
export const AUTH = 'Authorization'; // header头部 携带短token
export const PASS = 'pass'; // header头部 携带长token
方法文件
js
// /config/storage.js
// 存储短token
import * as constants from './constants';
// 存储短token
export const setAccessToken = (token) => localStorage.setItem(constants.ACCESS_TOKEN, token);
// 存储长token
export const setRefreshToken = (token) => localStorage.setItem(constants.REFRESH_TOKEN, token);
// 获取短token
export const getAccessToken = () => localStorage.getItem(constants.ACCESS_TOKEN);
// 获取长token
export const getRefreshToken = () => localStorage.getItem(constants.REFRESH_TOKEN);
// 删除短token
export const removeAccessToken = () => localStorage.removeItem(constants.ACCESS_TOKEN);
// 删除长token
export const removeRefreshToken = () => localStorage.removeItem(constants.REFRESH_TOKEN);
// 请求后端测试数据
export const getUserInfoFormServer = () => axios.get('/vue/info');
// 登录验证接口
export const userLoginFormServer = (details) => axios.post('/vue/Login', details);
axios 的请求响应拦截
js
import axios from 'axios';
import * as storage from '../../config/storage';
import * as constants from '../../config/constants';
import { addRequest, refreshToken } from '../refresh';
const server = axios.create({
baseURL: 'http://localhost:3000/',
timeout: 5000 * 10,
headers: { 'Content-Type': 'application/json' },
});
// 请求拦截器
server.interceptors.request.use(
(config) => {
// 获取短token 添加进请求头,在短token失效时,再使用长token去请求后端的 /refresh 接口验证长token是不是过期 refreshToken()方法
let accessToken = storage.getAccessToken(constants.ACCESS_TOKEN);
config.headers[constants.AUTH] = accessToken;
return config;
},
(error) => {
return Promise.reject(error);
},
);
/*响应拦截器*/
server.interceptors.response.use(
async (response) => {
// 获取到配置和后端响应的数据
let { config, data } = response;
return new Promise((resolve, reject) => {
// 短token失效
if (data.code === 403) {
// 移除失效的短token
storage.removeAccessToken(constants.ACCESS_TOKEN);
console.log('短token失效');
// 把过期请求存储起来,用于请求到新的短token,再次请求,达到无感刷新
addRequest(() => resolve(server(config)));
// 携带长token去请求新的token
refreshToken();
} else {
// 有效返回相应的数据
resolve(data);
}
});
},
(error) => {
return Promise.reject(error);
},
);
export default server;
处理短 token 过期(refresh)
处理短 token 过期
js
export { REFRESH_TOKEN, PASS } from '@/config/constants';
import { getRefreshToken, removeRefreshToken, setAccessToken, setRefreshToken, backToLogin } from '@/config/storage';
import server from './http/index';
let subscribes = []; // 存 被打断的响应函数 后面继续访问
let flag = false; // 设置开关,保证一次只能请求一次短token,防止客户多此操作,多次请求
/*把过期请求添加在数组中*/
export const addRequest = (request) => {
subscribes.push(request);
};
/*调用过期请求*/
export const retryRequest = () => {
console.log('重新请求上次中断的数据');
subscribes.forEach((request) => request());
subscribes = []; // 清空队列,用于下次中断的请求
};
/*短token过期,携带token去重新请求token*/
export const refreshToken = async () => {
// console.log('flag--', flag) /*防止重复请求 && 调试*/
if (!flag) {
flag = true;
let r_tk = getRefreshToken(); // 获取长token
if (r_tk) {
let res = await server.get(
'/vue/refresh',
Object.assign(
{},
{
headers: { PASS: r_tk },
},
),
);
//长token失效,退出登录
if (res.code === 401) {
console.log('长token失效');
flag = false;
removeRefreshToken();
// 退出登录
window.alert('请重新登录');
backToLogin();
} else {
// 存储新的token
setAccessToken(res.data.accessToken);
setRefreshToken(res.data.refreshToken);
flag = false;
// 重新请求数据
retryRequest();
}
}
}
};
Vue 导航守卫
js
// 全局前置导航守卫
router.beforeEach((to, from, next) => {
let token = getAccessToken();
if (token) {
next();
} else {
if (to.path === '/login') {
next();
} else {
next('/login');
}
}
});
页面
vue
<script setup>
import { ref } from 'vue';
import { loginRules, userLogin } from '@/server/Login';
import { useRouter } from 'vue-router';
const details = ref({ username: '', password: '' }); // 提交信息
const router = useRouter();
// 登录本地存储token
const LoginValidate = () => {
if (loginRules(details.value)) {
userLogin(details.value).then((res) => {
if (res.code === 200) {
localStorage.setItem('accessToken', res.accessToken);
localStorage.setItem('refreshToken', res.refreshToken);
router.push('/home');
} else {
alert(res.msg);
}
});
}
};
</script>
<template>
<form
class="login_from"
@submit.prevent>
<input
class="login-username"
type="text"
v-model="details.username"
placeholder="用户名/账号" />
<input
class="login-password"
type="text"
v-model="details.password"
placeholder="密码" />
<button @click="LoginValidate">提交</button>
</form>
<!-- 找回账号密码 -->
<div class="login-forget">
<span>忘记账号名</span>
<span>找回密码</span>
</div>
</template>
后端 - node+express
中间件
验证 token 前端收到 403 直到短 token 过期了,立即使用长 token 获取
js
// /middleware/jwtAuth.js
const { secret } = require('../utils/token'); // 引入密钥
const jwt = require('jsonwebtoken'); // 我在全局配置过其实这里不用配置了,但是觉得这样可能会减小认知负担就留下了,问就是第六感
// 白名单
const whiteList = ['/vue/Login', '/vue/refresh'];
// 判断是否在白名单中
const isWhiteList = (path) => whiteList.includes(path);
// 中间件
const auth = async (req, res, next) => {
// 是白名单直接进入路由
if (isWhiteList(req.path)) return next();
// 获取请求头中的短token
const authHeader = req.headers['authorization'];
// 验证短token过没过期
jwt.verify(authHeader, secret, (err, decoded) => {
if (err) {
// 短token过期了返回信息 403(服务器理解请求,但, 拒绝执行!)
return res.send({
code: 403,
msg: 'token验证失败',
});
} else {
req.user = decoded;
next();
}
});
};
module.exports = auth;
App 配置文件
在配置文件中应用中间件
js
// App.js
var jwtAuth = require('./middleware/jwtAuth');
var cors = require('cors');
var app = express();
// 顺序很重要
app.use(cors()); //1 跨域
app.use(jwtAuth); //2 中间件验证token
路由文件
js
// 登录校验接口
router.post('/Login', async function (req, res, next) {
let { username, password } = req.body;
let data = await userModel.find(req.body).lean();
if (data.length == 0) {
res.send({
code: 400,
msg: '用户不存在',
});
} else {
if (data[0].username === username && data[0].password === password) {
res.send({
code: 200,
msg: '登录成功',
// 登录成功颁发两个token
accessToken: accessToken({ username: username }),
refreshToken: refreshToken({ username: username }),
});
} else {
res.send({
code: 400,
msg: '用户名或密码错误',
});
}
}
});
/**
* 验证长token是否有效?刷新accessTokne : 重新登录
*
* 注意: 刷新短tokne的时候 长token也刷新,延续长tokne
*
* 这样就可以实现,长时间不活跃才会重新登录
*/
// 验证长token接口
router.get('/refresh', async function (req, res, next) {
let code,
msg,
data = null;
console.log('执行了refresh');
let r_tk = req.headers['pass'];
//解析token 参数 token 密钥 回调函数返回信息
jwt.verify(r_tk, secret, (error) => {
if (error) {
(code = 401), (msg = '长token无效,请重新登录');
} else {
(code = 200), (msg = '长token有效,返回新的token');
data = {
accessToken: accessToken({ name: 'zs' }),
refreshToken: refreshToken({ name: 'zs' }),
};
}
res.send({
code,
msg: msg ? msg : null,
data,
});
});
});
// 请求数据测试接口
router.get('/info', async function (req, res, next) {
let data = await userModel.find();
res.send({
code: 200,
msg: '响应成功',
data: data,
});
});
常量&方法文件
js
// utils/token.js
const jwt = require('jsonwebtoken'); // 引入jsonwebtoken
/**
* jwt.sign({存放的数据,通常是用户名等信息},密钥,{expiresIn:过期时间})
* jwt.verify(token,密钥,(err)=>{执行的回调})
*/
const secret = 'ZY_ML'; // 密钥
const accessTokenTime = 5;
const refreshTokenTime = 10;
// 生成accessToken
const accessToken = (payload) => {
return jwt.sign(payload, secret, { expiresIn: accessTokenTime });
};
// 生成refreshTokne
const refreshToken = (payload) => {
return jwt.sign(payload, secret, { expiresIn: refreshTokenTime });
};
// 导出
module.exports = {
secret,
accessToken,
refreshToken,
};