# 无感刷新Token
# 关于Token
token刷新是前端安全中必要的一部分,本文从后端到前端整个流程介绍如何实现无感刷新token。
# 一、实现思路
通过长短token实现:短token用来请求应用数据,长token用于获取新的短token(长短指的是过期时间)
# 二、后端设计
- 后端存有两个字段,分别保存长短token,并且每一段时间更新他们
- 短token过期,返回 returncode:104;长token过期,返回 returncode: 108;请求成功返回returncode: 0;
- 请求头中pass用来接收客户端长token,请求头中authorization用来接收客户端短token
# 1. 搭建Node服务器
安装Koa
npm init
npm i koa
新建index.js
const Koa = require('koa')
const app = new Koa();
app.use(async(ctx,next)=>{
ctx.body = "这是一个应用中间件";
await next()
})
app.listen(4000,() => {
console.log('server is listening on port 4000')
})
安装nodemon并配置启动命令
npm i nodemon -g
// package.json
"dev":"nodemon index.js",
# 2. 使用路由中间件
安装
npm i koa-router -S
新建routes/index.js
const router = require("koa-router")();
let accessToken = "init_s_token"; //短token
let refreshToken = "init_l_token"; //长token
/* 5s刷新一次短token */
setInterval(() => {
accessToken = "s_tk" + Math.random();
}, 5000);
/* 一小时刷新一次长token */
setInterval(() => {
refreshToken = "l_tk" + Math.random();
}, 600000);
/* 登录接口获取长短token */
router.get("/login", async (ctx) => {
ctx.body = {
returncode: 0,
accessToken,
refreshToken,
};
});
/* 获取短token */
router.get("/refresh", async (ctx) => {
//接收的请求头字段都是小写的
let { pass } = ctx.headers;
if (pass !== refreshToken) {
ctx.body = {
returncode: 108,
info: "长token过期,重新登录",
};
} else {
ctx.body = {
returncode: 0,
accessToken,
};
}
});
/* 获取应用数据1 */
router.get("/getData", async (ctx) => {
let { authorization } = ctx.headers;
if (authorization !== accessToken) {
ctx.body = {
returncode: 104,
info: "token过期",
};
} else {
ctx.body = {
code: 200,
returncode: 0,
data: { id: Math.random() },
};
}
});
/* 获取应用数据2 */
router.get("/getData2", async (ctx) => {
let { authorization } = ctx.headers;
if (authorization !== accessToken) {
ctx.body = {
returncode: 104,
info: "token过期",
};
} else {
ctx.body = {
code: 200,
returncode: 0,
data: { id: Math.random() },
};
}
});
module.exports = router;
修改index.js
//删除
app.use(async(ctx,next)=>{
ctx.body = "这是一个应用中间件";
await next()
})
//新增
const index = require('./routes/index')
app.use(index.routes(),index.allowedMethods())
# 3. 跨域处理
安装
npm i koa2-cors
修改index.js
// 新增
const cors = require('koa2-cors');
app.use(cors());
# 三、前端设计
# 1. 定义常量
新建 config/constant.js
/* localStorage存储字段 */
export const ACCESS_TOKEN = "s_tk"; //短token
export const REFRESH_TOKEN = "l_tk"; //长token、
/* HTTP请求头字段 */
export const AUTH = "Authorization"; //存放短token
export const PASS = "PASS"; //存放长token
新建 config/returnCodeMap.js
// 在其它客户端被登录
export const CODE_LOGGED_OTHER = 106;
// 重新登陆
export const CODE_RELOGIN = 108;
// token过期
export const CODE_TOKEN_EXPIRED = 104;
//接口请求成功
export const CODE_SUCCESS = 0;
# 2.封装服务
关键点:将token过期的请求,借助Promise将请求存进数组中,让这个Promise一直处于pending状态(即不调用resolve),当获取到新的短token时,再逐个重新请求
如果不保持promise链,就会当成一个新的请求,页面内容不会更新
安装axios
npm i axios -S
新建 service/index.js
import axios from "axios";
import { refreshAccessToken, addSubscriber } from "./refresh";
import { clearAuthAndRedirect } from "./clear";
import {
CODE_LOGGED_OTHER,
CODE_RELOGIN,
CODE_TOKEN_EXPIRED,
CODE_SUCCESS,
} from "../config/returnCodeMap";
import { ACCESS_TOKEN, AUTH } from "../config/constant";
const service = axios.create({
baseURL: "//127.0.0.1:4000",
timeout: 30000,
});
service.interceptors.request.use(
(config) => {
let { headers } = config;
const s_tk = localStorage.getItem(ACCESS_TOKEN);
s_tk &&
Object.assign(headers, {
[AUTH]: s_tk,
});
return config;
},
(error) => {
return Promise.reject(error);
}
);
service.interceptors.response.use(
(response) => {
let { config, data } = response;
//retry:第一次请求过期,接口调用refreshAccessToken,第二次重新请求,还是过期则reject出去
let { retry } = config;
/* 延续Promise链 */
return new Promise((resolve, reject) => {
if (data["returncode"] !== CODE_SUCCESS) {
if ([CODE_LOGGED_OTHER, CODE_RELOGIN].includes(data.returncode)) {
clearAuthAndRedirect();
} else if (data["returncode"] === CODE_TOKEN_EXPIRED && !retry) {
config.retry = true;
addSubscriber(() => resolve(service(config)));
refreshAccessToken();
} else {
return reject(data);
}
} else {
resolve(data);
}
});
},
(error) => {
return Promise.reject(error);
}
);
export default service;
新建 service/refresh.js
import service from "./index";
import { ACCESS_TOKEN, REFRESH_TOKEN, PASS } from "../config/constant";
import { clearAuthAndRedirect } from "./clear";
let subscribers = [];
let pending = false; //同时请求多个过期链接,保证只请求一次获取短token
export const addSubscriber = (request) => {
subscribers.push(request);
};
export const retryRequest = () => {
subscribers.forEach((request) => request());
subscribers = [];
};
export const refreshAccessToken = async () => {
if (!pending) {
try {
pending = true;
const l_tk = localStorage.getItem(REFRESH_TOKEN);
if (l_tk) {
/* 重新获取短token */
const { accessToken } = await service.get(
"/refresh",
Object.assign({}, { headers: { [PASS]: l_tk } })
);
localStorage.setItem(ACCESS_TOKEN, accessToken);
retryRequest();
}
return;
} catch (e) {
clearAuthAndRedirect();
return;
} finally {
pending = false;
}
}
};
新建 service/clear.js
import {ACCESS_TOKEN} from '../config/constant'
/* 清除长短token,并定位到登录页(在项目中使用路由跳转) */
export const clearAuthAndRedirect = () =>{
localStorage.removeItem(ACCESS_TOKEN)
window.location.href = '/login'
}
# 3.使用
修改App.vue
<template>
<p>{{ data1 }}</p>
<p>{{ data2 }}</p>
<button @click="getData">获取数据</button>
<button @click="getToken">登录</button>
</template>
<script setup>
import service from './service/index';
import { ACCESS_TOKEN, REFRESH_TOKEN } from './config/constant';
import { onMounted, ref } from 'vue';
const data1 = ref('')
const data2 = ref('')
const getData = () => {
service.get('/getData').then((res) => {
data1.value = res.data.id
})
service.get('/getData2').then((res) => {
data2.value = res.data.id
})
}
const getToken = () => {
service.get('/login').then((res) => {
//存储长token
localStorage.setItem(REFRESH_TOKEN, res.refreshToken);
//存储短token
localStorage.setItem(ACCESS_TOKEN, res.accessToken);
});
};
</script>
<style scoped></style>
效果图