Appearance
下面给你两部分:(A)最佳实践要点(基于规范与资料的广泛检索),(B)最小改造代码片段(只贴需要改的地方,复杂逻辑行上方有注释)。
A)最佳实践要点(结论)
- 不要把 access_token 暴露在 URL(尤其是
?access_token=查询串):URL 会进浏览器历史、服务器/代理日志与 Referer,存在泄露风险;规范明确不推荐/禁止用查询参数传递 Bearer Token。应在前端拿到后立即清理 URL。(datatracker.ietf.org) - 优先方案:拿到链接里的一次性 token,**调用后端“交换接口”**换成 HttpOnly + Secure + SameSite=Lax 的会话 Cookie,前端不长期保存 token。这样可避免 XSS 窃取本地存储里的 token。(cheatsheetseries.owasp.org)
- 如果暂时改不了后端:退而求其次仅内存或
sessionStorage短期保存,并设置较短过期;避免localStorage长期持久化(XSS 风险)。(Auth0) - 清理 URL 的正确方式:处理完就用
history.replaceState移除access_token(不新增历史记录,不刷新页面),兼容 query 与 hash。(MDN文档) - 额外加固:设置
Referrer-Policy(或<meta name="referrer">)减少 Referer 携带参数的外泄面。(MDN文档)
注:长远看,外部 SSO/三方登录应采用 Authorization Code + PKCE,而不是隐式把
access_token放在 URL。(datatracker.ietf.org)
B)最小改造代码片段
1)在 全局路由前置守卫里捕获 access_token(支持 ? 与 #),先“交换”、后清 URL
文件:
src/router/index.ts(或你注册全局守卫的地方)
ts
// 复杂逻辑:捕获 URL(query/hash)里的 access_token,首选调用后端兑换会话;无论成功与否都清理 URL
router.beforeEach(async (to, _from, next) => {
const auth = useAuthStore()
// 上一行注释:优先从 query 取 ?access_token=...
const queryToken = (to.query.access_token as string | undefined) ?? undefined
// 上一行注释:兼容 #access_token=...(如历史隐式流/深链)
let fragmentToken: string | undefined
if (!queryToken && typeof window !== 'undefined') {
const hash = window.location.hash.startsWith('#') ? window.location.hash.slice(1) : window.location.hash
fragmentToken = new URLSearchParams(hash).get('access_token') ?? undefined
}
const linkToken = queryToken ?? fragmentToken
if (linkToken) {
try {
// 上一行注释:最佳实践——向后端兑换一次性链接 token 为 HttpOnly 会话
await auth.exchangeLinkToken(linkToken)
// 如果后端暂不可改,可临时启用:auth.setBearerToken(linkToken)
} finally {
// 上一行注释:用 replaceState 彻底清理 URL 中的 token(不新增历史,不刷新)
const url = new URL(window.location.href)
url.searchParams.delete('access_token')
url.hash = ''
window.history.replaceState({}, '', url.toString())
}
}
next()
})2)在 Pinia Auth 仓库里新增两个方法(首选 exchange,备选 bearer)
文件:
src/stores/auth.ts(根据你的实际路径调整)
ts
// 复杂逻辑:调用后端交换一次性链接 token -> 后端 Set-Cookie: session=...; HttpOnly/Secure/SameSite=Lax
async function exchangeLinkToken(linkToken: string): Promise<void> {
await http.post('/auth/link-login', { token: linkToken }, { withCredentials: true })
// 上一行注释:统一拉取用户信息,走你现有“已登录”分支
await fetchCurrentUser()
}
// 复杂逻辑(备选,不改后端时使用):把 access_token 暂存于 sessionStorage,并驱动后续请求头
function setBearerToken(accessToken: string): void {
state.accessToken = accessToken
sessionStorage.setItem('access_token', accessToken)
}3)Axios 实例(按你选的方案二选一)
- 若采用后端会话 Cookie(推荐):保证跨域时带上凭证
ts
// 复杂逻辑:Cookie 会话模式需要携带凭证
export const http = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, withCredentials: true })- 若采用 Bearer 兜底模式:请求前附加
Authorization
ts
// 复杂逻辑:在请求头里附加 Bearer
http.interceptors.request.use((config) => {
const token = useAuthStore().accessToken
if (token) config.headers = { ...config.headers, Authorization: `Bearer ${token}` }
return config
})4)可选:减少 Referer 外泄(index.html 里加一行即可)
html
<!-- 复杂逻辑:降低 URL 参数通过 Referer 泄露的风险 -->
<meta name="referrer" content="no-referrer" />运维/安全补充(落地建议)
- 生成“带 token 的链接”时应使用一次性、极短 TTL(如 1–5 分钟)、限定来源/用途的临时票据,后端兑换后立刻作废;并限制可用范围(Scope)。(datatracker.ietf.org)
- 立刻在前端清理 URL(见上)并在 Nginx/网关层设置
Referrer-Policy头为no-referrer或至少strict-origin-when-cross-origin。(MDN文档) - 若必须前端保存 Bearer,优先 内存/
sessionStorage,并设置不活跃超时与刷新即失效;避免localStorage的长期持久化。(Auth0)
需要我把上述片段按你项目的具体文件结构“就地定位”一下吗?(我会继续遵循“只给改动片段+中文行内注释”的风格。)