vue-vben-admin 5.8.0 修复解读:Ant Design Vue Next 变体的 refresh/logout 请求 withCredentials 处理
2026/9/10 13:15:54 网站建设 项目流程

vue-vben-admin 5.8.0 修复解读:Ant Design Vue Next 变体的 refresh/logout 请求 withCredentials 处理

【免费下载链接】vue-vben-adminA modern vue admin panel built with Vue3, Shadcn UI, Vite, TypeScript, and Monorepo. It's fast!项目地址: https://gitcode.com/GitHub_Trending/vu/vue-vben-admin

本文基于apps/web-antdv-next的 5.8.0 版本 CHANGELOG 记录,深入解读该版本中一项关键的认证修复——在refresh(刷新 accessToken)与logout(退出登录)两个请求上显式携带withCredentials配置,并结合仓库源码与后端 Mock 实现,剖析其背后的 httpOnly Cookie 刷新令牌机制、请求拦截器刷新流程与验证方式。

一、这次 5.8.0 修复了什么

在 CHANGELOG 的5.8.0版本下,有一条 Patch Changes 记录(PR #8308):

fix(apps): pass withCredentials as request config for refresh/logout

修复者kilisamemarisaaawithCredentials作为请求配置传入 refresh 与 logout 两个接口。这虽然只是一行配置的变更,但它修复的是一个在**跨域 + 携带 Cookie 的刷新令牌(Refresh Token)**场景下极易出现、且难以排查的认证失效问题。

要理解这次修复的价值,需要先回答三个问题:

  1. withCredentials是什么、为什么刷新令牌必须依赖它;
  2. 为什么login能正常工作,而refresh/logout却可能拿不到 Cookie;
  3. 修复之后,完整的令牌刷新链路是如何串起来的。

下文将逐层展开,并用仓库源码作为证据。

二、withCredentials:让跨域请求带上 Cookie

浏览器对跨域请求默认不会携带 Cookie。XMLHttpRequest/fetchwithCredentials选项用于指示浏览器在跨域请求中是否携带目标域名的 Cookie 与 HTTP 认证信息

在 vue-vben-admin 中,前端应用与后端接口往往部署在不同的源(例如前端localhost:5173、Mock 后端localhost:5320,参见各应用vite.config.ts中的代理配置),因此只要涉及"通过 Cookie 传递凭证"的接口,就必须显式开启withCredentials: true,否则请求到达后端时 Cookie 为空,后端会认为请求未携带刷新令牌而直接拒绝。

而 vue-vben-admin 恰好把refreshToken 存放在 httpOnly Cookie中(而不是像 accessToken 那样放在请求头Authorization: Bearer <token>里),这正是本次修复的技术背景,详见下文第三节。

修复点对应的源码

本次修复落实到web-antdv-next的认证 API 定义中,见 apps/web-antdv-next/src/api/core/auth.ts:

/** * 刷新accessToken */ export async function refreshTokenApi() { return baseRequestClient.post<AuthApi.RefreshTokenResult>( '/auth/refresh', undefined, { withCredentials: true, }, ); } /** * 退出登录 */ export async function logoutApi() { return baseRequestClient.post('/auth/logout', undefined, { withCredentials: true, }); }

从代码可以看到:

  • refreshTokenApi使用baseRequestClient(不携带 Authorization 头的裸请求客户端)调用POST /auth/refresh,并把withCredentials: true作为第三参数(请求配置)传入;
  • logoutApi同样以withCredentials: true调用POST /auth/logout
  • loginApi走的是requestClient,登录成功后由后端通过Set-Cookie种下刷新令牌,因此登录请求本身不依赖已有 Cookie。

baseRequestClient的定义位于 apps/web-antdv-next/src/api/request.ts:

export const requestClient = createRequestClient(apiURL, { responseReturn: 'data', }); export const baseRequestClient = new RequestClient({ baseURL: apiURL });

三、为什么要靠 Cookie 传递刷新令牌:Mock 后端的凭证设计

web-antdv-next的 Mock 后端(Nitro 应用)中,刷新令牌的生命周期完全由 Cookie 驱动。核心实现在 apps/backend-mock/utils/cookie-utils.ts:

export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) { deleteCookie(event, 'jwt', { httpOnly: true, sameSite: 'none', secure: true, }); } export function setRefreshTokenCookie( event: H3Event<EventHandlerRequest>, refreshToken: string, ) { setCookie(event, 'jwt', refreshToken, { httpOnly: true, maxAge: 24 * 60 * 60, // unit: seconds sameSite: 'none', secure: true, }); } export function getRefreshTokenFromCookie(event: H3Event<EventHandlerRequest>) { const refreshToken = getCookie(event, 'jwt'); return refreshToken; }

该 Cookie 的关键属性值得逐条对照:

Cookie 属性取值作用
httpOnlytrue脚本无法通过document.cookie读取,降低 XSS 窃取刷新令牌的风险
sameSitenone允许跨站携带 Cookie(配合跨域部署)
securetrue仅允许 HTTPS 传输(本地开发环境需注意)
maxAge24 * 60 * 60秒(1 天)刷新令牌 Cookie 的存活时长

刷新令牌本身是 JWT,由 apps/backend-mock/utils/jwt-utils.ts 生成与校验:

const ACCESS_TOKEN_SECRET = 'access_token_secret'; const REFRESH_TOKEN_SECRET = 'refresh_token_secret'; export function generateAccessToken(user: UserInfo) { return jwt.sign(user, ACCESS_TOKEN_SECRET, { expiresIn: '7d' }); } export function generateRefreshToken(user: UserInfo) { return jwt.sign(user, REFRESH_TOKEN_SECRET, { expiresIn: '30d', }); }

可以看到默认策略是:accessToken 有效期 7 天、走请求头;refreshToken 有效期 30 天、走 httpOnly Cookie。accessToken 过期后,前端用 refreshToken 换取新的 accessToken,无需用户重新输入密码。

四、登录 → 刷新 → 退出的完整链路

1. 登录:种下刷新令牌 Cookie

登录接口 apps/backend-mock/api/auth/login.post.ts 在校验用户名密码通过后,同时生成 accessToken 与 refreshToken:

const accessToken = generateAccessToken(findUser); const refreshToken = generateRefreshToken(findUser); setRefreshTokenCookie(event, refreshToken); return useResponseSuccess({ ...findUser, accessToken, });

即:accessToken 通过响应体返回给前端(随后被存入 access store),refreshToken 通过Set-Cookie写入浏览器的 httpOnly Cookie。

2. 刷新:读取 Cookie 并签发新 accessToken

刷新接口 apps/backend-mock/api/auth/refresh.post.ts 的处理逻辑:

export default defineEventHandler(async (event) => { const refreshToken = getRefreshTokenFromCookie(event); if (!refreshToken) { return forbiddenResponse(event); } clearRefreshTokenCookie(event); const userinfo = verifyRefreshToken(refreshToken); if (!userinfo) { return forbiddenResponse(event); } const findUser = MOCK_USERS.find( (item) => item.username === userinfo.username, ); if (!findUser) { return forbiddenResponse(event); } const accessToken = generateAccessToken(findUser); setRefreshTokenCookie(event, refreshToken); return accessToken; });

这段代码清晰展示了 Cookie 刷新机制的两个关键点:

  • 读取凭证的唯一途径就是请求携带的jwtCookiegetRefreshTokenFromCookie)。如果前端请求没有withCredentials: true,跨域场景下 Cookie 不会被携带,接口直接返回 403——这正是本次修复要解决的场景;
  • 旋转式刷新(Refresh Token Rotation):每次刷新都会先clearRefreshTokenCookie,验证成功后重新setRefreshTokenCookie,即刷新令牌的 Cookie 被重新种下(Mock 实现中复用了原 refreshToken,生产环境可签发新令牌并作废旧令牌)。

3. 退出:清除 Cookie

退出接口 apps/backend-mock/api/auth/logout.post.ts 同样依赖 Cookie:

export default defineEventHandler(async (event) => { const refreshToken = getRefreshTokenFromCookie(event); if (!refreshToken) { return useResponseSuccess(''); } clearRefreshTokenCookie(event); return useResponseSuccess(''); });

如果logout请求没有携带withCredentials: true,服务端读取不到jwtCookie,虽然接口仍返回成功,但服务端无法真正清除服务端视角下的会话状态;而前端清除的只是内存中的 accessToken,刷新令牌 Cookie 仍残留在浏览器中,造成"退出后刷新令牌依然有效"的隐患。修复后浏览器会随请求携带 Cookie,服务端得以可靠地清除它。

五、前端拦截器如何串起整条刷新链路

refreshTokenApi/logoutApi被谁调用?答案在 apps/web-antdv-next/src/api/request.ts 中定义的两个拦截器中。

1. 请求拦截器:为每个请求注入 Authorization 头

client.addRequestInterceptor({ fulfilled: async (config) => { const accessStore = useAccessStore(); config.headers.Authorization = formatToken(accessStore.accessToken); config.headers['Accept-Language'] = preferences.app.locale; return config; }, });

从 access store 中读取当前 accessToken,拼成Bearer <token>写入Authorization头,并附带当前语言偏好。

2. 响应拦截器:401 时自动刷新并重放请求

authenticateResponseInterceptor位于 packages/effects/request/src/request-client/preset-interceptors.ts,其核心逻辑:

rejected: async (error) => { const { config, response } = error; // 如果不是 401 错误,直接抛出异常 if (response?.status !== 401) { throw error; } // 判断是否启用了 refreshToken 功能 // 如果没有启用或者已经是重试请求了,直接跳转到重新登录 if (!enableRefreshToken || config.__isRetryRequest) { await doReAuthenticate(); throw error; } // 如果正在刷新 token,则将请求加入队列,等待刷新完成 if (client.isRefreshing) { return new Promise((resolve) => { client.refreshTokenQueue.push((newToken: string) => { config.headers.Authorization = formatToken(newToken); resolve(client.request(config.url, { ...config })); }); }); } // 标记开始刷新 token client.isRefreshing = true; // 标记当前请求为重试请求,避免无限循环 config.__isRetryRequest = true; try { const newToken = await doRefreshToken(); // 处理队列中的请求 client.refreshTokenQueue.forEach((callback) => callback(newToken)); // 清空队列 client.refreshTokenQueue = []; return client.request(error.config.url, { ...error.config }); } catch (refreshError) { // 如果刷新 token 失败,处理错误(如强制登出或跳转登录页面) client.refreshTokenQueue.forEach((callback) => callback('')); client.refreshTokenQueue = []; console.error('Refresh token failed, please login again.'); await doReAuthenticate(); throw refreshError; } finally { client.isRefreshing = false; } },

doRefreshToken正是调用本次修复的refreshTokenApi

async function doRefreshToken() { const accessStore = useAccessStore(); const resp = await refreshTokenApi(); const newToken = resp.data; accessStore.setAccessToken(newToken); return newToken; }

刷新失败时的兜底doReAuthenticate则会清空 accessToken,并根据preferences.app.loginExpiredMode决定是弹出登录过期弹窗还是直接登出:

async function doReAuthenticate() { console.warn('Access token or refresh token is invalid or expired. '); const accessStore = useAccessStore(); const authStore = useAuthStore(); accessStore.setAccessToken(null); if ( preferences.app.loginExpiredMode === 'modal' && accessStore.isAccessChecked ) { accessStore.setLoginExpired(true); } else { await authStore.logout(); } }

刷新失败时的报错文案与状态码映射

同一文件中的errorMessageResponseInterceptor负责把网络错误与 HTTP 状态码翻译成用户可读的提示,其中 401 对应"未登录或登录已过期":

case 401: { errorMessage = $t('ui.fallback.http.unauthorized'); break; }

六、修复的验证方式

修复是否生效,可以通过以下方式在本地验证:

  1. 启动应用与 Mock 后端:在web-antdv-next目录执行pnpm dev(对应 package.json 中的"dev": "pnpm vite --mode development"),同时启动apps/backend-mock
  2. 登录:调用loginApi登录成功后,在浏览器开发者工具中确认响应Set-Cookie: jwt=...,且该 Cookie 属性包含HttpOnlySameSite=NoneSecure
  3. 观察刷新请求:将 accessToken 改短使其过期(或直接构造一个 401 响应),触发authenticateResponseInterceptor,在 Network 面板检查POST /auth/refresh请求的Request Headers 中是否携带了Cookie: jwt=...。修复前该请求不带 Cookie、返回 403;修复后携带 Cookie 并返回新的 accessToken,随后原请求被自动重放。
  4. 观察退出请求:点击退出登录,检查POST /auth/logout请求同样携带Cookie,响应后服务端清除jwtCookie。

如果部署在跨域环境,还需确保后端Access-Control-Allow-Credentials: trueAccess-Control-Allow-Origin不是*(必须为具体源),否则即使前端开启withCredentials,浏览器也会拦截带凭证的跨域响应。

七、5.8.0 版本中该应用的其他同步更新

本次修复属于@vben/web-antdv-next应用的补丁变更,随同版本还同步升级了一批底层依赖包(均为5.8.0版本),它们在 CHANGELOG 中列出,并在 package.json 中以workspace:*的形式引用:

依赖包说明
@vben/styles全局样式与主题样式(含antdv-next适配样式)
@vben/plugins应用插件集合
@vben/preferences全局偏好配置(含enableRefreshTokenloginExpiredMode等开关)
@vben/layouts布局系统
@vben/common-ui通用 UI 组件
@vben/access权限控制
@vben/hooks组合式函数
@vben/constants常量定义
@vben/request请求客户端与拦截器(本次修复依赖的RequestClient即来自此包)
@vben/icons图标
@vben/locales国际化
@vben/storesPinia store 集合
@vben/types类型定义
@vben/utils工具函数

UI 层使用antdv-next(Ant Design Vue Next)组件库,因此该应用的启动入口 bootstrap.ts 会引入@vben/styles/antdv-next样式,并通过 adapter/component/index.ts 完成组件适配器的初始化。

八、小结

web-antdv-next5.8.0 的这则补丁变更虽小,却是"httpOnly Cookie 刷新令牌"安全模型能否在跨域环境下闭环的关键一环:

  • 修复前refresh/logout请求不带withCredentials,浏览器不携带jwtCookie,刷新令牌失效、退出无法清理服务端会话;
  • 修复后:两个接口显式携带凭证,配合请求拦截器实现 401 自动刷新、请求重放与失败兜底登出,形成完整的令牌生命周期管理。

如果你正在基于 vue-vben-admin 的任一应用变体(web-antdweb-antdv-nextweb-eleweb-naiveweb-tdesignplayground)进行二次开发,且后端将刷新令牌放入 httpOnly Cookie,请务必检查自己的refreshTokenApi/logoutApi是否同样携带了withCredentials: true——这是极易被遗漏、又直接影响登录态稳定性的细节。

【免费下载链接】vue-vben-adminA modern vue admin panel built with Vue3, Shadcn UI, Vite, TypeScript, and Monorepo. It's fast!项目地址: https://gitcode.com/GitHub_Trending/vu/vue-vben-admin

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询