PAYATHON 2026

ASP.NET MVC 5 支付回跳后 Session 丢失问题

支付小周

结论

这通常不是支付网关“销毁”了 Session 对象,而是浏览器从支付网关跨站 POST 回跳时,没有携带 ASP.NET 的 Session Cookie,通常是 ASP.NET_SessionId。服务器找不到原来的 Session ID,只能创建一个新 Session,所以读取:

Session["TstConfSESSION"]

会得到 null。

Chrome、Firefox 和 Edge 的表现不同,应先检查 Cookie 的 SameSite、Secure、域名及 HTTPS 配置。近年来,浏览器逐步收紧了跨站 Cookie 策略。即使旧系统没有改过代码,也可能在浏览器升级后突然出现问题。

延长 Session 超时时间通常不起作用。问题很可能不是 Session 已经过期,而是回跳请求根本没有带上原来的 Session Cookie。

最可能的原因:SameSite 限制

支付过程通常包含以下步骤:

  1. 用户访问会议系统,例如 https://conference.example.com。
  2. 系统创建 Session,并向浏览器写入 ASP.NET_SessionId。
  3. 用户跳转到支付网关域名。
  4. 支付网关通过表单 POST 返回会议系统。
  5. 浏览器将其判断为跨站 POST 请求。
  6. 如果 Session Cookie 被视为 SameSite=Lax,浏览器可能不会发送这个 Cookie。

SameSite=Lax 通常允许顶层跨站 GET 导航携带 Cookie,却往往不会在跨站 POST 中携带。支付网关通过自动提交表单返回商户网站时,恰好会触发这项限制。

不同浏览器、版本和隐私设置可能有不同表现。因此,即使某个 Edge 环境运行正常,也不能据此排除 Cookie 配置问题。

不要只检查 Session["TstConfSESSION"]。应使用 Chrome 或 Firefox 的开发者工具,完整记录支付前后的网络请求。

重点检查:

  • 支付前的响应是否写入了 ASP.NET_SessionId。
  • 浏览器是否保存了这个 Cookie。
  • Cookie 的 Domain、Path、SameSite、Secure 和过期时间。
  • 支付网关回跳请求的 Cookie 请求头是否包含 ASP.NET_SessionId。
  • 回跳响应是否重新下发了另一个 ASP.NET_SessionId。
  • 支付前后的 Session ID 是否一致。
  • 回跳地址是否从 HTTPS 降级为 HTTP。
  • 支付前后是否使用了不同的主机名,例如:
    • example.com
    • www.example.com
    • payment.example.com
  • 请求中是否出现额外的 301、302、307 或 308 重定向。

也可以临时记录请求信息:

var sessionId = Session.SessionID;
var cookieHeader = Request.Headers["Cookie"];
var host = Request.Url.Host;
var scheme = Request.Url.Scheme;
var method = Request.HttpMethod;

日志中不要记录支付密码、银行卡信息、完整身份凭证或其他敏感字段。

如果浏览器仍保存着 ASP.NET_SessionId,但回跳请求没有携带它,基本可以判断问题出在浏览器的 Cookie 发送策略、域名或协议不匹配,而不是 Session 超时。

配置 SameSite=None

如果跨站 POST 必须携带 Session Cookie,应将 Cookie 设置为:

SameSite=None; Secure

SameSite=None 允许浏览器跨站发送 Cookie。现代浏览器同时要求此类 Cookie 带有 Secure,所以站点必须全程使用 HTTPS。

在支持相应配置项的 ASP.NET MVC 5 / .NET Framework 环境中,可以尝试:

<configuration>
  <system.web>
    <httpCookies sameSite="None" requireSSL="true" />
    <sessionState
      mode="InProc"
      timeout="30"
      cookieSameSite="None"
      cookieless="UseCookies" />
  </system.web>
</configuration>

cookieSameSite 及相关 SameSite 行为取决于服务器实际安装的 .NET Framework、System.Web 更新和补丁。如果部署环境无法识别该属性,不要直接照搬配置。应先根据服务器当前版本确认支持方式,再在测试环境中检查最终响应里的 Set-Cookie。

最终需要确认的是,浏览器收到的响应头是否类似:

Set-Cookie: ASP.NET_SessionId=...; Path=/; Secure; HttpOnly; SameSite=None

如果应用还使用 Forms Authentication 或 OWIN Cookie Authentication,也要分别检查它们的认证 Cookie。修改 Session Cookie 不会自动改变认证 Cookie。

强制使用 HTTPS 并统一域名

即使设置了 SameSite=None,以下情况仍可能导致 Cookie 无法发送:

  • 回跳 URL 是 http://,而 Cookie 带有 Secure。
  • 支付前使用 www.example.com,回跳时却使用 example.com。
  • Cookie 的 Path 不包含回跳路径。
  • 反向代理终止了 HTTPS,但应用误以为请求使用的是 HTTP。
  • 支付网关后台仍保存着旧域名或旧协议。

应将支付网关的返回地址固定为统一的规范地址,例如:

https://conference.example.com/payment/payment

还要检查负载均衡器、CDN、WAF 和反向代理是否改写了协议、Host 或 Cookie 属性。

不要通过关闭 Secure 或退回 HTTP 来绕过问题,这会降低会话和支付流程的安全性。

不要让支付结果处理依赖浏览器 Session

即使解决了 SameSite 问题,支付回调也不应只依靠 Session 识别订单,原因包括:

  • 用户可能清理 Cookie。
  • Session 可能自然过期。
  • 应用池可能重启。
  • 支付返回可能延迟。
  • 用户可能关闭页面后重新打开。
  • 网关可能重复发送通知。
  • 支付结果可能由网关服务器直接通知,而不是通过用户浏览器返回。

创建支付请求时,应将订单、用户和支付状态保存到数据库,并生成不可猜测的关联标识:

public ActionResult StartPayment()
{
    var paymentAttempt = new PaymentAttempt
    {
        Id = Guid.NewGuid(),
        UserId = GetCurrentUserId(),
        OrderNo = CreateOrderNumber(),
        Status = "Pending",
        CreatedAtUtc = DateTime.UtcNow
    };

    db.PaymentAttempts.Add(paymentAttempt);
    db.SaveChanges();

    var callbackToken = CreateSignedCallbackToken(paymentAttempt.Id);

    return Redirect(BuildGatewayUrl(
        paymentAttempt.OrderNo,
        callbackToken));
}

支付返回后,根据订单号或签名令牌读取数据库记录,并验证支付网关签名:

[AllowAnonymous]
[HttpPost]
public ActionResult PaymentCallback(FormCollection form)
{
    if (!VerifyGatewaySignature(form))
    {
        return new HttpStatusCodeResult(400, "Invalid signature");
    }

    var orderNo = form["order_no"];
    var transactionId = form["transaction_id"];

    var payment = db.PaymentAttempts
        .SingleOrDefault(x => x.OrderNo == orderNo);

    if (payment == null)
    {
        return HttpNotFound();
    }

    if (payment.Status != "Paid")
    {
        payment.Status = "Paid";
        payment.GatewayTransactionId = transactionId;
        payment.PaidAtUtc = DateTime.UtcNow;
        db.SaveChanges();
    }

    return RedirectToAction("Result", new { id = payment.Id });
}

字段名称和签名算法必须以实际支付网关协议为准。不能只凭浏览器提交的 order_no 或 transaction_id 确认支付成功。

更稳妥的结构如下:

  • 服务器通知地址接收网关的异步通知,验证签名并更新支付状态。
  • 浏览器返回地址只负责展示结果。
  • 数据库中的订单记录是支付状态的可信来源。
  • Session 只用于改善用户体验,不是确认付款的必要条件。

调整过滤器位置

当前过滤器会在控制器 Action 执行前检查 Session:

[CustomFilter]
[HttpPost]
public ActionResult payment(string id, FormCollection frm)
{
}

如果跨站回跳没有携带 Cookie,过滤器会立即把请求重定向到首页,支付响应代码根本没有机会执行。这既会掩盖真正的问题,也可能导致合法的支付通知得不到处理。

支付回调通常不应使用要求已有 Session 的过滤器:

[AllowAnonymous]
[HttpPost]
public ActionResult PaymentCallback(FormCollection form)
{
    // 验证网关签名,根据订单号处理支付结果
}

正常的登录或 Session 检查可以保留在支付完成页:

[CustomFilter]
[HttpGet]
public ActionResult PaymentResult(Guid id)
{
    // 查询数据库并展示支付结果
    return View();
}

AllowAnonymous 只表示回调不依赖用户的登录状态,并不意味着可以跳过网关签名验证。

如果开发者工具确认回跳请求携带了相同的 ASP.NET_SessionId,但 Session 数据仍然丢失,就要继续排查服务器端问题。

应用池回收

InProc 模式将 Session 保存在 IIS 工作进程的内存中。以下操作会导致所有 Session 丢失:

  • 应用池回收。
  • IIS 或服务器重启。
  • 修改 web.config。
  • 更新 bin 目录。
  • 杀毒软件或发布程序触发文件变更。
  • 应用因长时间空闲而停止。
  • 内存限制触发回收。

检查 IIS 应用池的回收设置、空闲超时、事件查看器和部署日志。即使系统每年只运行几个月,也要确认服务器、IIS、.NET Framework、浏览器和代理设施是否在停用期间升级过。

多实例或 Web Garden

如果应用运行在多台服务器或多个工作进程中,使用 InProc Session 时,请求可能被分配到另一个进程,因而无法读取原来的 Session 数据。

可以采用以下办法:

  • 在负载均衡器上启用可靠的会话粘滞。
  • 改用共享的 Session 存储。
  • 将支付流程需要的状态持久化到数据库。

对于支付业务,将数据持久化到数据库通常比依赖粘滞会话更可靠。

Session 对象序列化

如果改用 StateServer 或 SQL Server Session,存入 Session 的自定义对象通常必须支持序列化,例如:

[Serializable]
public class ConferenceSession
{
    public string username { get; set; }
    public int cid { get; set; }
}

实际字段类型应以项目模型为准。切换 Session 模式前,还要确认对象图中的所有成员是否都支持序列化。

Session timeout 为什么没有解决问题

下面的配置只能延长服务器端 Session 的空闲时间:

<sessionState timeout="30" />

它不能解决以下问题:

  • 跨站请求不发送 Cookie。
  • Cookie 的域名或路径配置错误。
  • HTTP 与 HTTPS 不一致。
  • 应用池回收。
  • 多个实例之间不共享 Session。
  • 浏览器阻止第三方或跨站 Cookie。

建议按以下顺序排查:

  1. 确认回跳方式是 GET 还是跨站 POST。
  2. 对比支付前后的 ASP.NET_SessionId。
  3. 检查回跳请求是否携带 Session Cookie。
  4. 检查 SameSite=None; Secure 和全站 HTTPS。
  5. 检查域名、路径及重定向链。
  6. 如果 Cookie 相同,再检查 IIS 回收、多实例和 Session 存储。
  7. 将支付状态从 Session 迁移到数据库,并通过网关签名确认支付结果。

最终要解决的并不只是让某个浏览器“不再掉 Session”,而是让支付回调在没有浏览器 Session 的情况下,也能安全、幂等地完成处理。

备注:内容仅供参考。