十一、vue3后台项目系列——封装请求,存储token,api统一化管理,封装token的处理工具
一、前言(建议搭配第十二章进行阅读)
上一篇我们搭建了登录页面,其中涉及到登录按钮之后触发的登录事件。在通过表单校验之后,会调用登录接口,获取token。之后再将token进行存储,可实现在有效期无需重复登录的功能,也是对一些需要登录之后才能访问的页面作一个权限控制,总之这个token很关键,只有在登录成功后,才能获取到该值,获取之后,我们应该存放在用户模块的状态管理中,以及存到cookie中。
二、为什么要存到cookie中
1. 自动携带,无需手动处理请求头
Cookie 最核心的优势是浏览器自动管理:一旦服务器通过 Set-Cookie
指令将 Token 写入 Cookie,后续所有向该服务器的请求(包括同域、符合路径 / 域名规则的请求),浏览器都会自动将 Cookie 附加到请求头的 Cookie
字段中,无需前端手动写代码(如 axios.interceptors
拦截器添加 Authorization: Bearer Token
)。
对比 localStorage 的差异:
如果 Token 存在 localStorage,前端必须手动在每个请求中添加请求头,例如:
// localStorage 需手动处理请求头
axios.interceptors.request.use(config => {const token = localStorage.getItem('token');if (token) config.headers.Authorization = `Bearer ${token}`;return config;
});
而 Cookie 无需这一步,减少前端代码冗余,也避免遗漏请求的风险。
2. 支持「HttpOnly」安全属性,防御 XSS 攻击
XSS(跨站脚本攻击)是前端 Token 存储的最大威胁之一:攻击者通过注入恶意脚本(如 <script>盗取localStorage.token</script>
),可轻松获取 localStorage/sessionStorage 中存储的 Token。
而 Cookie 支持 HttpOnly 属性:一旦设置 HttpOnly=true
,浏览器会禁止前端通过 JavaScript(如 document.cookie
)读取或修改该 Cookie,只能由浏览器在请求时自动携带。这就从根本上阻断了 XSS 脚本盗取 Token 的可能,安全性远高于 localStorage。
示例:服务器设置 HttpOnly Cookie
后端返回响应时,通过 Set-Cookie
指令添加 Token 并配置安全属性:
// 响应头示例(后端返回)
Set-Cookie: token=admin-token; HttpOnly; Secure; SameSite=Strict; Path=/; Domain=your-domain.com
HttpOnly
:禁止前端 JS 访问,防 XSS;Secure
:仅在 HTTPS 协议下传输,防中间人窃取;SameSite=Strict
:仅同域请求携带,防 CSRF(下文会讲);Path=/
:该域名下所有路径的请求都携带 Cookie。
3. 支持「SameSite」属性,防御 CSRF 攻击
CSRF(跨站请求伪造)是另一种常见攻击:攻击者诱导用户在已登录的状态下,访问恶意网站并发送伪造请求(如转账、修改密码),利用浏览器自动携带的 Cookie 冒充用户操作。
Cookie 的 SameSite 属性 专门用于防御 CSRF:
SameSite=Strict
:仅当请求来自「当前域名」时,浏览器才携带 Cookie(完全禁止跨站携带);SameSite=Lax
:仅允许「GET 方法的跨站导航请求」携带(如从百度跳转你的网站),禁止 POST 等修改型请求携带;SameSite=None
:允许跨站携带(需配合Secure
属性,仅 HTTPS 可用)。
通过设置 SameSite=Strict/Lax
,可大幅降低 CSRF 攻击风险。而 localStorage 存储的 Token 虽不直接触发 CSRF(需手动加请求头),但一旦被 XSS 盗取,风险同样极高。
4. 可配置「过期时间」,自动失效
Cookie 支持 Max-Age
或 Expires
属性,可设置 Token 的过期时间:
Max-Age=86400
:Cookie 从创建起存活 86400 秒(1 天),过期后自动删除;Expires=Wed, 17 Sep 2025 08:00:00 GMT
:指定具体过期时间。
这让 Token 的「自动失效」机制更易实现,无需前端手动管理过期逻辑(如 localStorage 需要前端判断 expireTime
是否过期)。
5. 跨域场景的可控性(配合 CORS)
在跨域请求中(如前端域名 a.com
调用后端 api.b.com
),Cookie 可通过 CORS 配置实现「跨域携带」:
- 后端需在响应头中设置
Access-Control-Allow-Credentials: true
,允许跨域请求携带 Cookie; - 前端请求时需配置
withCredentials: true
(如 axios 中axios.defaults.withCredentials = true
); - 后端
Set-Cookie
需指定Domain=api.b.com
并确保SameSite=None
+Secure
。
虽然配置稍复杂,但相比 localStorage 在跨域时的「手动传 Token」,Cookie 的自动携带仍更便捷,且安全性(HttpOnly)仍在。
什么时候不适合用 Cookie 存 Token?
Cookie 也有局限性,以下场景更适合 localStorage/sessionStorage
- 前端需要主动读取 Token:例如前端需根据 Token 解析用户角色(如 JWT Token 可前端解码),而 HttpOnly Cookie 禁止前端读取;
- 跨域场景复杂且无需高安全:例如多个子域名间共享 Token,且无需防御 XSS(如内部管理系统);
- Token 体积过大:Cookie 有大小限制(约 4KB),若 Token 是长字符串(如包含复杂权限的 JWT),可能超出限制。
总结:Cookie 存 Token 的核心价值
优势 | 具体作用 | 对比 localStorage |
---|---|---|
自动携带 | 减少前端代码,避免遗漏请求 | 需手动加请求头,易遗漏 |
HttpOnly 属性 | 防 XSS 盗取,安全性高 | 可被 JS 读取,易遭 XSS 攻击 |
SameSite 属性 | 防 CSRF 攻击 | 不直接防 CSRF,但 XSS 风险更高 |
可配置过期时间 | 自动失效,无需前端管理 | 需前端手动判断过期时间 |
当你的项目需要高安全性(防 XSS/CSRF)、低前端维护成本(自动携带)时,Cookie 是 Token 存储的最优选择(尤其是后端渲染、管理系统、金融类应用)。
三、封装请求方法
好处我觉得有以下:
1.统一化管理、全局配置:比如统一设置请求base Url,统一设置请求需要携带的请求头,改base url时如果有更改,这时候替换就会很方便。
2.减少代码冗余:如果没有对请求进行封装,可能会有重复性的代码,比如:共同的请求base url,在更换时,要改动很多地方,以及更多相同的地方。
3.对请求、响应进行拦截做二次处理:可以对请求和响应做二次处理,包括对错误的统一处理,可配合element使用错误提示组件,对不同的响应码做统一的判断处理等等。
参考代码src/utlis/request.js/:
import axios from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useUserStore } from '@/store';
import { getToken } from '@/utils/auth';// 创建 axios 实例
const service = axios.create({baseURL: import.meta.env.VITE_APP_BASE_API,timeout: 5000
});// 请求拦截器 (保持不变)
service.interceptors.request.use(config => {const userStore = useUserStore();if (userStore.token) {config.headers['X-Token'] = getToken();}return config;},error => {console.error('request error:', error);return Promise.reject(error);}
);// 响应拦截器
service.interceptors.response.use(response => {const res = response.data;// 当后端返回的自定义状态码表示成功时,返回响应数据if (res.code === 200) {return res;} // 当自定义状态码不为 200 时,统一处理为业务错误ElMessage({message: res.message || '错误',type: 'error',duration: 5 * 1000});// 特别处理需要重新登录的业务状态码if (res.code === 401 || res.code === 508 || res.code === 512) {ElMessageBox.confirm('登录状态已过期,您可以取消以停留在此页面,或者重新登录', '登录过期', {confirmButtonText: '重新登录',cancelButtonText: '取消',type: 'warning'}).then(() => {const userStore = useUserStore();userStore.resetToken().then(() => {location.reload();});});}// 返回一个被拒绝的 Promise,业务代码将进入 catch 块return Promise.reject(new Error(res.message || '错误'));},error => {// 处理 HTTP 状态码错误(例如,404, 500)// 通常,后端的“账号或密码错误”不会返回 HTTP 401,而是自定义的业务状态码,// 所以这个拦截器通常只处理网络或服务器错误。// 如果后端确实返回了 HTTP 401,我们在这里统一处理一次if (error.response.status === 401) {ElMessage({message: '用户未授权,请重新登录',type: 'error',duration: 5 * 1000});const userStore = useUserStore();userStore.resetToken().then(() => {location.reload();});} else {ElMessage({message: error.message || '网络异常',type: 'error',duration: 5 * 1000});}return Promise.reject(error);}
);export default service;
四、对token的处理
推荐使用js-cookie,js-cookie
是一个轻量级的 JavaScript 库,专门用于简化浏览器中 Cookie 的创建、读取、修改和删除操作。它封装了原生 Cookie 操作的复杂细节,提供了简洁的 API,让开发者能更高效地处理 Cookie。
安装:
npm install -D js-cookie
在utlis文件下创建auth.js文件:
import Cookies from 'js-cookie'const TokenKey = 'Admin-Token'export function getToken() {return Cookies.get(TokenKey)
}export function setToken(token) {return Cookies.set(TokenKey, token)
}export function removeToken() {return Cookies.remove(TokenKey)
}
主要调用这些方法,就能配合cookie进行操作了。
五、统一管理api
对登录这样的用户操作相关的请求,我们对请求进行模块化区分。方便管理,代码结构清晰,不必都写在页面中。这时候封装好的请求方法就有作用了。
src/api/user.js
import request from '@/utils/request'export function login(data) {return request({url: '/admin/user/login',method: 'post',data})
}export function getInfo(token) {return request({url: '/admin/user/info',method: 'get',params: { token }})
}export function logout() {return request({url: '/admin/user/logout',method: 'post'})
}
六、模拟后端接口响应(mock)
mock/user.js
// 模拟不同用户返回的token
const tokens = {admin: {token: 'admin-token'},editor: {token: 'editor-token'}
}
// 俩种权限的用户信息
const users = {'admin-token': {roles: ['admin'],introduction: 'I am a super administrator',avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',name: 'Super Admin'},'editor-token': {roles: ['editor'],introduction: 'I am an editor',avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',name: 'Normal Editor'}
}export default [{url: '/admin/user/login',type: 'post',response: config => {const { username } = config.bodyconst { password } = config.bodyif((username === 'admin' || username === 'editor') && password === 'CDC123456'){return {code: 200,message: '登录成功',data: {token: tokens[username].token }}} else {return {code: 500,message: '用户名或密码错误'}}}},// 获取用户信息{url: '/admin/user/info',type: 'get',response: config => {const { token } = config.queryconst info = users[token]// mock errorif (!info) {return {code: 401,message: '登录错误,无法获取用户详情。'}}return {code: 200,data: info}}},// 退出操作{url: '/admin/user/logout',type: 'post',response: _ => {return {code: 200,data: 'success'}}}
]
七、更新vite配置
如果设置了开发环境和生产环境的不同baseURL,则需要在vite中声明,不然会跟mock中定义的地址不一样,产生404报错,原因就是mock中的地址不会携带这些环境的baseURL,而设置了这些环境的baseURL,则会在请求时带上,到时候俩者不对应就会导致地址不一致,产生报错。
主要是这一行:
urlPrefix: '/dev-api'
完整vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { viteMockServe } from "vite-plugin-mock";
import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
import path from "path";export default defineConfig({// 配置路径别名resolve: {alias: {"@": path.resolve(__dirname, "src"),},},plugins: [vue(),// 配置mock服务viteMockServe({// 本地开发时启用mocklocalEnabled: true,// mock文件所在目录mockPath: "./mock/",urlPrefix: '/dev-api'}),createSvgIconsPlugin({// 指定SVG图标存放目录(使用@别名的话可以写成 '@/icons/svg',但这里需要绝对路径)iconDirs: [path.resolve(process.cwd(), "src/icons/svg")],// 指定symbolId格式(与SvgIcon组件中的iconName对应)symbolId: "icon-[dir]-[name]",// 配置SVGO优化svgoOptions: {multipass: true,plugins: [{name: "removeAttrs",params: {attrs: ["fill", "fill-rule"],},},],},}),],
});