• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

前端双token策略(uni-app-vue3-ts版)

武飞扬头像
Justin3go
帮助7

前端双token策略(uniapp-vue3-ts版)

前言

前面写了一篇详细介绍JWT相关的文章,其中提到了JWT中使用双token的作用,这里简单回顾一下:

简单来说也是为了减轻JWT被泄露而造成的影响,具体来说分为refresh tokenaccess token

  access token refresh token
有效时长 较短(如半小时) 较长(如一天)
作用 验证用户是否有操作权限 获取access token
什么时候使用 每次需要用户登录态时传递该token access token失效时使用

这样做的好处就是:

  1. access token频繁传输,泄露风险较大,所以将其有效期设为较短可以有效降低泄露而造成的影响,比如此时攻击方最多伪装你半个小时;
  2. access token存在时间较短,需要频繁获取新的,为了降低用户登录次数,提高用户体验,使用refresh token调用相关接口获取最新的access token
  3. refresh token存在时间长,泄露后影响较大,所以只有在access token失效时才传递,所以并不会频繁传输,即泄露风险较小

主要就是兼顾泄露token的风险与泄露token的影响

由此可以看出双token的实现是很有必要的,所以本文将从前端角度介绍一下相关的策略,当后端有如下接口时:

  1. 登录成功后返回accessTokenrefreshToken
  2. 携带refreshToken调用刷新Token的接口返回accessTokenrefreshToken

由于笔者使用的是GraphQL接口,担心有些读者可能没有使用过,所以上面仅用文字描述接口

策略概述

学新通

  1. 发起一个正常请求,如获取用户的资料详情

  2. 检查accessToken是否过期,这里是通过其中的expire字段,后续会详细谈到

  3. 如果没有过期,则直接在header上添加该字段,就可以表明自己的身份了,从而正常请求

  4. 如果已经过期,则判断refreshToken是否过期

  5. 如果refreshToken没有过期,则携带该token进行refresh请求,获取新的双token并保存

  6. 携带新的accessToken进行请求

  7. 如果已经过期,则要求用户重新登录

为了实现上述策略,我们先准备几个通用函数,比如获取token中的expire字段、保存token等通用函数

实现浏览器的atob函数

简单介绍一下atob函数,它是用来解码base64字符串的,因为为了方便网络传输,JWT是进行了base64编码的,所以想要获取JWT中携带的相关信息,就需要先使用atob解码。

但是在微信小程序中,你可以发现在微信开发工具中可以调用该atob函数,但到真机演示的时候就无法调用了,会显示atob不存在,所以这里我们需要自实现一个atob函数,比如在/utils/base64.ts文件中:

const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 /=';

export function atob(input: string) {
  var str = (String (input)).replace (/[=] $/, ''); // #31: ExtendScript bad parse of /=
  if (str.length % 4 === 1) {
    throw new Error ("'atob' failed: The string to be decoded is not correctly encoded.");
  }
  for (
    // initialize result and counters
    var bc = 0, bs, buffer, idx = 0, output = '';
    // get next character
    buffer = str.charAt (idx  ); // eslint-disable-line no-cond-assign
    // character found in table? initialize bit storage and add its ascii value;
    // @ts-ignore
    ~buffer && (bs = bc % 4 ? bs * 64   buffer : buffer,
      // and if not first of each 4 characters,
      // convert the first 8 bits to one ascii character
      bc   % 4) ? output  = String.fromCharCode (255 & bs >> (-2 * bc & 6)) : 0
  ) {
    // try to find character in table (0-63, not found => -1)
    buffer = chars.indexOf (buffer);
  }
  return output;
}

如上函数改写自该仓库的代码实现

如下是该函数的调用效果演示:

学新通

当然前提是后端返回的JWT是包含了该字段的,该字段也是规范中的字段,赶紧叫你的后端加上该字段吧,又不麻烦[doge]:

学新通

实现操作token的通用函数

同样是在utils/auth.ts文件中,逻辑非常简单,如下:

import { atob } from "./base64";

// 在auth.js中定义设置和获取token的方法
export function getToken(accessOrRefreshKey: "accessToken" | "refreshToken"): string {
	return uni.getStorageSync(accessOrRefreshKey);
}

export function setToken(accessOrRefreshKey: "accessToken" | "refreshToken", value: string) {
	return uni.setStorageSync(accessOrRefreshKey, value);
}
// 清除双token
export function clearToken() {
	uni.removeStorageSync("accessToken");
	uni.removeStorageSync("refreshToken");
}

// 获取过期时间,token需要符合JWT格式且有exp属性
export function getExpireInPayload(token: string): number {
	if(!token) return -1; // 所有时间戳都会大于-1,即没有token也算过期,做相应的过期处理,如跳转登录
	const parts = token.split(".");
	const payload = JSON.parse(atob(parts[1]));
	return Number(payload.exp);
}

拦截器实现该策略

然后,我们就可以使用uniapp自带的request拦截器实现该双token策略,基本逻辑就和概述中描述的逻辑是一致的,可以参考着查看以下代码,main.ts中:

let inRefresh = false;
// 请求拦截器
uni.addInterceptor("request", {
	async invoke(request) {
		uni.showLoading({ title: "正在请求中..." });
		const meStore = useMeStore();
		// meStore.inLogin是pinia中判断当前请求是否为登录请求
		if (meStore.inLogin || inRefresh) return request;

		const timestamp = Math.ceil( new Date().getTime() / 1000); //获取当前的时间戳

		// 1. access部分
		const accessToken = getToken("accessToken"); // 获取身份验证令牌
		const expInAccessToken = getExpireInPayload(accessToken);
		// accessToken未过期,直接加入请求头请求
		if (timestamp < expInAccessToken) {
			request.header.Authorization = `Bearer ${accessToken}`;
			return request;
		}

		// 2. refresh部分
		const refreshToken = getToken("refreshToken");
		const expInRefreshToken = getExpireInPayload(refreshToken);
		// refreshToken未过期,刷新Token
		if (timestamp < expInRefreshToken) {
			const { execute } = useMutation(refreshTokenGQL);
			inRefresh = true; // 避免递归栈溢出
			const { data, error } = await execute({ token: refreshToken });
			inRefresh = false;
			console.log("refresh data: ", data);
			console.log("refresh error: ", error);
			// save
			const { accessToken: newAccessToken, refreshToken: newRefreshToken } = data?.refreshToken || {};
			request.header.Authorization = `Bearer ${newAccessToken}`;
			setToken("accessToken", newAccessToken);
			setToken("refreshToken", newRefreshToken);
		} else {
			// refreshToken过期,需要重新登录
			uni.reLaunch({
				url: "/pages/me/index",
				success: () => {
					uni.showToast({
						title: "登录凭证无效",
						icon: "error",
						duration: 2000,
					});
				},
			});
		}
		return request;
	},
	fail(err) {
		uni.showToast({
			title: `网络请求错误`,
			icon: "error",
			duration: 2000,
		});
	},
	complete() {
		// showLoading需要每次请求前手动添加,因为里面有可自定义的title
		uni.hideLoading();
	},
});

最后

上述代码在该仓库中都全部存在,欢迎查看

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhcefacg
系列文章
更多 icon
同类精品
更多 icon
继续加载