深入解析Cookie机制与前端应用实践

百里义霞 前端 阅读 1,569
赞 16 收藏
二维码
手机扫码查看
反馈

又踩坑了,登录态莫名其妙丢了

今天早上刚到公司,泡好咖啡准备摸鱼,结果测试群里一条消息炸了:用户登着登着突然跳登录页了。我第一反应是后端接口抽风,查了下日志发现 session 没过期,token 也正常返回,前端却拿不到 Cookie —— 明明上一秒还好好地带着 Set-Cookie 头。

深入解析Cookie机制与前端应用实践

这里我踩了个大坑:我以为是 axios 配置漏了 withCredentials: true,赶紧加上,发测试环境一试……还是不行。折腾了快一个小时,抓包看了十几遍请求,发现第一次登录请求确实收到了 Set-Cookie,但第二次请求(比如获取用户信息)就完全没带上这个 Cookie。

浏览器开发者工具里看 Application -> Cookies 根本没有这条记录。这就邪门了。

排查过程像在盲人摸象

我先怀疑是不是跨域问题。项目前端跑在 http://localhost:3000,后端是 https://api.jztheme.com,虽然 devServer 配了 proxy,但实际请求是直接打到线上域名的。于是我把本地启了个 HTTPS 服务(用 mkcert 搞了个本地证书),换成 https://localhost:3000 访问,心想总该满足 SameSite 的安全要求了吧?

结果还是不行。

后来翻 MDN 才意识到关键点:Cookie 的 Secure 属性意味着它只能通过 HTTPS 连接传输,但如果前端和后端不在同一个 origin 下,就算你本地开了 HTTPS,只要不是同源,浏览器默认也不会自动携带跨域 Cookie,除非你在请求时显式设置 credentials: 'include',而且服务端也得配合。

我这时候才想起来检查后端响应头:

Set-Cookie: sessionid=abc123; Path=/; Domain=jztheme.com; Secure; HttpOnly; SameSite=Lax

看到 Domain=jztheme.com 我就懂了——这玩意根本不会被 localhost 存下来。浏览器只允许当前页面所在 domain 或其父 domain 设置 Cookie。你现在让 jztheme.com 给 localhost 设置 Cookie?门都没有。

所以哪怕你 withCredentials: true 写得再对,浏览器压根就不收这个 Cookie,自然也就谈不上后续发送。

换方案:反向代理搞定开发环境

后来试了下发现最简单的解法就是:别让前端直接请求线上 API 域名。开发环境下统一走本地反代。

我在 vite.config.js 里加了这么一段:

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'https://api.jztheme.com',
        changeOrigin: true,
        secure: false,
        cookieDomainRewrite: 'localhost',
        cookiePathRewrite: '/'
      }
    }
  }
})

重点来了:changeOrigin: true 是为了让 Host 头改成目标服务器;secure: false 允许转发 HTTPS 时忽略证书错误(本地调试常见操作);最关键的是中间件会自动帮你 rewrite Set-Cookie 里的 Domain 和 Path。

不过 vite 自带的 proxy 并不直接支持 cookieDomainRewrite,这是我自己封装的中间件逻辑。真实代码其实是这样的:

import { createProxyMiddleware } from 'http-proxy-middleware'

export default defineConfig({
  server: {
    middlewareMode: false,
    proxy: {
      '/api': createProxyMiddleware({
        target: 'https://api.jztheme.com',
        changeOrigin: true,
        secure: false,
        onProxyRes: (proxyRes, req, res) => {
          const setCookie = proxyRes.headers['set-cookie']
          if (Array.isArray(setCookie)) {
            proxyRes.headers['set-cookie'] = setCookie.map(cookie =>
              cookie
                .replace(/Domain=[^;s]*/ig, 'Domain=localhost')
                .replace(/Secure;?/ig, '')
            )
          }
        }
      })
    }
  }
})

这里做了两件事:

  • 把所有 Set-Cookie 里的 Domain=jztheme.com 改成 Domain=localhost,这样浏览器愿意收
  • 去掉 Secure 标志,因为本地是 HTTP,带 Secure 的话 Chrome 直接无视这条 Cookie

改完之后刷新页面,登录成功,Cookie 出现在 localhost 下,后续请求也能正常带上,问题解决。

当然这方法有个小瑕疵:生产环境不能这么搞。所以我们用环境变量控制:

const isDev = import.meta.env.DEV

const proxyOptions = isDev
  ? {
      target: 'https://api.jztheme.com',
      changeOrigin: true,
      secure: false,
      onProxyRes: (proxyRes) => {
        const cookies = proxyRes.headers['set-cookie']
        if (!cookies) return
        proxyRes.headers['set-cookie'] = cookies.map(c =>
          c.replace(/Domain=[^;s]*/i, 'Domain=localhost').replace(/Secure;/i, '')
        )
      }
    }
  : null // 生产环境走正常部署,同域就没这些问题

// 然后传给 proxy

顺手补了个小工具函数

虽然主问题解决了,但我还是担心某些老接口可能漏设 SameSite,导致 Safari 或 iOS 微信里出问题。于是写了个简单检测脚本,在 devtools console 里跑一下看看当前页面有哪些可疑 Cookie:

function checkCookieIssues() {
  const url = new URL(window.location.href)
  const domain = url.hostname
  const cookies = document.cookie.split(';').map(c => c.trim())

  console.group('🔍 Cookie 安全检查')
  cookies.forEach(cookie => {
    const [key] = cookie.split('=')
    const value = document.cookie.match(new RegExp(${key}=([^;]+)))?.[1]
    
    // 实际上 JS 拿不到 HttpOnly 的值,但这只是提示
    if (value === undefined && cookie.includes('HttpOnly')) {
      console.warn(⚠️  ${key} 是 HttpOnly,JS 不可读)
    }

    // 检查是否设置了 Secure 但当前是 HTTP
    if (location.protocol === 'http:' && /Secure/i.test(cookie)) {
      console.error(❌ ${key} 设置了 Secure,但在 HTTP 下无效)
    }
  })

  console.log(✅ 当前域:${domain},共 ${cookies.length} 条 Cookie)
  console.groupEnd()
}

checkCookieIssues()

这个脚本其实帮我在另一个项目里发现了问题:某个第三方 SDK 注入的 Cookie 带了 Secure 却跑在内网 HTTP 环境下,结果一直登不进去。这种细节纯靠肉眼很难发现。

说点原理上的事

这次折腾让我重新看了遍 Cookie 的作用域规则。总结几点容易忽略的点:

  • Domain 必须是当前 host 的父级或相同。比如你不能让 baidu.com 给 taobao.com 设 Cookie,但可以给 .baidu.comsub.baidu.com
  • Secure 的 Cookie 只能在 HTTPS 下传输,包括发送和接收。就算你本地开 HTTPS,只要目标 API 是 HTTP,照样不会发 Secure Cookie
  • SameSite 默认是 Lax,意味着跨站 POST 请求都不会带 Cookie。如果是 iframe 里的表单提交、图片加载等场景要注意
  • HttpOnly 的 Cookie 无法被 JS 读取,防 XSS 的,但仍然会在请求中自动带上
  • 浏览器是否存储 Cookie 还受隐私策略限制,比如 Safari 的 ITP、Chrome 的第三方 Cookie 禁用等

另外很多人不知道的一点:当浏览器收到一个 Set-Cookie,它并不会无脑存下来。它要判断当前 response 是否属于“same-site” or “same-origin”,否则可能直接丢弃(尤其是设置了 SameSite=StrictLax 的时候)。

结尾吐槽一下

改完之后仍有小问题:某些旧安卓机的 WebView 在清除缓存后依然会丢失 Cookie,怀疑是厂商定制系统搞的事。但这已经超出可控范围了,暂时不管。

以上是我踩坑后的总结,希望对你有帮助。如果你有更好的方案欢迎评论区交流。特别是有没有办法不改 Domain 就能让跨域 Cookie 被正确处理?我目前还没找到靠谱做法。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论