token刷新是前端安全中必要的一部分,本文从后端到前端整个流程介绍如何实现无感刷新token。页面代码亲自实现并跑通,请放心食用。
通过长短token实现:短token用来请求应用数据,长token用于获取新的短token(长短指的是过期时间)
后端存有两个字段,分别保存长短token,并且每一段时间更新他们。短token过期,返回 returncode:104;长token过期,返回 returncode: 108;请求成功返回returncode: 0;请求头中pass用来接收客户端长token,请求头中authorization用来接收客户端短token 。
1. 搭建node服务npm init -y安装koa
npm i koa -s新建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",
运行:npm run dev 访问:127.0.0.1:4000,可以看到页面显示这是一个应用中间件。
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使用:
const cors = require('koa2-cors'); // 堆代码 duidaima.com app.use(cors());最终index.js文件
const Koa = require('koa') const app = new Koa(); const index = require('./routes/index') const cors = require('koa2-cors'); app.use(cors()); app.use(index.routes(),index.allowedMethods()) app.listen(4000,() => { console.log('server is listening on port 4000') })重新运行 npm run dev,这时服务端已准备好
/* 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;
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. 使用
import { useState } from 'react'; import service from './service/index.js'; import { ACCESS_TOKEN, REFRESH_TOKEN } from './config/constant'; const App = () => { const [data1, setData1] = useState(); const [data2, setData2] = useState(); const getData = () => { service.get('/getData').then((res) => { setData1(res.data.id); }); service.get('/getData2').then((res) => { setData2(res.data.id); }); }; const getToken = () => { service.get('/login').then((res) => { //存储长token localStorage.setItem(REFRESH_TOKEN, res.refreshToken); //存储短token localStorage.setItem(ACCESS_TOKEN, res.accessToken); }); }; return ( <div> {data1}--{data2} <button onClick={getData}>按钮</button> <button onClick={getToken}>登录</button> </div> ); }; export default App;
2.页面数据能更新,并且refresh接口只调用一次