PAYATHON 2026

从外部支付网站返回后 ASP Session variables 被清除

支付老李

结论

支付网站通常无法直接清除你服务器上的 ASP.NET Session。更常见的情况是,支付成功后的返回请求没有携带原来的 ASP.NET_SessionId Cookie,ASP.NET 随后创建了新 Session,因此看起来像是 Session variables 被清空了。

如果取消支付后 Session 正常,而支付成功后丢失,应先比较两条返回路径。常见差异有:

  • 取消时通过顶层 GET 重定向,成功时则是跨站 POST。
  • 成功地址与取消地址使用了不同的协议、主机名或路径。
  • 成功页面在 iframe、弹窗、内置浏览器或新的浏览器上下文中打开。
  • 浏览器采用了不同的 SameSite Cookie 策略。
  • 本地环境使用 HTTP,但 Cookie 被标记为 Secure。
  • 多个 localhost 应用使用了默认的 ASP.NET_SessionId Cookie 名称。

支付结果的最终处理不应建立在 Session 必然存在的前提上。更可靠的做法是,通过订单号或一次性关联令牌,将支付请求与数据库中的待支付记录关联,再使用支付平台签名、服务端查询或 webhook 验证结果。

首先确认是不是换了 Session

在发起支付和支付返回的位置分别记录 Session ID:

protected void Page_Load(object sender, EventArgs e)
{
    System.Diagnostics.Trace.WriteLine(
        "SessionID: " + Session.SessionID);
}

同时打开浏览器开发者工具,在 Network 面板中检查:

  1. 离开网站前,响应是否设置了 ASP.NET_SessionId。

  2. 取消支付的返回请求是否携带:

    Cookie: ASP.NET_SessionId=...
  3. 支付成功的返回请求是否携带同一个 Cookie。

  4. 两种返回请求分别使用 GET 还是 POST。

  5. 返回地址的 scheme、host 和 path 是否符合预期。

  6. Console 或 Cookies 面板是否提示 Cookie 因 SameSite、Secure 或第三方 Cookie 策略而被阻止。

如果成功返回时没有携带 Cookie,问题出在客户端传递 Cookie 的环节。如果请求携带了相同的 Session ID,但 Session 数据依然不存在,则要继续检查服务器端 Session 存储、应用程序重启以及清理 Session 的代码。

最可能的原因:SameSite 与跨站 POST

现代浏览器通常会更严格地处理未明确声明 SameSite 的 Cookie。在 SameSite=Lax 下,顶层跨站 GET 导航通常仍会携带 Cookie,而跨站 POST 往往不会。

这可以解释以下情况:

  • 取消支付通过 GET 返回,所以 Session 得以保留。
  • 支付成功后,支付平台通过表单向返回地址发起跨站 POST,浏览器没有发送 Session Cookie。
  • 服务器收到请求后分配了新的 ASP.NET_SessionId。

先确认支付平台采用哪种成功返回方式。如果平台支持,优先让浏览器通过顶层 GET 返回结果展示页面,支付状态则通过 webhook 或服务端 API 确认。

如果业务必须让跨站 POST 携带 Session Cookie,该 Cookie 通常需要设置为:

SameSite=None; Secure

根据补丁和运行时支持情况,ASP.NET 配置可能类似:

<configuration>
  <system.web>
    <sessionState
      cookieSameSite="None"
      cookieName="MyApplication.Session"
      timeout="20" />

    <httpCookies
      sameSite="None"
      requireSSL="true" />
  </system.web>
</configuration>

这些属性能否使用,取决于实际安装的 .NET Framework 更新和 ASP.NET 补丁,不能只看项目的目标版本。部署前需要在当前运行环境中确认配置是否被识别。

此外,SameSite=None Cookie 通常还必须带有 Secure,而 Secure Cookie 不会通过普通 HTTP 发送。因此,本地测试地址应使用 HTTPS,例如:

https://localhost:44300/payment/return

如果本地地址仍是 http://localhost,直接设置 SameSite=None; Secure 反而可能使浏览器完全不发送该 Cookie。

检查返回地址是否改变了站点身份

Cookie 不按端口隔离,但会受到域名、路径和协议安全要求等条件限制。以下地址在开发者看来可能指向同一个应用,但浏览器不一定会将它们视为相同的 Cookie 环境:

http://localhost:5000/
http://127.0.0.1:5000/
https://localhost:44300/
https://machine-name.local/

检查支付成功地址是否从 localhost 变成了 127.0.0.1、计算机名或其他子域,同时确认支付平台是否规范化了成功地址。

多个本地 ASP.NET 应用还可能在 localhost 下使用相同的默认 Cookie 名称。由于 Cookie 不按端口隔离,一个应用可能覆盖另一个应用的 ASP.NET_SessionId。可以为每个应用设置独立名称,避免此类冲突:

<configuration>
  <system.web>
    <sessionState cookieName="MyPaymentApp.Session" />

    <authentication mode="Forms">
      <forms name="MyPaymentApp.Auth" />
    </authentication>
  </system.web>
</configuration>

如果返回请求携带的 Session ID 与离开前完全相同,但数据已经消失,应检查:

  • IIS 应用程序池是否发生过回收。
  • 应用是否重新编译,或者 bin、web.config 是否被触碰。
  • Session 是否使用默认的 InProc 模式。
  • 是否启用了 web garden,或者请求是否被分发到不同进程。
  • 代码中是否调用了 Session.Clear()、Session.RemoveAll() 或 Session.Abandon()。
  • 成功回调是否经过了其他应用、虚拟目录或认证流程。
  • Session 是否在支付过程中超时。
  • 返回页面是否禁用了 Session state。
  • 是否存在同名 Cookie,导致浏览器发送多个 ASP.NET_SessionId。

默认的 InProc Session 保存在 IIS 工作进程中。进程回收后,即使浏览器仍保留原来的 Session ID,Session 数据也已丢失。如果需要跨进程或跨服务器保存 Session,可以考虑 SQL Server Session State、State Server 或应用自己的持久化存储。不过在支付业务中,数据库订单记录仍然比 Session 更合适。

支付流程不应依赖 Session 完成交易

Session 可以改善用户返回后的页面体验,但不能作为确认支付归属的唯一依据。用户可能关闭浏览器、禁用 Cookie 或换一台设备完成支付,应用程序池也可能在支付过程中回收。

更可靠的流程如下:

  1. 发起支付前创建本地订单。
  2. 生成不可预测的一次性关联令牌。
  3. 将订单、用户、金额和令牌保存到数据库。
  4. 将订单号或令牌写入支付平台支持的商户参考字段或返回参数。
  5. 返回后验证支付平台签名,或者由服务器调用支付平台 API 查询交易。
  6. 根据数据库中的订单关系确定用户。
  7. 以幂等方式更新订单,避免重复回调造成问题。

示例:

public ActionResult StartPayment(decimal amount)
{
    var userId = GetAuthenticatedUserId();
    var state = Convert.ToBase64String(
        System.Security.Cryptography.RandomNumberGenerator.GetBytes(32));

    var order = new PaymentOrder
    {
        Id = Guid.NewGuid(),
        UserId = userId,
        Amount = amount,
        State = state,
        Status = "Pending",
        CreatedUtc = DateTime.UtcNow
    };

    db.PaymentOrders.Add(order);
    db.SaveChanges();

    var returnUrl = Url.Action(
        "PaymentReturn",
        "Payment",
        new { orderId = order.Id, state = state },
        protocol: Request.Url.Scheme);

    var paymentUrl = paymentClient.CreatePayment(
        merchantReference: order.Id.ToString(),
        amount: amount,
        returnUrl: returnUrl);

    return Redirect(paymentUrl);
}

处理返回请求时,不能只相信浏览器提交的 status=success:

[HttpGet]
public async Task<ActionResult> PaymentReturn(Guid orderId, string state)
{
    var order = db.PaymentOrders.SingleOrDefault(x => x.Id == orderId);

    if (order == null ||
        !FixedTimeEquals(order.State, state))
    {
        return new HttpStatusCodeResult(400);
    }

    var payment = await paymentClient.GetPaymentAsync(
        order.Id.ToString());

    if (!payment.IsPaid ||
        payment.Amount != order.Amount)
    {
        return View("PaymentNotConfirmed");
    }

    if (order.Status != "Paid")
    {
        order.Status = "Paid";
        order.PaidUtc = DateTime.UtcNow;
        order.ProviderTransactionId = payment.TransactionId;
        db.SaveChanges();
    }

    return View("PaymentComplete", order);
}

实际项目还需要验证支付平台提供的签名,并使用固定时间比较处理安全令牌。未经签名的 userId、金额和支付状态都不能视为可信数据,也不能只凭返回页面更新订单。

推荐的排查顺序

先比较成功和取消支付时返回请求的 HTTP method、完整 URL 与 Cookie。如果成功请求缺少 ASP.NET_SessionId,重点检查 SameSite、HTTPS、返回方式和 localhost Cookie 冲突。如果 Session ID 相同但数据不存在,再检查 InProc Session、应用程序池回收以及清理 Session 的代码。

即使最终恢复了 Session Cookie,也应把支付完成逻辑改为基于持久化订单和服务端验证。这样一来,Session 丢失最多只会影响返回页面的登录状态,不会影响交易落库和用户归属。

备注:内容仅供参考。