Skip to content

下面给你两部分:(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)

需要我把上述片段按你项目的具体文件结构“就地定位”一下吗?(我会继续遵循“只给改动片段+中文行内注释”的风格。)

本站总访问