适用场景
Cookie 安全属性配置是会话安全的第一道防线,适用于所有依赖 Cookie 维持登录态的业务系统:后台管理系统、电商购物车与结算流程、SaaS 多租户控制台、以及任何使用 Session ID 或 JWT 承载于 Cookie 的应用。若 Cookie 缺少 HttpOnly,一次 XSS 即可通过 document.cookie 批量窃取会话;缺少 Secure,会话票据会在 HTTP 明文链路中泄露;缺少 SameSite,跨站请求伪造可直接复用受害者的登录态完成敏感操作。
常见的高危信号包括:Set-Cookie: SESSIONID=abc123; Path=/ 这类完全裸奔的下发方式;登录前后 Session ID 不变(会话固定);Cookie 域名设置为 .example.com 导致子域可互相覆盖;以及 SameSite 全部写成 None 却未配套 Secure。
前置条件
- 站点全量 HTTPS(Secure 属性生效的前提),证书链完整且无混合内容;
- Nginx 1.19.3 以上(支持
proxy_cookie_flags),或可直接修改应用代码; - 可访问浏览器开发者工具 → Application → Cookies 面板,或使用
curl -I检查响应头; - 了解当前会话实现:是框架原生 Session、还是自签 JWT 写入 Cookie;
- 具备灰度或本地环境,避免直接在生产调整 SameSite 等级导致登录态批量失效。
原理说明
Cookie 安全属性由浏览器强制实施,共有五个关键控制点:
- HttpOnly:禁止 JS 读取 Cookie,缺失则 XSS 可直接窃取会话;
- Secure:仅通过 HTTPS 发送,缺失则明文链路与会话劫持风险;
- SameSite:限制跨站请求携带,缺失则 CSRF 与跨站信息泄露;
- Domain / Path:限定发送范围,配置过宽导致子域越权覆盖与泄露;
- __Host- 前缀:强约束(必须 Secure、Path=/、无 Domain),可阻止子域写入同名伪造 Cookie。
SameSite 三个取值的差异需准确理解:Strict 在任何跨站请求中都不发送,安全性最高但会打断从外部链接进入的登录态;Lax 仅在顶级导航的 GET 请求中发送,可挡住绝大多数跨站 POST 型 CSRF;None 不做限制,必须同时具备 Secure,否则现代浏览器会直接拒绝写入该 Cookie。
操作步骤
第一步:审计现有 Cookie 的安全属性
逐条拉取站点各入口的响应头,检查会话 Cookie 的属性缺失情况:
# 检查登录接口与首页下发的 Set-Cookie
for u in / /login /api/me /admin; do
echo "=== $u ==="
curl -sI "https://www.example.com$u" | grep -i "^set-cookie"
done
# 批量汇总:列出所有缺少 HttpOnly / Secure / SameSite 的 Cookie
curl -sI https://www.example.com/login | grep -i "^set-cookie" \
| awk -F'; ' '{print $1}' | while read -r c; do
case "$c" in
*HttpOnly*) h="OK";; *) h="MISSING";; esac
case "$c" in
*Secure*) s="OK";; *) s="MISSING";; esac
case "$c" in
*SameSite*) m="OK";; *) m="MISSING";; esac
echo "[$h][$s][$m] $c"
done
同时验证会话固定:先记录登录前 Cookie,再登录后比对是否会话标识发生变化。若 ID 不变,需在认证成功时调用会话重建接口(如 PHP 的 session_regenerate_id(true)、Java 的 request.changeSessionId())。
第二步:在 Nginx 层统一补齐安全属性
对于自建后端或不便立即改代码的老系统,可在反向代理层用 proxy_cookie_flags 统一注入,避免逐模块修改:
server {
listen 443 ssl;
server_name www.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
# 对名为 session 的 Cookie 追加属性(正则匹配 Cookie 名)
proxy_cookie_flags ~^(session|SESSIONID|JSESSIONID|PHPSESSID)$ secure httponly samesite=lax;
# 第三方嵌入场景需 None,则必须带 Secure
proxy_cookie_flags ~^csrf_token$ secure samesite=lax;
# 统一去除冗余 Domain,避免子域共享会话
proxy_cookie_domain ~^(?<sub>.*)\.example\.com www.example.com;
}
}
要点:proxy_cookie_flags 只能追加属性,不能删除,若源站下发了 SameSite=None 而需要改成 Lax,必须回到应用层修改;samesite 关键字必须小写且不带空格,否则 Nginx 会报配置错误。修改后务必先执行语法检查:
nginx -t && nginx -s reload
第三步:应用层设置正确的属性组合
以常见技术栈为例,给出可直接落地的配置片段:
# --- PHP(php.ini 或运行时设置)---
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = "Lax"
session.use_strict_mode = 1
session.cookie_lifetime = 0
# 认证成功后必须重建会话
session_regenerate_id(true);
# --- Python / Flask ---
app.config.update(
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_SAMESITE="Lax",
PERMANENT_SESSION_LIFETIME=timedelta(minutes=30),
)
# --- Node.js / Express ---
app.use(session({
name: "__Host-session", // 使用 __Host- 前缀强约束
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true, sameSite: "lax", maxAge: 1800000 }
}));
# --- Spring Boot (application.yml) ---
# server.servlet.session.cookie:
# http-only: true
# secure: true
# same-site: lax
# name: __Host-SESSION
对最敏感的会话 Cookie,建议使用 __Host- 前缀:该前缀由浏览器强制校验,要求必须设置 Secure、必须 Path=/ 且不得设置 Domain,从而杜绝任意子域写入同名 Cookie 的伪造攻击。
第四步:区分 Cookie 用途,避免一刀切
- 会话票据:HttpOnly + Secure + SameSite=Lax,不含 Domain,Max-Age 与会话超时一致;
- CSRF Token:Secure + SameSite=Lax,是否需要 HttpOnly 取决于前端是否读取,采用「双提交 Cookie」模式时需允许 JS 读取;
- 第三方嵌入 / 跨站支付回调:SameSite=None 且必须 Secure,同时启用 Partitioned(CHIPS)属性以避免被第三方 Cookie 策略拦截;
- 个性化偏好:非敏感数据可长有效期,但仍应 Secure,并避免与登录态同域同级。
需要特别强调:SameSite=Lax 不能替代 CSRF Token。Lax 允许顶级 GET 导航携带 Cookie,因此任何「通过 GET 实现状态变更」的接口仍然可被利用;正确做法是敏感操作统一使用 POST/PUT/DELETE,并校验服务端生成的 CSRF Token。
第五步:会话生命周期配套加固
Nginx 侧对未携带会话的请求限速,防止会话探测:
map $http_cookie $has_session {
default 0;
~*SESSIONID 1;
}
limit_req_zone $binary_remote_addr zone=sess:10m rate=10r/s;
server {
location /api/ {
limit_req zone=sess burst=20 nodelay;
if ($has_session = 0) {
add_header X-No-Session 1 always;
}
proxy_pass http://127.0.0.1:8080;
}
}
配置验证
# 1. 检查响应头属性是否齐全
curl -sI https://www.example.com/login | grep -i "^set-cookie"
# 期望包含:HttpOnly、Secure、SameSite=Lax(或 Strict);不应出现 Domain=.example.com
# 2. 用 __Host- 前缀时,浏览器行为验证(Chrome DevTools 控制台)
# document.cookie 应读取不到会话 Cookie,返回其他非 HttpOnly 项
# 3. 验证跨站是否携带(本地构造跨站页面访问,观察 Network 请求头)
# SameSite=Lax 下,跨站 POST 请求的 Cookie 头应为空
# 4. 验证 HTTPS 强制:通过 http 端口访问应 301 到 https,且 Cookie 不下发
curl -sI http://www.example.com/login | grep -Ei "location|set-cookie"
# 5. 会话固定检查:登录前后对比
before=$(curl -sI https://www.example.com/login | grep -i "^set-cookie" | md5sum)
curl -s -c jar.txt -d "user=tester&pass=REPLACE_ME" https://www.example.com/login
after=$(grep -i session jar.txt | md5sum)
[ "$before" != "$after" ] && echo "会话已重建 OK" || echo "!!! 存在会话固定风险"
若使用 __Host- 前缀后登录态失效,绝大多数情况是三个条件之一未满足:未启用 Secure、Path 不是 /、或仍设置了 Domain。逐项核对即可定位。
常见问题(FAQ)
Q1:SameSite=None 时,只设置 Secure 就够了吗?
在现代浏览器中不足够。Chrome、Edge、Safari 均要求 SameSite=None 必须搭配 Secure,否则整条 Cookie 会被拒绝写入;此外在第三方上下文(iframe、跨站请求)中,还需评估第三方 Cookie 限制策略,必要时启用 Partitioned 属性,否则即使属性正确,Cookie 也可能因分区策略而无法在嵌入场景生效。
Q2:加了 HttpOnly 之后,前端读不到 Cookie,登录态怎么传给前端?
不要把会话票据交给 JS。正确做法是提供同源的会话信息接口(如 /api/me),由浏览器自动携带 HttpOnly Cookie,服务端据此返回用户信息与权限列表;前端只在内存中保存非敏感的展示态。若确实需要前端读取(如双提交 CSRF Token),应单独下发一个 非 HttpOnly 的独立 Cookie 或通过响应体返回 Token,不与会话票据混用。
Q3:为什么改完 SameSite=Lax 后,部分用户从外链进入时登录态「掉」了?
这是 SameSite 语义的正常表现。Lax 允许顶级 GET 导航携带 Cookie,因此通过外链(搜索引擎、邮件、第三方站点)进入站点首页时登录态应正常保留;出现掉登录通常有两个原因:一是站点对跨站 POST 表单回跳(如第三方单点登录回调)依赖 SameSite=None,此时应对该回调路径单独使用 None + Secure;二是回调域与前端域不一致,需检查 Domain 与重定向链路是否跨域,而不是回退到 None 全站放开。
总结
Cookie 安全属性的加固并不复杂,关键是把「会话票据」与「普通 Cookie」分开对待:会话票据必须同时满足 HttpOnly、Secure、SameSite=Lax 以上、无冗余 Domain,敏感场景优先使用 __Host- 前缀;跨站必需场景再单独放开为 None 并强制 Secure。配置完成后务必做三项回归:属性完整性检查、会话固定检查、跨站请求携带检查,三者全部通过才算落地。