PAYATHON 2026

ASP.Net Webforms 访问 Payment Gateway 后自动登出

支付小周

结论

跳转到支付网关通常不会清除 ASP.NET Session。更常见的情况是,支付网关通过跨站 POST 返回时,浏览器没有携带设置为 SameSite=Lax 的 Session Cookie 或身份认证 Cookie。应用因此创建了新的 Session,并把用户识别为未登录状态。

需要检查和处理以下两类 Cookie:

  • Session Cookie,通常是 ASP.NET_SessionId
  • 登录认证 Cookie,Forms Authentication 通常使用 .ASPXAUTH

如果支付网关必须通过跨站 POST 返回,应在受支持的 .NET Framework 环境中把这两类 Cookie 都配置为 SameSite=None; Secure,同时确保站点全程使用 HTTPS。建议升级到 .NET Framework 4.7.2 或 4.8,并安装对应的系统更新。.NET Framework 4.6 对新版 SameSite 规则的支持并不完整。

支付交易也不应完全依赖 Session。发起支付请求前,应将订单和交易状态保存到数据库。用户返回后,再根据交易编号向支付网关核验结果。

为什么返回后像是“Session 被清空”

SameSite=Lax 通常允许浏览器在跨站顶层 GET 导航中发送 Cookie,但不会在普通的跨站 POST 请求中发送。

许多支付网关会采用类似下面的返回方式:

POST https://example.com/checkout/payment-return.aspx

此时,浏览器可能不会发送 ASP.NET_SessionId 和 .ASPXAUTH。服务器找不到原来的 Cookie,就会:

  1. 创建一个新 Session;
  2. 生成新的 SessionID;
  3. 无法识别原来的登录身份;
  4. 将用户重定向到登录页,或者显示交易失败。

所以,出现“新 Session”并不表示旧 Session 被支付网关清除了。外部网站也无法直接删除其他域名下的 Cookie。

在浏览器开发者工具的 Network 面板中,找到支付网关返回本站的请求,然后检查 Request Headers 中的 Cookie。

重点查看:

ASP.NET_SessionId=...
.ASPXAUTH=...

也可以暂时在返回页面中记录以下信息:

protected void Page_Load(object sender, EventArgs e)
{
    string sessionId = Session.SessionID;
    bool authenticated = Request.IsAuthenticated;
    string userName = User?.Identity?.Name;
    string method = Request.HttpMethod;

    System.Diagnostics.Trace.WriteLine(
        $"Method={method}, SessionID={sessionId}, " +
        $"Authenticated={authenticated}, User={userName}");
}

可以根据结果进一步判断:

  • 如果 ASP.NET_SessionId 丢失,说明请求没有带回 Session Cookie。
  • 如果 .ASPXAUTH 丢失,说明请求没有带回登录认证 Cookie。
  • 如果两个 Cookie 都存在,但 Session 内容消失,应检查应用池、开发服务器是否重启,以及 InProc Session 是否丢失。
  • 如果 Cookie 存在但用户仍未登录,应检查 Forms Authentication 是否超时、Cookie 是否解密失败,以及 machineKey 是否发生变化。
  • 如果返回请求是 GET,Cookie 仍然丢失,应重点检查域名、端口、协议,以及 Cookie 的 Domain 和 Path。

Session 和身份认证是两套独立机制。只调整 Session Cookie,并不能保证 Forms Authentication 的登录状态仍然有效。

推荐配置

在支持新版 SameSite 行为的 .NET Framework 中,可以使用类似下面的配置:

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

    <sessionState
      mode="InProc"
      cookieless="UseCookies"
      cookieSameSite="None"
      timeout="20" />

    <authentication mode="Forms">
      <forms
        loginUrl="~/Login.aspx"
        timeout="60"
        slidingExpiration="true"
        requireSSL="true"
        cookieSameSite="None" />
    </authentication>
  </system.web>
</configuration>

使用这份配置需要注意:

  • SameSite=None 必须和 Secure 一起使用,否则现代浏览器通常会拒绝该 Cookie。
  • Secure Cookie 只通过 HTTPS 发送。如果本机使用普通的 http://localhost,测试结果可能与生产环境不同。
  • cookieSameSite 等配置项能否使用,取决于目标 .NET Framework、实际运行时版本和已安装的系统更新。如果 .NET Framework 4.6 无法识别这些属性,不要通过字符串改写 Set-Cookie 响应头来勉强兼容。应优先升级到仍受支持且完整支持 SameSite 的版本。
  • 如果应用使用的是 OWIN Cookie Authentication 或 ASP.NET Identity,而不是传统的 Forms Authentication,还需要单独配置相应的认证 Cookie。

如果支付网关允许选择返回方式,也可以把浏览器返回地址配置为顶层 GET。在这种情况下,通常可以继续使用 SameSite=Lax。不过,应用不能直接相信 URL 参数中的支付状态,仍需在服务器端进行验证。

不要依赖 Session 保存支付状态

即使 Cookie 配置没有问题,也不应只在 Session 中保存订单编号、金额或支付状态。用户可能关闭浏览器,支付过程可能超过 Session 的有效期,应用池可能重启,支付网关也可能稍后才发送通知。

发起支付前,应先创建一条待支付记录:

public string StartPayment(int orderId, int userId, decimal amount)
{
    var payment = new PaymentRecord
    {
        PaymentId = Guid.NewGuid(),
        OrderId = orderId,
        UserId = userId,
        Amount = amount,
        Status = "Pending",
        CreatedUtc = DateTime.UtcNow
    };

    paymentRepository.Insert(payment);

    return paymentGateway.CreatePaymentUrl(
        payment.PaymentId.ToString(),
        amount);
}

支付网关返回后,通过不可猜测的交易标识查找记录,并向网关验证支付结果:

protected void Page_Load(object sender, EventArgs e)
{
    string paymentIdText =
        Request.Form["payment_id"] ?? Request.QueryString["payment_id"];

    if (!Guid.TryParse(paymentIdText, out Guid paymentId))
    {
        ShowError("无效的支付请求。");
        return;
    }

    PaymentRecord payment = paymentRepository.Find(paymentId);

    if (payment == null)
    {
        ShowError("找不到对应的支付记录。");
        return;
    }

    GatewayResult result = paymentGateway.VerifyPayment(paymentIdText);

    if (!result.IsSuccessful ||
        result.Amount != payment.Amount ||
        result.Currency != payment.Currency)
    {
        ShowError("支付结果验证失败。");
        return;
    }

    paymentRepository.MarkPaid(
        payment.PaymentId,
        result.GatewayTransactionId,
        DateTime.UtcNow);

    Response.Redirect("~/checkout/payment-complete.aspx", false);
    Context.ApplicationInstance.CompleteRequest();
}

具体字段应以支付网关提供的 API 为准。处理时应遵循以下原则:

  • 不要直接相信浏览器提交的“支付成功”状态。
  • 校验签名、交易编号、金额、币种和订单归属。
  • 支付完成操作必须具备幂等性,防止重复回调造成重复入账。
  • 优先通过支付网关的服务器端通知或 Webhook 确认支付结果。
  • Webhook 是服务器请求,不会携带用户的 Session 或登录 Cookie。

本机开发时还要检查

开发环境中还经常遇到这些问题:

  • 发起支付时使用 https://localhost,返回时却使用机器名、IP 地址或临时隧道域名。
  • 发起地址是 www.example.com,返回地址却是 example.com。
  • Cookie 已标记为 Secure,返回地址却使用 HTTP。
  • Visual Studio 停止调试或重新编译,导致开发服务器、IIS Express 或应用域重启。
  • 应用使用 InProc Session,应用池回收后所有 Session 数据随之丢失。
  • 多台服务器没有共享 Session,负载均衡也没有启用会话粘滞。
  • 多个本地应用使用相同的 Cookie 名称和域名,造成 Cookie 相互覆盖。

StateServer、SQL Server Session 或分布式 Session 可以防止应用进程重启造成的数据丢失,但无法解决浏览器没有发送 Session Cookie 的问题。

最终需要两层保障:一是正确配置 Session Cookie 和认证 Cookie,让用户正常返回时仍然保持登录;二是持久化支付交易并向网关核验结果,避免偶发的 Session 丢失影响付款记录的完成。

备注:内容仅供参考。