返回博客

登录返回 200 但下一个请求立刻 401:Cookie Secure 标志在 HTTP 环境下的静默丢弃

给某个后台系统接入 Tauri 桌面壳,本地走 http://localhost:5891 调试。生产环境(HTTPS)登录一切正常,但本地启动后,登录接口返回 200,紧接着请求 /api/auth/me 就直接 401 了。重试、换账号、重启服务——全没用。

现象

Network 面板里,这两条请求背靠背出现:

POST /api/auth/login   → 200 OK  (服务端日志:session 已创建,Set-Cookie 已发送)
GET  /api/auth/me      → 401     (请求头里没有 Cookie 字段)

打开 DevTools 仔细看:

  • Network → /login 响应头Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/
  • Application → Cookies → localhost:5891:空的 ❌

服务端完全正常,Cookie 也确实被发出来了——但浏览器就是没存。

根因

Set-Cookie 里带了 Secure 标志。根据 RFC 6265 §4.1.2.5,这个标志要求用户代理只能在加密连接(HTTPS)下存储和发送该 Cookie。在 HTTP 连接下收到带 Secure 的 Cookie,浏览器会静默丢弃——没有任何 console 警告,没有网络错误,Set-Cookie 响应头照样可见,只是永远不会被写入本地存储。

Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/
                                  ^^^^^^
                                  ↑ HTTP 环境下,这一个词让整个 Cookie 被丢弃

Tauri 的 WebView(macOS 上是 WKWebKit,Windows/Linux 上是 WebKit2GTK 或 Chromium)同样严格遵守此规范——"我是桌面应用"不能绕过浏览器 Cookie 规则。

陷阱在于,很多框架/项目为了生产安全会默认开启 secure: true,但这个默认值在本地 HTTP 开发时同样生效:

// 有问题的写法:默认值是 'true',不传环境变量时也会启用 Secure
const COOKIE_OPTS = {
  httpOnly: true,
  secure: (process.env.COOKIE_SECURE || 'true') === 'true',
  sameSite: 'lax' as const,
  path: '/',
}

开发者不设置 COOKIE_SECURE,值就是 'true',登录在本地永远失败——但错误信息完全指向 /me 接口鉴权,而不是 Cookie 本身。

坑:Secure Cookie 在 HTTP 下被静默丢弃,浏览器不报任何错

Secure 标志导致的 Cookie 丢失没有任何 console 警告(与 SameSite 不同,SameSite 会有提示)。表面症状是"登录后立刻 401",引导开发者去查鉴权中间件、Session Store、JWT 校验——几乎不会往 Cookie 本身查。正确的第一步是:打开 DevTools → Application → Cookies,看 Cookie 是否真的被存下来了。

正解

secure 标志跟着环境走,本地 HTTP 开发时强制关闭:

// 正确写法:默认 false,生产由环境变量显式开启
const COOKIE_OPTS = {
  httpOnly: true,
  secure: process.env.COOKIE_SECURE === 'true',  // 不传 = false
  sameSite: 'lax' as const,
  path: '/',
}

在开发启动脚本里不设置(或显式传 false),生产部署时才传 true

// package.json
{
  "scripts": {
    "dev": "tsx watch src/server/index.ts",
    "app:dev:local": "COOKIE_SECURE=false npm --prefix desktop run tauri -- dev --config ..."
  }
}
# 生产部署
COOKIE_SECURE=true node dist/server.js

如果是通过 concurrently / tauri devbeforeDevCommand 启动子进程,环境变量会沿进程树向下传播,不需要每个子命令单独设置。

不要这样做:

  • 全局关掉 HTTPS 重定向来"让本地过"——这破坏生产安全
  • 用 Chrome 的 --unsafely-treat-insecure-origin-as-secure 标志——不可移植,WebView 里也不起效
  • 无条件改成 secure: false——生产必须保持 true
正解:secure 标志必须由环境变量控制,默认值为 false

secure: process.env.COOKIE_SECURE === 'true' 是最简单的修法。不传环境变量 = false,生产显式传 true。修完后验证:DevTools → Application → Cookies 里应能看到 session cookie,下一个请求的 Cookie 请求头里也应包含它。

60 秒快速定位

怀疑这个问题时,不需要看任何业务代码,先做三步:

  1. DevTools → Network → 点 /login 响应 → 确认 Set-Cookie 响应头存在
  2. DevTools → Application (Chrome) / Storage (Firefox) → Cookies → 看 Cookie 是否被写入
  3. Set-Cookie 值里有没有 Secure 关键字

三步全中(Set-Cookie 有、Cookies 空、有 Secure)= 确认是这个坑,根因文件就是 Cookie 配置里的 secure 选项。

一句话外卖

secure: true 的 Cookie 在 HTTP 下被浏览器静默丢弃——没有任何报错;正确姿势是用环境变量控制,本地 HTTP 默认 false,生产 HTTPS 显式 true