Vue实现双token无感刷新的示例代码

 更新时间:2024年03月08日 09:55:17   作者:RCX明  
这篇文章主要介绍了Vue实现双token无感刷新,双token机制,尤其是指在OAuth 2.0授权协议中广泛使用的access token(访问令牌)和refresh token(刷新令牌)组合,文中通过代码示例讲解的非常详细,需要的朋友可以参考下

双token机制,尤其是指在OAuth 2.0授权协议中广泛使用的access token(访问令牌)和refresh token(刷新令牌)组合,用来实现无感刷新登录状态的原理如下:

初次授权与发放Token:

用户登录时,通过用户名、密码或其他认证方式向认证服务器请求授权。认证成功后,服务器不仅返回一个短期有效的access token(通常几分钟到几小时),还会发放一个长期有效的refresh token(几天到几个月)。

Access Token的作用:

access token是客户端访问受保护资源的临时凭证,每次客户端发起对受保护资源的请求时,都需要在HTTP请求头中携带access token。一旦access token过期,请求就会失败。

Refresh Token的作用:

refresh token的目的是在access token过期后,无需用户重新登录,客户端可以使用refresh token向认证服务器申请新的access token。通常refresh token的生命周期较长,而且存储得更为安全,因为它涉及到长期的授权。

无感刷新:

当客户端检测到access token即将过期或已经过期时,自动在后台向认证服务器发起请求,携带refresh token换取新的access token。这个过程对用户来说是无感知的,即用户不需要重新登录,页面也不会中断或刷新,因此被称为“无感刷新”。

安全机制:

为了保证安全性,refresh token一般具备一定的安全措施,例如限制其使用次数(防止无限刷新)、设置有效期(过期后必须重新登录)以及严格的存储策略(通常不会在客户端明文存储,而是存储在服务器端或经过加密存储在客户端本地)。

通过这种双token机制,可以在保障用户隐私和安全性的同时,大大提升用户体验,让用户在长时间操作过程中无需反复登录,实现所谓的“无感刷新登录状态”。

后端创建nest项目

# 创建
npx nest new token-test
#运行
cd token-test
npm run start

AppController 添加login、refresh、getinfo接口

// 登录请求
  @Post('api/login')
  login(@Body() userDto: UserDto) {

    console.log(userDto);
    const user = users.find(item => item.username === userDto.username);

    if (!user) {
      throw new BadRequestException('用户不存在');
    }

    if (user.password !== userDto.password) {
      throw new BadRequestException("密码错误");
    }

    const accessToken = this.jwtService.sign({
      username: user.username,
      email: user.email
    }, {
      expiresIn: '0.5h'
    });
    //access_token 过期时间半小时
    const refreshToken = this.jwtService.sign({
      username: user.username
    }, {
      expiresIn: '7d'
    })
    //refresh_token 过期时间 7 天
    return {
      userInfo: {
        username: user.username,
        email: user.email
      },
      accessToken,
      refreshToken
    };
  }

  // 刷新token请求
  @Post('api/refresh')
  refresh(@Body() body: any) {
    try {
      console.log('refresh token');
      console.log(body.token);
      const data = this.jwtService.verify(body.token);

      const user = users.find(item => item.username === data.username);

      const accessToken = this.jwtService.sign({
        username: user.username,
        email: user.email
      }, {
        expiresIn: '0.5h'
      });

      const refreshToken = this.jwtService.sign({
        username: user.username
      }, {
        expiresIn: '7d'
      })
      return {
        accessToken,
        refreshToken
      };
    } catch (e) {
      throw new UnauthorizedException('token 失效,请重新登录');
    }
  }

  // 验证token获取用户信息
  @Get('api/getinfo')
  getinfo(@Req() req: Request) {
    const authorization = req.headers['authorization'];

    if (!authorization) {
      throw new UnauthorizedException('用户未登录');
    }
    try {
      const token = authorization.split(' ')[1];
      const data = this.jwtService.verify(token);

      return {
        userInfo: {
          username: data.username,
          email: data.email
        }
      };
    } catch (e) {
      throw new UnauthorizedException('token 失效,请重新登录');
    }
  }

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

创建user.dto.ts

export class UserDto {
    username: string;
    password: string;
}

在这里插入图片描述

AppController添加模拟数据

const users = [
  { username: 'test', password: 'success', email: 'abc@163.com' }
]

在这里插入图片描述

前端Hbuilder创建VUE3项目

安装axios

pnpm i axios

src目录下创建以下两个文件

utils/request.js

//request.js
import axios from "axios";
import { resolveResError } from "./helpers";

const server = axios.create({
	baseURL: "/api",
	timeout: 1000 * 10,
	headers: {
		"Content-type": "application/json"
	}
})
var requesting = false
/*请求拦截器*/
function reqResolve(config) {
	let accessToken = localStorage.getItem('access_token')
	if (accessToken) {
		config.headers.Authorization = 'Bearer ' + accessToken
	}
	return config
}

function reqReject(error) {
	return Promise.reject(error)
}

const SUCCESS_CODES = [0, 200, 201, 202, 203, 204, 205]
/*响应拦截器*/
function resResolve(response) {
	const { data, status, config, statusText, headers } = response
	if (headers['content-type']?.includes('json')) {
		//获取状态码
		const code = data?.code ?? status
		//检查是否保持
		if (SUCCESS_CODES.includes(code)) {
			return Promise.resolve(data)
		}

		// 根据code处理对应的操作,并返回处理后的message
		const message = resolveResError(code, data?.message ?? statusText)
			//需要错误提醒(是否不需要提示)

			!config?.noNeedTip && message && window.$message?.error(message)

		return Promise.reject({ code, message, error: data ?? response })
	}
	return Promise.resolve(data ?? response)
}

async function resReject(error) {
	if (!error || !error.response) {
		const code = error?.code
		/** 根据code处理对应的操作,并返回处理后的message */
		const message = resolveResError(code, error.message)
		window.$message?.error(message)
		return Promise.reject({ code, message, error })
	}
	const { data, status, config } = error.response
	const code = data?.code ?? status
	const message = resolveResError(code, data?.message ?? error.message)
	let originalRequest = error.config;
	let refreshToken = localStorage.getItem('refresh_token');
	switch (code) {
		case 400:
			if (message == '用户不存在') {
				return Promise.reject({ code, message, error })
			}
			break;
		case 401:
			if (refreshToken && !originalRequest._retry && !requesting) {
				originalRequest._retry = true;
				requesting = true
				try {
					// 使用refresh token尝试获取新的tokens/
					refreshToken = localStorage.getItem('refresh_token');
					console.log("刷新refreshToken");
					console.log(refreshToken);
					
					const refreshResponse = await axios.post('/api/refresh', {
						"token": refreshToken
					}).then((res) => {
						return res;
					}).catch((e) => {
						// 刷新token失效会跳转下面的catch
						return e;
					})

					if (refreshResponse?.data.accessToken) {
						localStorage.setItem('access_token', refreshResponse.data.accessToken);
						localStorage.setItem('refresh_token', refreshResponse.data.refreshToken);
						// 在原始请求中添加新的access token,并标记为重试请求
						originalRequest.headers.Authorization = `Bearer ${refreshResponse.accessToken}`;
						requesting = false
						// 重新发起请求
						return await server(originalRequest);
					}
				} catch (refreshError) {
					// 若刷新token失败,清除存储的tokens并通知用户重新登录
					localStorage.removeItem('access_token');
					localStorage.removeItem('refresh_token');
					alert('登录过期,请重新登录');
					console.log("刷新token失败");
					requesting = false
				}
			} else {
				// 若无refresh token,直接提示用户重新登录
				localStorage.removeItem('access_token');
				localStorage.removeItem('refresh_token');
				console.log("无刷新token");
				alert('登录过期,请重新登录');
			}
			break;
		case 403:
			console.log("没有权限");
			break;
	}
	/** 需要错误提醒 */
	!config?.noNeedTip && message && window.$message?.error(message)
	return Promise.reject({ code, message, error: error.response?.data || error.response })
}
server.interceptors.request.use(reqResolve, reqReject)
server.interceptors.response.use(resResolve, resReject)

export default server

unitls/helper.js

export function resolveResError(code, message) {
	switch (code) {
		case 401:
			message = '登录已过期,是否重新登录'
			break
		case 11007:
		case 11008:
			message = '退出登录'
			break
		case 403:
			message = '请求被拒绝'
			break
		case 404:
			message = '请求资源或接口不存在'
			break
		case 500:
			message = '服务器发生异常'
			break
		default:
			message = message ?? `【$[code]】: 未知异常!`
			break
	}
	return message
}

根目录下添加.env配置环境

VITE_TITLE = '待煎的闲鱼'
# 是否使用Hash路由
VITE_USE_HASH = 'true'

# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/'

# 代理配置-target 本地服务
VITE_PROXY_TARGET = 'http://localhost:3000' 

根目录下创建vite.config.js配置代理

import path from 'path'
import { defineConfig, loadEnv } from 'vite'
import Vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => {
	const isBuild = command === 'build'
	const viteEnv = loadEnv(mode, process.cwd())
	const { VITE_TITLE, VITE_PUBLIC_PATH, VITE_PROXY_TARGET } = viteEnv
	return {
		plugins: [Vue()],
		base: VITE_PUBLIC_PATH || '/',
		resolve: {
			alias: {
				'@': path.resolve(process.cwd(), 'src'),
				'~': path.resolve(process.cwd()),
			},
		},
		server: {
			port: 3200, // 设置服务启动端口号
			// open: true, // 设置服务启动时是否自动打开浏览器
			cors: true, // 允许跨域
			// 设置代理,根据我们项目实际情况配置
			proxy: {
				'/api': { //api是自行设置的请求前缀,按照这个来匹配请求,有这个字段的请求,就会进到代理来
					target: "http://localhost:3000", //是自己需要调的接口的前缀域名
					ws: false,
					changeOrigin: true
				},
			}
		}
	}
})

在这里插入图片描述

以上就是Vue实现双token无感刷新的示例代码的详细内容,更多关于Vue双token无感刷新的资料请关注脚本之家其它相关文章!

相关文章

  • vue3中$attrs的变化与inheritAttrs的使用详解

    vue3中$attrs的变化与inheritAttrs的使用详解

    $attrs现在包括class和style属性。 也就是说在vue3中$listeners不存在了,vue2中$listeners是单独存在的,在vue3 $attrs包括class和style属性, vue2中 $attrs 不包含class和style属性,这篇文章主要介绍了vue3中$attrs的变化与inheritAttrs的使用 ,需要的朋友可以参考下
    2022-10-10
  • vue父组件触发事件改变子组件的值的方法实例详解

    vue父组件触发事件改变子组件的值的方法实例详解

    这篇文章主要介绍了vue父组件触发事件改变子组件的值的实例代码,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-05-05
  • vue cli3 项目中如何使用axios发送post请求

    vue cli3 项目中如何使用axios发送post请求

    这篇文章主要介绍了vue cli3 项目中如何使用axios发送post请求,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-04-04
  • vue+threejs写物体动画之物体缩放动画效果

    vue+threejs写物体动画之物体缩放动画效果

    最近在vue中安装Three.js,无聊顺便研究一些关于3D图形化库,下面这篇文章主要给大家介绍了关于vue+threejs写物体动画之物体缩放动画效果的相关资料,需要的朋友可以参考下
    2022-10-10
  • vue跳转页面并且实现参数传递接受示例

    vue跳转页面并且实现参数传递接受示例

    这篇文章主要为大家介绍了vue跳转页面并且实现参数传递接受示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-06-06
  • vue3 api自动导入神器推荐

    vue3 api自动导入神器推荐

    在做vue3项目中时,每次使用都需要先进行引入,下面这篇文章主要给大家介绍了关于vue3 api自动导入的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-02-02
  • vue项目中使用crypto-js实现加密解密方式

    vue项目中使用crypto-js实现加密解密方式

    这篇文章主要介绍了vue项目中使用crypto-js实现加密解密方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-05-05
  • Vue 实现输入框新增搜索历史记录功能

    Vue 实现输入框新增搜索历史记录功能

    这篇文章主要介绍了Vue 输入框新增搜索历史记录功能,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-10-10
  • vue实现点击当前标签高亮效果【推荐】

    vue实现点击当前标签高亮效果【推荐】

    这篇文章主要介绍了vue实现点击当前标签高亮效果的思路详解,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友参考下吧
    2018-06-06
  • 解决vue-cli中stylus无法使用的问题方法

    解决vue-cli中stylus无法使用的问题方法

    这篇文章主要介绍了解决vue-cli中stylus无法使用的问题方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-06-06

最新评论