PAYATHON 2026

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 应用程序池可能发生回收。
  • InProc Session 会在进程重启后全部丢失。
  • 网关发送的服务器通知通常不会携带用户浏览器中的 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 本身的限制:

  1. 应用程序池回收

    InProc Session 保存在 IIS 工作进程中。应用程序池回收、配置文件变化、程序集更新或进程崩溃都会清空 Session。

  2. Web Garden

    如果应用程序池的 Maximum Worker Processes 大于 1,同一用户的请求可能被分配到不同进程。每个进程都有各自独立的 InProc Session。

  3. 多服务器部署

    如果负载均衡器将请求分配到不同服务器,InProc Session 无法在服务器之间共享。此时可以配置可靠的会话保持,或改用 StateServer、SQL Server 等进程外 Session 存储。

  4. 主机名和协议变化

    Cookie 对域、路径和 HTTPS 属性很敏感。支付前后切换了子域、虚拟目录或协议,都可能导致浏览器不再发送原来的 Session Cookie。

  5. 返回耗时超过 20 分钟

    timeout="20" 表示 Session 在连续 20 分钟没有收到访问后过期,并不代表它会在整个支付流程中一直有效。

建议的最终处理方式

关闭 Cookieless Session,统一支付前后的 HTTPS 域名,并检查 SameSite 配置和 Cookie 的返回情况。同时,将订单和支付状态持久化到数据库,通过交易引用和网关签名恢复支付上下文,不要再把 Session 作为支付流程唯一的状态来源。

备注:内容仅供参考。