最近总是回想起大三时为了体(装)验(逼)提出要在学校新版系统中加入无感刷新token的功能。当时只是听到这个玩意甚至还没搞明白是咋回事,于是直到几个月后出来实习也没真正加上这个功能。

先说一个场景:我们都知道 JWT。假如说某系统将其token存留时间设置为5分钟。那么,如果说用户在这一时间快结束时恰好有其他事,回来以后再点“提交”,发现弹出“用户凭证已失效,请重新登录”。怎一个惨字了得!
当然,类似的场景还有许多,毫无疑问的是,如果过期时间设置的很短,用户就必须每隔一段时间重新登录,以获取新的凭证,这会极大挫伤用户的积极性。但是设置得太长,用户数据的安全性将大打折扣。

关于这一点,当时我与学弟们讨论了些许时间,最后得出以“埋点”为辅助,或监听用户行为,延长用户凭证存储时间 —— 可以发现,这里其实只是“巧妙”地解决了一个极限场景:若用户在过期时间前(很短时间时)又回来了。但实际上并没有解决上面说的问题。
最终还必须要借助双token,也就是标题说的“无感刷新token”。

解决思路也很简单:两个token存储(过期)时间不同。短token用来请求应用数据,长token用于获取新的短token。

后端设计逻辑

这里笔者必然用Koa实现。一共有三步:

  • 后端存有两个字段,分别保存长短token,并且每一段时间更新(?是否有其他方案 !可以用koa的ctx.state造一个全局的状态变量。但是就开发成本来说肯定是setInterval要低一些);
  • 为长token、短token约定单独的code值,便于前端排查、判断;
  • 请求头中新增两个字段携带表示长、短token(JWT思想),便于后端处理;

在router文件夹下的index.js文件中:

const router = require("koa-router")();
let accessToken = "s_token"; //短token
let refreshToken = "l_token"; //长token// 30min刷新一次短token
setInterval(() => {accessToken = "s_tk" + Math.random();
}, 300000);// 12小时刷新一次长token
setInterval(() => {refreshToken = "l_tk" + Math.random();
}, 7200000);// 在登录接口后拿到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,};}
});// 获取应用数据时后端校验一次token,这里如果过期了返回后前端不能跳登录页而是要重新拿token,续期
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: { 数据 },};}
});module.exports = router;

然后在主文件中引用:

const Koa = require('koa')
const app = new Koa();
const cors = require('koa2-cors');
const index = require('./router/index')app.use(cors());
app.use(index.routes(),index.allowedMethods())app.listen(8088,() => {console.log('server is listening on port 8088')
})

前端封装逻辑

新建config文件夹,处理请求事宜。
在config下的token_enum.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

然后在code_map.js中存放前后端约定好的code:

// 在其它客户端被登录
export const CODE_LOGGED_OTHER = 106;
// 重新登陆
export const CODE_RELOGIN = 108;
// token过期
export const CODE_TOKEN_EXPIRED = 104;
//接口请求成功
export const CODE_SUCCESS = 0;

新建service文件夹,在其中的index.js文件中:

import axios from "axios";
import { refreshAccessToken, NoneTokenRequestList, clearAuthAndRedirect } from "./refresh";
import {CODE_LOGGED_OTHER,CODE_RELOGIN,CODE_TOKEN_EXPIRED,CODE_SUCCESS,
} from "../config/code_map.js";
import { ACCESS_TOKEN, AUTH } from "../config/token_enum.js";const service = axios.create({baseURL: "//127.0.0.1:8088",timeout: 30000,
});// 劫持请求,往request-header添加短token,用于后端处理
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;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) { //当前是第一次调用发现token失效的情况config.retry = true;NoneTokenRequestList(() => resolve(service(config)));refreshAccessToken();} else {return reject(data);}} else {resolve(data);}});},(error) => {return Promise.reject(error);}
);export default service;

在同级新建refresh.js文件:

import service from "./index.js";
import { ACCESS_TOKEN, REFRESH_TOKEN, PASS } from "../config/token_enum.js";let subscribers = [];
let pending = false; //同时请求多个过期链接,保证只请求一次获取短tokenexport const NoneTokenRequestList = (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重新获取短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;}}
};/* 清除长短token,并定位到登录页(在项目中使用路由跳转) */
export const clearAuthAndRedirect = () =>{localStorage.removeItem(ACCESS_TOKEN)window.location.href = '/login'
}

我觉得这样设计还很流批的一点是:它是上层封装,基本是不侵入业务代码的。


尾记

上海这一波极限操作搞得我学校也回不去,家也回不去,,,竟无语凝噎。想了想高中考完聚餐别人拍班照的时候我因为不胜酒力在旁边吐,现在又因为疫情回不去学校。合着我注定不能出现在班级照片中呗┗( ▔, ▔ )┛,一时竟悲从中来。

感谢学弟@*辉给我提供的一些想法,以及让我能虽然不在学校还能在学校系统中查看实际使用和与其他功能联动的效果,毕竟没有经过检验的想法注定只能是想法。我就有许多这种想法,但是马上就彻底跟学校分开了,不能白嫖学校资源了。一想到此,虽值五一佳节,更悲伤不已。

无感token刷新,我是怎么做的相关推荐

  1. uniapp 实现无感刷新token, 适应大多数项目

    不管你是用taro uni 还是vue-cli 或者 react-cli 刷新token这块一通百通 本质上 都一样 我之前讲了一个是 在响应拦截哪里做token刷新 其实这样做还是不好的,因为这样我 ...

  2. 实现无感刷新token我是这样做的

    大家好,我是漫步,今天来分享一个登录常常遇到的难题,即登录超时时间与安全性的纠结问题. 原文: https://juejin.cn/post/6983582201690456071 前言 最近在做需求 ...

  3. 实现无感刷新 token 我是这样做的

    原文: https://juejin.cn/post/6983582201690456071 前言 最近在做需求的时候,涉及到登录token,产品提出一个问题:能不能让token过期时间长一点,我频繁 ...

  4. 关于无感刷新Token,我是这样子做的

    本文正在参加「金石计划 . 瓜分6万现金大奖」 什么是JWT JWT是全称是JSON WEB TOKEN,是一个开放标准,用于将各方数据信息作为JSON格式进行对象传递,可以对数据进行可选的数字加密, ...

  5. 使用Axios进行无感刷新Token

    前言 本人在开发项目时,在做登录模块时,参考了oauth2,在用户认证成功后会返回给前端一些令牌相关数据.接下来,再用进行接口请求时,前端根据令牌数据进行一系列的判断,然后做出最好的选择. 举个例子: ...

  6. 关于实现token无感刷新的解决方案

    问题引入 在开发中为了安全或满足分布式场景,通常会舍弃原有的session认证手段,而采用jwt(json web token):但是使用token难免遇到token有效期的问题,如果token长期有 ...

  7. Laravel6通过jwt(tymon/jwt-auth)实现API用户无感刷新TOKEN

    Laravel6通过jwt实现API用户无感刷新TOKEN 1.TOKEN是什么 2.jwt是什么 3.jwt安装&配置 3.1.通过composer安装 3.2.发布配置 3.3.生成加密密 ...

  8. Vue 无感刷新token

    关于无感刷新的理解:  实现token无感刷新对于前端来说是一项非常常用的技术,其本质是为了优化用户体验,当token过期时不需要用户跳回登录页重新登录,而是当token失效时,进行拦截,发送刷新to ...

  9. token过期怎么办 无感刷新token

    (1)可以通过响应拦截器或者全局前置守卫强制跳转登录页 // 全局前置守卫 router.beforeEach((to, from) => {let token = sessionStorage ...

最新文章

  1. 微软研究院和清华大学联合发布 “开放学术图谱(OAG)2.0版本”
  2. 洛谷P1541 乌龟棋
  3. 5万字长文:Stream和Lambda表达式最佳实践-附PDF下载
  4. 2019-03-22-算法-进化(回文链表)
  5. C++ STL 容器之 deque简单使用
  6. HTML框架(Frames)
  7. react 用html插件,VSCode拓展插件推荐(HTML、Node、Vue、React开发均适用)-Go语言中文社区...
  8. OpenLayers相关资料
  9. Ubuntu 15.10 默认壁纸?
  10. 实现拍照_实现“拍照自由”!vivo IFEA分离式镜头打破思维定势
  11. 《深入理解Java虚拟机》 第4章 虚拟机性能监控与故障处理工具
  12. aplay与call
  13. LHDC、AAC、aptx、ldac、wha哪个更好,各有什么优缺点?
  14. python进行对应分析_对应分析
  15. IEEE 754 浮点数
  16. 1.深入.NET框架
  17. 【paper】DenseFusion: 6D Object Pose Estimation by Iterative Dense Fusion 6D姿态估计
  18. 两种图像拼接(无重叠相邻图有重叠相邻图)以及matlab实现(边线查找法)
  19. windows10中创建Direct3D11设备出现0x887a002d错误的解决方案
  20. Python,爬取电影天堂,你觉得怎么样?

热门文章

  1. 云之家群组机器人如何定时自动发消息?
  2. android捕鱼达人修改方法(反编译、修改、打包)
  3. CDH6官方文档中文系列(2)----Cloudera安装指南(安装前)
  4. Python3基础入门自学教程
  5. 连接数据库出现:Connections could not be acquired from the underlying database
  6. HBase的CRUD的Java Api操作
  7. js判断0==‘‘,判断1==true
  8. Monkey测试(APP稳定性)
  9. Oracle SPARC T7-2 服务器:硬件规格
  10. 女生学java开发难吗?女生适合学java吗?