如何为在线购物网站安全设置 SameSite cookie?
结论
SameSite=None; Secure 不会让 Cookie 被发送给”所有网站”。浏览器只会将 Cookie 发送到符合其 Domain、Path 等属性的目标地址。
SameSite=None 表示,当其他网站触发对你的网站的请求时,浏览器可以附带该 Cookie。它放宽的是跨站请求场景,并不会扩大 Cookie 的目标域名范围。
假设商城域名是 shop.example.com,支付网关域名是 pay.example-gateway.com:
- 请求
https://shop.example.com/payment/return时,可能携带商城 Cookie。 - 请求
https://pay.example-gateway.com/checkout时,不会携带属于shop.example.com的 Cookie。 - 商城无法设置一个仍归商城所有、却只在访问支付网关域名时发送的 Cookie。
这里真正需要防范的是 CSRF。恶意网站可能诱导用户的浏览器向商城发起跨站请求,而 SameSite=None 允许浏览器在该请求中附带匹配的 Cookie。
是否必须使用 SameSite=None
这取决于支付流程以什么方式返回商城。
如果用户通过顶层页面的普通 GET 跳转回商城,例如:
https://shop.example.com/payment/return
通常可以先尝试:
Set-Cookie: session=...; Path=/; Secure; HttpOnly; SameSite=Lax
SameSite=Lax 通常允许顶层跨站导航使用安全方法发起请求,同时会阻止许多第三方子资源请求和跨站 POST 请求携带 Cookie。
如果支付网关通过浏览器向商城提交跨站 POST 表单,或者商城页面被嵌入第三方站点的 iframe,相关 Cookie 才可能需要设置为:
SameSite=None; Secure
浏览器的隐私策略也会影响具体行为。部分浏览器可能进一步限制第三方 Cookie,因此支付流程不能只依赖第三方 Cookie 是否可用。
推荐设置
1. 不设置 Domain
省略 Domain 会创建 host-only Cookie,它只会发送给设置该 Cookie 的主机:
Set-Cookie: payment_state=...; Path=/payment/return; Max-Age=600; Secure; HttpOnly; SameSite=None
如果响应来自 shop.example.com,该 Cookie 就不会发送给 api.example.com、example.com 或支付网关。
不要仅仅为了方便就扩大范围:
Domain=example.com
否则,Cookie 可能被发送给 example.com 下多个匹配的子域名。
2. 使用尽可能窄的 Path
如果 Cookie 只供支付返回接口使用,可以限制其路径:
Set-Cookie: payment_state=...; Path=/payment/return; Max-Age=600; Secure; HttpOnly; SameSite=None
这样,浏览器访问 /account 或 /products 时通常不会附带它。
不过,Path 主要控制 Cookie 的发送范围,不能作为严格的安全隔离机制。敏感权限仍须由服务端验证。
3. 设置较短的有效期
支付状态 Cookie 通常只需要保留几分钟:
Max-Age=600
支付成功、失败或状态验证完成后,应立即删除:
Set-Cookie: payment_state=; Path=/payment/return; Max-Age=0; Secure; HttpOnly; SameSite=None
删除 Cookie 时,Path 和 Domain 必须与原 Cookie 一致。
4. 同时启用 Secure 和 HttpOnly
Secure; HttpOnly
Secure:Cookie 只通过 HTTPS 发送。现代浏览器通常要求SameSite=None与Secure配合使用。HttpOnly:禁止前端 JavaScript 直接读取 Cookie,降低 Cookie 被注入脚本窃取的风险。
Secure 无法防止 CSRF,也不能弥补 XSS 漏洞或服务端校验缺失。
Express 示例
如果跨站支付返回确实需要携带 Cookie:
app.post('/start-payment', (req, res) => {
const paymentState = crypto.randomUUID();
res.cookie('payment_state', paymentState, {
httpOnly: true,
secure: true,
sameSite: 'none',
path: '/payment/return',
maxAge: 10 * 60 * 1000
});
// paymentState 还应与订单、用户和支付会话在服务端关联
res.redirect(createPaymentGatewayUrl(paymentState));
});
处理返回请求时,不能只确认 Cookie 是否存在:
app.post('/payment/return', async (req, res) => {
const cookieState = req.cookies.payment_state;
const returnedState = req.body.state;
if (!cookieState || cookieState !== returnedState) {
return res.status(400).send('Invalid payment state');
}
const payment = await verifyPaymentWithGateway(req.body);
if (!payment.valid) {
return res.status(400).send('Payment verification failed');
}
res.clearCookie('payment_state', {
httpOnly: true,
secure: true,
sameSite: 'none',
path: '/payment/return'
});
res.redirect('/order/success');
});
如果支付网关采用顶层 GET 跳转,而且测试确认业务流程正常,应优先考虑:
res.cookie('payment_state', paymentState, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/payment/return',
maxAge: 10 * 60 * 1000
});
不要把 Cookie 当作支付结果证明
支付网关返回的”支付成功”参数和浏览器 Cookie 都可能遭到伪造或重放。服务端至少需要验证:
state或一次性随机值是否匹配;- 状态是否绑定当前订单和用户;
- 状态是否已过期或已被使用;
- 支付网关签名是否有效;
- 订单号、金额、币种和商户号是否一致;
- 必要时通过支付网关的服务端 API 查询实际交易状态。
支付网关的 webhook 是服务器到服务器的请求,不会携带用户浏览器中的商城 Cookie。Webhook 应使用签名、密钥或网关规定的认证机制进行验证。
合理的做法不是把商城 Cookie “发送给支付网关”,而是让支付网关将一个短期、不可预测的一次性状态值带回商城,再由商城在服务端完成关联和验证。
备注:内容仅供参考。
