付款回来购物车就空了,而浏览器那两分钟的宽限只发给从没配过SameSite的站
本文目录
- 付款成功跳回来,购物车为什么反而空了?
- 服务器看到的是一个陌生人
- Lax到底禁掉了什么
- 为什么测试环境永远是好的
- 同样显示Lax,浏览器为什么区别对待?
- 先给自己搭一个能判决的场子
- 浏览器给出的判决表
- 那两分钟的宽限期,2026年还在不在?
- 它是什么,给谁的
- 窗口到底几秒关上
- 两分钟意味着什么样的用户
- 挑战窗口开在iframe里,为什么连宽限都够不着?
- 验证页大多不是整页跳转
- 把这一格也测出来
- 顺手撞见的一个附带伤害
- 114个电商站的结账页,会话Cookie都写了什么?
- 怎么取的样
- 越靠近结账,Lax越多
- 把会话类单独拎出来
- 可以点名的部分
- 支付网关自己是怎么写的?
- 结账这一步真的跨站吗?
- 先量一量再说
- 那5个跨了域的更有意思
- Partitioned能把这个问题修好吗?
- 先看有多少人在用
- 实测:它救不了回跳
- 为什么这类故障只打击一部分用户,报表上却看不见?
- 三个条件同时成立才出事
- 为什么报表帮不上忙
- 该怎么把它量出来
- 该怎么排查,又该怎么改?
- 十分钟先做完的自查
- 四种改法,按代价从低到高
- 三条别踩的
- 常见问题解答
- 我的站从来没写过SameSite,是不是反而安全?
- 直接把会话Cookie改成SameSite=None,安全吗?
- 为什么开发者工具里看不出“没写”和“写了Lax”的区别?
- 用了Shopify、Magento这类现成系统,还需要管这件事吗?
- 把Cookie的有效期设长一点,能绕过两分钟的限制吗?
- 这个问题会影响SEO吗?
- 权威参考资料
摘要:支付网关把用户送回你的站,用的往往是一次跨站POST。而SameSite=Lax的会话Cookie在这条路上不会被带上,浏览器不报错、日志不飘红,用户看到的只是购物车空了。这次搭了两个真实注册域做回跳实验,又扫了114个电商站的1086条Set-Cookie。最扎心的一组数字是:结账路径上的会话类Cookie,能挺过跨站POST的只有3.6%。更反直觉的是,浏览器确实留了一个两分钟的宽限窗口,但它只发给那些从来没写过SameSite的站——认真写了Lax的,一秒都没有。
先说这事儿是怎么找上门的。
一个做家居的独立站,客服那边攒了十几条投诉,说法出奇一致:钱扣了,银行短信也来了,可跳回网站一看,购物车是空的,还让重新登录。技术那边查了三天,服务器日志干干净净,支付网关后台显示交易成功,订单表里也确实有记录。两边都没错,就是用户体验上凭空少了一截。
更麻烦的是复现不了。开发在测试环境点了几十遍,每一遍都顺顺当当。
后来定位到的原因,说出来只有一行字:那条会话Cookie上写着SameSite=Lax。它没写错,语法完全合法,安全扫描工具还会因为它给你加分。问题在于Lax这一档明确规定,跨站的POST请求不带Cookie,而支付验证完把人送回来的那一下,恰好就是一次跨站POST。
这篇文章想把这条链子从头到尾拆开。不光讲机制,还带着实测——两个真实域名搭的回跳实验,加上114个电商站的Cookie属性普查。有几个结果和保哥事先的预期是反的,其中一个反到不得不推翻自己写好的判定逻辑重来。
先把话说在前面:这件事归根到底不是安全问题,是服务器配置那类会顺手把业务撞出坑的细节里的一员。一个属性写没写,决定了一笔订单还认不认得出人。
付款成功跳回来,购物车为什么反而空了?
要理解这件事,得先接受一个不太符合直觉的前提:你的会话没有丢,它一直好好躺在浏览器里;只是在回跳那一次请求上,浏览器决定不把它交出来。
服务器看到的是一个陌生人
会话这套东西的运作方式很朴素。用户第一次来,服务器发一条Cookie给他,里面是个随机串;此后每次请求,浏览器把这个串带回来,服务器凭它到自己的存储里翻出这个人是谁、购物车里有什么。
整个机制的命门在于那句“浏览器把这个串带回来”。一旦某次请求没带,服务器这边的反应不是报错,而是完全合理地认为:来了个新访客。于是它做了新访客该有的一切——建一个新会话、发一条新Cookie、给你一个空购物车。
这就是为什么日志里什么都查不到。从服务器的角度看,这次请求没有任何异常,它甚至不知道刚才那个带着满购物车的人和现在这个空手的人是同一个。
Lax到底禁掉了什么
SameSite这个属性一共三档,管的是“这条Cookie在跨站请求里带不带”。规范里的定义相当克制,Lax这一档要同时满足两个条件才放行:这次请求得是顶层导航,也就是地址栏会变的那种跳转;同时得用安全方法,只有GET、HEAD、OPTIONS算。
POST不在安全方法里面。所以一次跨站的POST,无论它多正当、多重要,Lax都不放行。
| SameSite取值 | 同站请求 | 跨站GET顶层导航 | 跨站POST | iframe内的跨站请求 |
|---|---|---|---|---|
| Strict | 带 | 不带 | 不带 | 不带 |
| Lax | 带 | 带 | 不带 | 不带 |
| None(须配Secure) | 带 | 带 | 带 | 带 |
| 不写 | 带 | 带 | 见下文,有个窗口 | 不带 |
注意最后一行留了个尾巴。那一格是这篇文章里最有意思的地方,稍后专门拆。
为什么测试环境永远是好的
开发点几十遍都没事,不是运气好,是测试卡本来就不触发额外验证。沙箱环境的测试卡刷过去就是成功,中间不跳银行、不弹验证页,压根没有那次跨站POST。能复现问题的,恰恰是真实世界里那些被要求额外验证的订单。这批订单在3DS 2新规之后的拒付率账本里是重点保护对象,在这里却成了唯一的受害者。
这一点和结账页放弃率超70%的那些真实成因是同一个毛病的两面:真正吃掉转化的环节,往往正好是内部测试覆盖不到的那一段。
同样显示Lax,浏览器为什么区别对待?
这是保哥这次动手做实验的起因,也是整轮里最出乎意料的一个结果。
先给自己搭一个能判决的场子
光看文档推不出结论,得让浏览器自己说话。手上正好有几个不同的注册域名,于是搭了这么一套:
- A站扮演商户,有个页面一次性下发11条Cookie,每条的属性组合都不一样,答案事先都是已知的;
- B站是另一个注册域,扮演支付网关,负责把访客送回A站,可以选择用POST送,也可以选择用GET送;
- A站还有个回显页,专门打印这一次请求到底带来了哪几条Cookie。
这套东西的好处是,它不依赖任何一方的自我描述。不管文档怎么写、工具怎么显示,最终判决书由浏览器真实发出的那个请求签发。
动手测别人之前先做了一件事:拿这11条已知答案的Cookie去校验自己写的解析器。11条全部判对,才敢拿它去跑真实站点。这个习惯是上次排查PHP会话缓存头时留下的——工具没校准就出门,量回来的数字看着挺像样,其实全是自己的偏差。
浏览器给出的判决表
用无头Chromium 148和本机真实的Chrome 150各跑了一遍,两者结论完全一致:
| Cookie写法 | 浏览器存下了吗 | 跨站POST | 跨站GET导航 | 同站GET | 同站POST |
|---|---|---|---|---|---|
| SameSite=Strict | 存了 | 没到 | 没到 | 到达 | 到达 |
| SameSite=Lax | 存了 | 没到 | 到达 | 到达 | 到达 |
| 不写SameSite | 存了 | 到达 | 到达 | 到达 | 到达 |
| SameSite=None; Secure | 存了 | 到达 | 到达 | 到达 | 到达 |
| SameSite=None,漏了Secure | 压根没存 | 没到 | 没到 | 没到 | 没到 |
| __Host-前缀,写法合规 | 存了 | 没到 | 到达 | 到达 | 到达 |
| __Host-前缀,带了Domain | 压根没存 | 没到 | 没到 | 没到 | 没到 |
| __Secure-前缀,漏了Secure | 压根没存 | 没到 | 没到 | 没到 | 没到 |
| SameSite=None; Secure; Partitioned | 存了 | 到达 | 到达 | 到达 | 到达 |
第三行让保哥盯了很久。不写SameSite的那条,居然挺过了跨站POST;而老老实实写了SameSite=Lax的那条,没挺过去。
这跟所有资料里那句“不写就按Lax处理”是矛盾的。于是回头去翻浏览器实际存下的记录,结果更奇怪:
| Cookie | 响应头里写的 | 浏览器记录的sameSite值 | 跨站POST的实际结果 |
|---|---|---|---|
| d74_lax | SameSite=Lax | Lax | 不带 |
| d74_missing | (什么都没写) | Lax | 带 |
两条Cookie在浏览器的记录里是同一个值,行为却相反。这意味着开发者工具、任何读Cookie的接口、任何自动化检查脚本,看到的都是同一个Lax,而它们看不到的那个差别,恰好就是决定订单成不成的那个差别。
说得刻薄一点:这是一个只有浏览器自己知道、并且刻意不告诉你的状态。你手里所有的检查工具,在这件事上是集体色盲。
那两分钟的宽限期,2026年还在不在?
差别的来源找到了,是一条叫Lax+POST的临时兼容措施。
它是什么,给谁的
2020年前后,各家浏览器把SameSite的默认值从“不限制”改成了Lax。这个改动会一次性打断大量线上流程,于是留了个缓冲:对那些没有显式写SameSite的Cookie,如果它的年龄不超过两分钟,跨站POST照样放行。
关键在“没有显式写”这五个字。这个兼容措施的对象,从设计上就只包括那些还没来得及配置的站。你一旦写下SameSite=Lax,就等于主动声明我已经知道自己在干什么,浏览器于是把你从名单里摘了出去。
这是整轮实验里最想记下来的一条:合规动作本身,把你从兼容名单里剔除了。守规矩的人先撞墙,因为兼容层是为不守规矩的人准备的。
窗口到底几秒关上
官方说法是两分钟。但这类“临时措施”往往悄悄改过或者早就下线了,光看文档不算数,于是拿真实Chrome实测了一轮。每个检查点都开一套全新的浏览器上下文,重新下发Cookie,等到指定年龄再发跨站POST:
| Cookie年龄 | 不写SameSite的那条 | 显式写Lax的那条 | SameSite=None的那条 |
|---|---|---|---|
| 2.5秒 | 到达 | 没到 | 到达 |
| 70.6秒 | 到达 | 没到 | 到达 |
| 100.6秒 | 到达 | 没到 | 到达 |
| 115.6秒 | 到达 | 没到 | 到达 |
| 118.6秒 | 到达 | 没到 | 到达 |
| 122.0秒 | 没到 | 没到 | 到达 |
| 135.9秒 | 没到 | 没到 | 到达 |
| 200.6秒 | 没到 | 没到 | 到达 |
放行的最大实测年龄是118.6秒,拦截的最小实测年龄是122.0秒。门就卡在120秒上,和文档写的分毫不差。
顺带确认了一件事:这条被官方称为临时方案、写明将来会移除的措施,2026年7月在Chrome 150上仍然活着。六年过去了,它还在替一大批从没配置过SameSite的站兜底。
两分钟意味着什么样的用户
把这个数字放回结账场景,画像立刻就清晰了。两分钟够干什么?刷个测试卡绰绰有余。不够干什么呢:
- 银行判定这笔交易需要额外验证,弹出验证页;
- 用户切到手机银行App找动态口令,回来发现验证码过期,再要一次;
- 短信通道慢了半分钟;
- 年纪大一点的用户,对着输入框研究了一会儿;
- 网络本身就慢,几个跳转各花几秒。
换句话说,这个两分钟的闸门,精确地筛掉了那批最需要额外验证、也最谨慎的用户。他们花的时间越长,就越是在证明这笔交易值得被保护,而代价是回来时会话没了。
这类“只打击慢的那一批”的故障有个共性:它在总量里占比不高,但在特定切片里能占大头,而报表默认不按这个维度切。这和GA4直接流量突然暴增背后的六类成因是同一类症状——会话断裂之后,回来的那一跳会被记成一次全新的直接访问,转化归因也就跟着断了。
挑战窗口开在iframe里,为什么连宽限都够不着?
做到这里本以为可以收工,直到去核对支付验证的技术细节,发现下面还有一层。
验证页大多不是整页跳转
现代的支付验证流程里,银行那个验证页通常不是把整个页面跳走,而是嵌在一个iframe里显示,用户在这个小窗口里完成验证。验证结束后,银行侧的系统从这个iframe内部发一次POST,把结果送回商户指定的通知地址。
这个细节要命的地方在于:宽限期那条规则的原文是“顶层的跨站POST请求”。而iframe内部的导航,按定义不是顶层导航。
把这一格也测出来
光推理不够,加了一组实验:同一批Cookie,年龄都控制在5秒以内(宽限窗口正中间),一次走顶层导航POST,一次走iframe内的POST。
| 回跳形态 | SameSite=None | 不写SameSite(5秒内) | 显式Lax | Partitioned | 合计到达 |
|---|---|---|---|---|---|
| 顶层导航POST | 到达 | 到达 | 没到 | 到达 | 4条 |
| iframe内POST | 到达 | 没到 | 没到 | 没到 | 2条 |
服务端记下的请求特征里,两次的跨站标记完全一样,都标着这是一次跨站的导航请求。浏览器区分的不是请求类型,而是这次导航发生在顶层还是发生在框架里。
结论因此收得很紧:如果你的验证流程走的是iframe,那两分钟宽限期对你完全不存在,唯一能活下来的写法只有SameSite=None加Secure。不写、写Lax、写Strict,在这条路上是同一个下场。
顺手撞见的一个附带伤害
测iframe那一轮还带出个意外。实验用的那个站上配了内容安全策略,限制只有本站才能把页面嵌进框架。结果是:那次回跳的POST请求照样发出去了,服务器照样收到、照样处理、照样返回200,只有浏览器最后拒绝把结果画到屏幕上。
翻译成生意上的话:订单在后台成了,用户眼前是一片空白。这种故障比丢会话还难查,因为后台数据完全正常,而用户会坚持说自己什么都没看到。要是你的站也配了这条策略,又打算把验证页嵌进iframe,这一格得单独验一遍。
114个电商站的结账页,会话Cookie都写了什么?
机制清楚了,接下来的问题是现实里有多少人踩在坑上。于是做了一轮普查。
怎么取的样
域名池按类别拼了164个,覆盖海外DTC品牌、几个主流电商系统的商户站、跨境平台,另外把支付服务商自己和一批非电商站放进去当对照。可达的149个,逐个按五种路径去请求:首页、购物车、结账页、登录页、搜索页,只发匿名GET,只读响应头,不做任何写操作。
最后拿到114个站、379个页面组合、1086条Set-Cookie。
分层这么切是有意的:首页和搜索页本来就不该有个性化,购物车和结账页则必须认得出你是谁。把两类页面分开当对照,才看得出这个属性的写法到底跟不跟业务语义走。
越靠近结账,Lax越多
| 页面层 | Cookie条数 | 没写 | Lax | Strict | None |
|---|---|---|---|---|---|
| 首页 | 186 | 26.3% | 26.9% | 2.2% | 44.6% |
| 购物车 | 338 | 14.5% | 59.8% | 0.6% | 25.1% |
| 结账页 | 210 | 13.8% | 65.7% | 0.0% | 20.5% |
| 登录/账户 | 157 | 15.3% | 44.6% | 0.0% | 40.1% |
| 搜索页 | 195 | 30.3% | 27.2% | 2.1% | 40.5% |
| 合计 | 1086 | 19.3% | 47.2% | 0.9% | 32.5% |
结账页的Lax占比是五层里最高的,65.7%。这不难理解——越是敏感的页面,安全建议就越倾向于收紧,而Lax正是各类加固清单里最常被推荐的那一档。问题在于这些清单默认的场景是防跨站请求伪造,没有人在写清单的时候想着支付回跳。
把会话类单独拎出来
更直接的问法是:那些真正承载着“你是谁”的Cookie,有多少能挺过回跳?把会话和购物车类的Cookie单独拎出来算:
| Cookie类型 | 条数 | 跨站POST带得过去 | 带不过去 |
|---|---|---|---|
| 会话/购物车类 | 165 | 6条(3.6%) | 159条(96.4%) |
| 第三方追踪类 | 12 | 1条(8.3%) | 11条 |
| 其他 | 909 | 346条(38.1%) | 563条 |
那159条带不过去的里面,构成是这样:显式写了Lax的139条,占84.2%,这批连两分钟宽限都拿不到;没写SameSite的20条,占12.1%,这批还有两分钟;剩下6条是Strict之类。
写Lax的比不写的多出六倍,而前者的处境更糟。如果把“有没有配置过”当成认真程度的代理指标,那么这个数据说的是:在这件事上,越认真的站死得越干脆。
可以点名的部分
结账和购物车路径上,显式写了Lax的会话与购物车类Cookie一共145条,分布在41个站上。里面能看出明显的平台特征:
- 用某主流托管电商系统的站,那两条以下划线开头的会话Cookie和货币偏好Cookie,一律是显式Lax,二十多个站的写法完全一致——说明这不是各站自己的选择,是平台统一发的;
- 用某开源PHP电商系统的9个站,会话Cookie全都是显式Lax,没写的比例是0.0%,用None的比例也是0.0%;
- 用某开源博客系统改的商城,情况相反,没写的比例高达86.7%。
第二条那个“两个0.0%”很值得停一下。这批站是这次样本里配置最规整的一档,每一条Cookie都显式声明了SameSite,从加固的角度挑不出毛病。而恰恰因为条条都写了,它们在跨站POST这一格上,一条兜底都没有。
这跟上次查PHP缓存头时看到的分布正好是镜像。上次是成熟平台全部显式关掉了危险默认值,所以全都干净;这次是成熟平台全部显式写死了保守值,所以全都没有豁免。同一个动作,在两件事上一个救命一个要命。
支付网关自己是怎么写的?
对照组里放了15家支付和结账基础设施的域名,一共150条Cookie。数字很干脆:SameSite=None占74.0%。
| 类型 | 站数 | Cookie数 | 没写 | 用None | 带Secure | 带HttpOnly |
|---|---|---|---|---|---|---|
| 支付/结账基础设施 | 15 | 150 | 14.0% | 74.0% | 86.0% | 56.0% |
| DTC品牌独立站 | 41 | 630 | 15.7% | 32.7% | 76.2% | 30.6% |
| 开源PHP电商系统站 | 9 | 63 | 0.0% | 0.0% | 82.5% | 65.1% |
| 跨境平台站 | 6 | 83 | 66.3% | 33.7% | 61.4% | 45.8% |
| 非电商对照组 | 5 | 56 | 16.1% | 10.7% | 83.9% | 42.9% |
具体到单家更明显:有一家支付服务商的46条Cookie,全部是None;另一家的32条,也基本全是None。他们的业务本质就是被嵌在别人页面里、被别人的域名跳来跳去,所以跨站是常态而不是例外,写法上没有含糊的余地。
而商户站这一侧,会话Cookie用None的只有3.6%。这个落差在配置WooCommerce支付网关时最容易被忽略:插件把网关接通了,Cookie策略却还是主题和商城核心各写各的。同一条支付链路的两端,一端把跨站当成默认工作方式,另一端把跨站当成需要防范的攻击。两边都没写错,但对接的时候会话就掉在中间那道缝里。
顺便说一个写法上的小乐子。有一家的Cookie是这么写的:Secure;SameSite=None;Secure=true。Secure本来是个开关,写上就生效,不需要赋值;这里既写了开关又给它赋了一遍true,中间还漏了个空格。功能上不影响,但它相当清楚地说明这串东西是手工拼字符串拼出来的,不是哪个库统一生成的。
结账这一步真的跨站吗?
数据看到这儿得停下来问一句:会不会整个担心都是多余的?如果结账全程都在自己的域名底下,那压根不存在跨站问题。
先量一量再说
把每个站的结账页请求,比对它最终落到哪个注册域上。147个结账页可达的站里:
- 留在自己注册域的142个,占96.6%;
- 跳到了另一个注册域的5个,占3.4%。
所以结论要说清楚:风险不在打开结账页那一跳,而在从支付验证回来那一跳。结账页本身在自己家,用户在这一页上填完卡号点提交,才被送出去验证,验证完再送回来——跨站发生在回程,不在去程。这也是为什么用普通的页面测试根本发现不了。想把这一跳看清楚,得像排查那些每次reload都通过、却在真实抓取里翻车的Nginx配置一样,从站外发起真实请求去验。
那5个跨了域的更有意思
那5个例外里,有4个跟支付网关一点关系都没有,是品牌自己把结账放到了另一个注册域上:
| 访问的域名 | 结账最终落在 | 中间跳了几次 |
|---|---|---|
| athleticgreens.com | drinkag1.com | 6次 |
| beautycounter.com | counter.com | 4次 |
| hydrojug.com | thehydrojug.com | 1次 |
| luxuryflooringandfurnishings.co.uk | luxuryflooring.co.uk | 2次 |
这几家共同的故事是品牌改名或者启用新域名,老域名留着做跳转。但在浏览器眼里,这不是同一家公司换了个招牌,而是两个毫不相干的站点。老域名上攒的会话,跨过去的时候要按跨站规则走一遍——而这跟支付一点关系都没有,是自己站内的两个域打架。
顺带提醒一句:判断两个域名算不算同一个站,用的不是“看起来像不像一家”,而是一份叫公共后缀列表的清单。品牌名一样、只差一个词的两个域名,是彻头彻尾的两个站;而不同二级域名之间反倒算同一个站。做多域名布局之前,这条得先确认,别等到跨境用户跳不过去才发现——这类问题的排查手感,和海外用户说独立站打不开时的网络层排障很像,都是本地一切正常、异地才现原形。
Partitioned能把这个问题修好吗?
最近谈第三方Cookie的场合,Partitioned这个属性出现得很频繁。既然它是给跨站场景准备的新机制,那能不能拿来救回跳?
先看有多少人在用
1086条Cookie里的采用情况相当冷清:
| 机制 | 条数 | 涉及站数 | 备注 |
|---|---|---|---|
| __Host-前缀 | 2 | 2 | 其中1条还是删除操作,真正在用的只有1个站 |
| __Secure-前缀 | 0 | 0 | 整个样本里一条都没有 |
| Partitioned | 18 | 3 | 多数是删除操作,真正在用的是1个站 |
| 带HttpOnly | 420 | — | 占38.7% |
| 带Secure | 836 | — | 占77.0% |
顺带一提,删除一条Cookie的做法是重发一条同名的、寿命为0的Cookie,属性得写得跟当初一模一样才删得掉。所以这些“删除操作”其实也是有效证据——它证明这些站当初确实设过带这些属性的Cookie。
实测:它救不了回跳
更关键的是效果。在前面那组iframe实验里,Partitioned那条Cookie的表现是:顶层导航POST能到达,iframe内的POST到不了。
原因在于分区Cookie的存放方式。它的分区键取的是用户当时所在的那个顶层站点,也就是地址栏里的域名。用户在你的站上时,这条Cookie存在“你的站”这个分区下;等验证页在网关域名的顶层环境里从iframe发请求回来,此刻的顶层站点已经变成了网关,分区键对不上,浏览器自然取不出来。
所以结论很直接:Partitioned解决的是第三方内容在不同宿主页面之间互相串数据的问题,它不是用来让你自己的会话穿过跨站回跳的。拿它当替代方案,属于把螺丝刀当撬棍使——能拿在手上,但方向就不对。
为什么这类故障只打击一部分用户,报表上却看不见?
这一节其实是整篇里最该被业务方看到的部分。
三个条件同时成立才出事
把前面所有实测串起来,会话真正丢掉需要三件事同时发生:会话Cookie是Lax或者没写且超过两分钟;回跳用的是POST而不是GET;如果是iframe形态,那前一条的两分钟豁免也没有。
三个条件全中的订单,比例不会高。但这三个条件不是独立事件,它们高度相关——需要额外验证的交易,往往同时意味着耗时更长、用iframe弹窗、走POST回调。所以在“被要求额外验证”这个切片里,命中率可以高得吓人;而在总订单里一摊平,可能就剩百分之几。
为什么报表帮不上忙
几乎所有的转化报表都按全站或者渠道来切,没有一个默认维度叫“这笔交易有没有被要求额外验证”。而这恰恰是唯一能把问题照出来的切法。
更麻烦的是,会话断掉之后,用户回来的那一跳会被当成一次全新访问,来源信息也跟着丢了。于是这批订单要么统计不到,要么被算到直接流量头上,看起来还像是自然量涨了。问题信号被伪装成了好消息。
这类故障最难的地方不是修,是承认它存在。因为所有的仪表盘都在说一切正常,唯一喊疼的是客服工单,而工单在多数公司里不算数据。
该怎么把它量出来
不用等着重构,几件小事就能把这个盲区照亮:
- 在支付回跳的落地处理里加一行日志,记下这次请求有没有带到会话标识,以及从发起支付到回跳一共过了多少秒;
- 把这两个字段交叉一下,看看丢会话的那批订单,耗时分布是不是明显偏右;
- 把客服工单里“付了钱购物车空了”“付完要重新登录”这类描述单独打个标签,跟上面的日志对时间。这类工单在退款与拒付争议的处理流程里往往被归成支付失败,其实根本没到支付那一层。
这套做法和从慢查询日志里区分信号和噪声是一个路子:先让现象可计数,再谈优化,否则讨论只能停留在互相说服。
该怎么排查,又该怎么改?
最后落到动作上。
十分钟先做完的自查
- 把会话Cookie的原文抓出来。用不带Cookie的方式请求一次结账页,直接看Set-Cookie这一行的完整属性串。不要用开发者工具的Cookie面板,前面已经证明它分不出“没写”和“写了Lax”。用能看清完整响应头的工具看原始字符串。
- 确认回跳到底用什么方法。翻网关文档里关于回调地址那一节,看它写的是重定向还是表单提交。写着表单提交、带着参数名的,就是POST。同时接了多个本地支付方式的站要逐个看,同一个站里不同网关的回跳方式经常不一样。
- 确认验证页是不是嵌在iframe里。是的话,两分钟豁免直接划掉。
- 确认结账域名和主站是不是同一个注册域。不同的话,问题在回跳之前就已经开始了。做多店架构、给不同市场配独立结账的时候,这一条尤其容易踩。
四种改法,按代价从低到高
| 做法 | 适用场景 | 代价与风险 |
|---|---|---|
| 把回跳接收端点专用的会话Cookie改成None加Secure | 必须走跨站POST回跳 | 放宽了这条Cookie的跨站限制,务必配合HttpOnly与令牌校验 |
| 让网关用GET重定向回来,参数放URL里 | 网关支持选择回跳方式 | Lax在跨站GET导航上是放行的,改动最小;但URL里别放敏感值 |
| 回跳落到一个中转页,由它在同站内跳转到真正的结果页 | 不想放宽任何Cookie | 中转页自己拿不到会话,得靠网关回传的订单号去查 |
| 把会话标识随回调参数带回来,服务端凭它重建会话 | iframe形态,别的招都不好使 | 最稳,但要做防重放与有效期,工作量最大 |
第二种往往是性价比最高的,很多人却想不到——因为大家的注意力都在“怎么把Cookie配对”,很少有人回头问一句“能不能干脆别用POST”。当你在下游反复调参数却一点效果都没有时,先去上游看看有没有一个开关能让整个问题不必发生。
三条别踩的
- 别把全站会话Cookie一把改成None。你需要放宽的只有回跳落地的那一个端点,全站放宽等于把跨站请求伪造的防线整条拆了。
- 别指望那两分钟。它是官方写明将来会移除的临时措施,且对iframe形态根本不生效。把它当成兜底,等于把生意押在一个随时会被删掉的兼容层上。
- 别只在沙箱里验收。沙箱环境不触发额外验证,那正是唯一能复现问题的路径。至少要用一张会触发验证的真实卡走一遍,并且在验证页上故意磨蹭三分钟。
最后回到开头那个家居站。他们最后选的是第二种,把回跳方式从表单提交换成重定向,代码改了不到十行。投诉停了,而三天的排查时间里,没有一个人的判断是错的——大家只是都在自己那一层里看,而这个故障恰好不住在任何一层里。这跟后端一清二楚、页面上却只剩一句输入有误是同一类毛病:信息在系统里是齐的,只是没有一层负责把它交到下一层手上。
常见问题解答
我的站从来没写过SameSite,是不是反而安全?
只能算暂时没事,而且条件很苛刻。没写的Cookie享有两分钟宽限,前提是回跳走的是顶层导航POST;只要验证页嵌在iframe里,这个宽限立刻作废,实测结果是一条都到不了。
何况官方文档明确写着这是临时方案、将来会移除。把没配置当成一种保护,本质上是在赌浏览器厂商不删这段代码。稳妥的做法是把它当成你有一个两分钟的缓刑期,而不是你不用改。
直接把会话Cookie改成SameSite=None,安全吗?
单独改这一条属性确实会放宽跨站限制,但它不等于门户大开,关键看有没有配套。三件事要一起做:必须加Secure,否则浏览器会把整条Cookie丢掉,等于会话直接没了;必须保留HttpOnly,防止脚本读取;表单和敏感操作必须有独立的令牌校验,不能只靠Cookie本身作为凭据。真正稳妥的做法是缩小范围,只给回跳落地那个端点单独一条放宽的Cookie,其他路径的会话Cookie维持原样。
为什么开发者工具里看不出“没写”和“写了Lax”的区别?
因为浏览器在存储的时候就把两者归一成同一个值了。实测里这两条Cookie在浏览器记录里都显示为Lax,但跨站POST的实际结果一个到达一个没到。差别不在存储的值上,而在浏览器内部另外记着的一个状态:这个Lax是你写的,还是我替你补的。这个状态没有任何对外接口暴露。所以唯一可靠的检查方式是看服务器发出的Set-Cookie原文,而不是看任何工具解析后的结果。
用了Shopify、Magento这类现成系统,还需要管这件事吗?
需要,但要先分清哪一段归你管。用托管结账的话,收单和验证都在平台自己的域名上完成,那一段的Cookie策略是平台的事。归你管的是两类情况:一是你自己接了额外的支付方式或者风控服务,回跳落在你的域名上;二是你做了品牌域名迁移,结账落在另一个注册域上。这次实测里就有4个品牌属于第二类。判据很简单:只要有任何一次跳转会离开你的注册域再回来,这件事就跟你有关。
把Cookie的有效期设长一点,能绕过两分钟的限制吗?
不能,这是两个完全不同的时间。有效期管的是这条Cookie能在浏览器里存活多久,两分钟宽限算的是这条Cookie从被创建到现在过了多久。你把有效期设成一年,它照样在创建满两分钟之后失去跨站POST的豁免。而且这个年龄是从最近一次被设置算起的,如果服务器每次响应都重发一遍同名Cookie,年龄会被刷新——这解释了为什么有些站的问题时有时无:用户在结账页多点了几下,会话Cookie被重发,计时重新开始了。
这个问题会影响SEO吗?
直接影响很小,搜索引擎爬虫不走结账流程,也不做支付回跳。但间接影响有两处值得留意。一是数据污染:会话断裂导致回跳被记成新的直接访问,来源丢失,你在做渠道价值评估时会低估付费和自然搜索的真实贡献。二是转化率失真:如果这批订单在统计里对不上账,你据此得出的落地页优化结论可能是错的。归根到底这是一个转化问题,而不是收录问题,但它会污染你判断收录价值所依赖的那份数据。
权威参考资料
本文标题:《付款回来购物车就空了,而浏览器那两分钟的宽限只发给从没配过SameSite的站》
本文链接:https://zhangwenbao.com/samesite-cookie-payment-return-session-loss.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0