IIS 7 部署后部分用户从 Payment Gateway 返回时 Session 丢失
明确结论
最可能的原因是启用了 cookieless="true"。
在 Cookieless Session 模式下,ASP.NET 不使用 Cookie 保存 Session ID,而是把它编码在 URL 中,例如:
https://example.com/(S/abc123xyz)/Payment/Start
用户跳转到 Payment Gateway 后,网关通常会将用户重定向到预先配置的返回地址:
https://example.com/Payment/Return
由于该地址不包含原来的 Session ID,ASP.NET 会创建一个新的 Session,看起来就像原 Session 丢失了。
如果问题只出现在部分用户身上,通常是因为他们经过的支付流程、网关返回方式、浏览器行为、代理缓存或 URL 重写规则有所不同,不一定与 IP 本身有关。
首选解决方案
将 Session 改为使用 Cookie:
<configuration>
<system.web>
<sessionState
mode="InProc"
cookieless="UseCookies"
timeout="20" />
</system.web>
</configuration>
也可以写成:
<sessionState mode="InProc" cookieless="false" timeout="20" />
修改配置后,重新完成一次完整的支付流程,并确认:
- 跳转到网关前,响应已经设置
ASP.NET_SessionId。 - 从网关返回时,浏览器仍会向应用发送该 Cookie。
- 支付前后的域名、协议和应用虚拟目录一致。
- 返回地址没有从
www.example.com变成example.com,也没有从 HTTPS 变成 HTTP。 - IIS URL Rewrite、反向代理或负载均衡器没有修改 Cookie 的域、路径或协议。
跨站支付返回还要检查 SameSite
Payment Gateway 的返回请求属于跨站请求。现代浏览器可能按照 SameSite 策略限制 Session Cookie,网关通过跨站 POST 返回时尤其需要注意。
如果应用使用的运行时和补丁级别支持相关配置,可以将 Session Cookie 限制为仅通过 HTTPS 发送,同时允许必要的跨站请求:
<configuration>
<system.web>
<httpCookies
requireSSL="true"
sameSite="None" />
<sessionState
mode="InProc"
cookieless="UseCookies"
timeout="20"
cookieSameSite="None" />
</system.web>
</configuration>
并非所有旧版 .NET Framework 都支持 cookieSameSite。IIS 7 只是宿主环境,能否使用该属性取决于服务器安装的 .NET Framework 版本和补丁。未确认支持前不要直接加入该属性,否则可能导致配置解析失败。
如果支付网关通过顶层 GET 重定向返回,SameSite=Lax 通常能在很多浏览器中正常工作。如果使用跨站 POST,则要重点检查 SameSite=None; Secure。
不要让支付结果依赖 Session
即使 Session 问题已经修复,支付回调也不应依赖 Session 中保存的订单号或支付状态,原因包括:
- 用户可能禁用或清除 Cookie。
- 支付过程可能超过
timeout="20"。 - IIS 应用程序池可能发生回收。
InProcSession 会在进程重启后全部丢失。- 网关发送的服务器通知通常不会携带用户浏览器中的 Session Cookie。
- 用户可能通过不同的浏览器、设备或标签页完成支付。
更稳妥的做法是,在跳转到网关前将订单保存到数据库,并向网关传递一个不可猜测的支付交易标识。返回时使用该标识重新查询订单,同时验证网关签名。
public ActionResult StartPayment(int orderId)
{
var order = orderRepository.Get(orderId);
if (order == null)
return HttpNotFound();
var paymentReference = Guid.NewGuid().ToString("N");
paymentRepository.Create(new PaymentRecord
{
OrderId = order.Id,
Reference = paymentReference,
Status = "Pending",
CreatedAtUtc = DateTime.UtcNow
});
var returnUrl = Url.Action(
"Return",
"Payment",
new { reference = paymentReference },
protocol: Request.Url.Scheme);
var gatewayUrl = gatewayClient.CreatePayment(order, returnUrl);
return Redirect(gatewayUrl);
}
返回处理示例:
[HttpPost]
public ActionResult Return(string reference)
{
if (!gatewayClient.IsValidSignature(Request))
return new HttpStatusCodeResult(400);
var payment = paymentRepository.FindByReference(reference);
if (payment == null)
return HttpNotFound();
var gatewayResult = gatewayClient.VerifyPayment(Request);
if (gatewayResult.IsPaid)
{
paymentRepository.MarkAsPaid(
payment.Id,
gatewayResult.TransactionId);
}
return RedirectToAction(
"Result",
new { reference = reference });
}
reference 只用于定位服务器端记录,不能单独证明支付成功。应用必须验证网关签名、交易金额、币种、商户号和交易状态,还要确保重复回调不会引发重复扣款或重复发货。
仍然丢失时的 IIS 检查项
改用 Cookie 后如果问题仍然存在,还要继续排查 InProc 本身的限制:
-
应用程序池回收
InProcSession 保存在 IIS 工作进程中。应用程序池回收、配置文件变化、程序集更新或进程崩溃都会清空 Session。 -
Web Garden
如果应用程序池的
Maximum Worker Processes大于1,同一用户的请求可能被分配到不同进程。每个进程都有各自独立的InProcSession。 -
多服务器部署
如果负载均衡器将请求分配到不同服务器,
InProcSession 无法在服务器之间共享。此时可以配置可靠的会话保持,或改用StateServer、SQL Server 等进程外 Session 存储。 -
主机名和协议变化
Cookie 对域、路径和 HTTPS 属性很敏感。支付前后切换了子域、虚拟目录或协议,都可能导致浏览器不再发送原来的 Session Cookie。
-
返回耗时超过 20 分钟
timeout="20"表示 Session 在连续 20 分钟没有收到访问后过期,并不代表它会在整个支付流程中一直有效。
建议的最终处理方式
关闭 Cookieless Session,统一支付前后的 HTTPS 域名,并检查 SameSite 配置和 Cookie 的返回情况。同时,将订单和支付状态持久化到数据库,通过交易引用和网关签名恢复支付上下文,不要再把 Session 作为支付流程唯一的状态来源。
备注:内容仅供参考。
