Vue实现双token无感刷新的示例代码
作者:RCX明
双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无感刷新的资料请关注脚本之家其它相关文章!