ASP MVC4 支付成功回调后 Session 被销毁
结论
支付网关跳转通常不会“销毁” ASP.NET Session。问题一般出在以下两处:
- 回调请求没有携带原来的
ASP.NET_SessionIdCookie,服务器因此创建了新 Session。 - Session 保存在进程内,但应用程序池回收、应用重启或负载均衡切换节点后,原有的 Session 数据已经丢失。
支付成功处理不应依赖 Session。支付网关发送的服务端通知通常不会携带用户浏览器的 Cookie。即使由浏览器跳转,Cookie 也可能受到域名、HTTPS、SameSite 等规则的影响。订单号、支付流水号和支付状态应保存到数据库中,回调签名验证通过后,再根据回调参数查询订单。
先区分两种“回调”
支付平台通常会提供两类地址:
return_url:付款完成后,用户浏览器跳回商户网站。notify_url或 webhook:支付平台服务器直接请求商户服务器。
如果请求来自支付平台服务器,它不会拥有用户浏览器中的 ASP.NET_SessionId。服务器在这种情况下创建新 Session,属于正常现象。
浏览器跳转也不能保证原 Session 一定可用。例如,支付平台通过跨站 POST 提交结果时,浏览器可能因为 SameSite 策略而不发送 Session Cookie。
排查 Session Cookie 是否发生变化
可以临时记录回调前后的 Session ID、Cookie 和请求地址:
public ActionResult BeforePayment()
{
System.Diagnostics.Trace.WriteLine(
"BeforePayment SessionID: " + Session.SessionID);
System.Diagnostics.Trace.WriteLine(
"BeforePayment Cookie: " +
Request.Cookies["ASP.NET_SessionId"]?.Value);
return View();
}
public ActionResult PaymentSuccess()
{
System.Diagnostics.Trace.WriteLine(
"PaymentSuccess SessionID: " + Session.SessionID);
System.Diagnostics.Trace.WriteLine(
"PaymentSuccess Cookie: " +
Request.Cookies["ASP.NET_SessionId"]?.Value);
System.Diagnostics.Trace.WriteLine(
"Callback URL: " + Request.Url);
return View();
}
还需要在浏览器开发者工具的 Network 面板中检查回调请求:
- 请求是否携带
ASP.NET_SessionId。 - 回调前后的 Cookie 值是否一致。
- Cookie 是否因
SameSite、Secure或 Domain 不匹配而被拦截。 - 支付前后的域名是否发生变化,例如
example.com和www.example.com。 - 是否从 HTTPS 跳转到了 HTTP。
- 支付网关使用的是
GET跳转还是跨站POST。
访问 Session.SessionID 本身可能触发新 Session 的创建。因此,看到新的 Session ID,只能说明原 Cookie 或原服务端状态没有恢复,不能据此认定支付网关主动删除了 Session。
检查域名、协议和 Cookie 设置
以下地址在 Cookie 看来可能属于不同环境:
https://example.com
https://www.example.com
http://example.com
https://pay.example.com
支付前页面和支付成功返回地址应尽量使用完全一致的协议与主机名,并统一跳转到一个规范域名。
如果支付网关通过跨站 POST 返回,现代浏览器通常要求相关 Cookie 使用:
SameSite=None; Secure
对于已经更新且支持相应 SameSite 配置的 .NET Framework,可以根据实际目标框架配置 Session Cookie:
<configuration>
<system.web>
<httpCookies requireSSL="true" sameSite="None" />
<sessionState
mode="InProc"
cookieless="UseCookies"
cookieSameSite="None"
timeout="30" />
</system.web>
</configuration>
SameSite=None 必须配合 HTTPS 和 Secure 使用。MVC4 项目可能运行在不同版本的 .NET Framework 上,旧版本或没有安装相关更新的运行环境未必支持 sameSite、cookieSameSite 属性。如果配置节提示无法识别,应先确认项目的目标框架、服务器实际安装的 .NET Framework 版本以及补丁情况,不要直接把这段配置复制到生产环境。
如果支付平台支持顶层 GET 跳转,SameSite=Lax 通常可以携带 Cookie。跨站 POST 更容易触发限制。不过,支付结果的传递方式必须遵循支付平台的接口规范,不能为了保留 Session 自行修改。
检查 InProc Session 是否丢失
MVC4 常见的默认配置是:
<sessionState mode="InProc" />
InProc 将 Session 保存在当前 IIS 工作进程中。出现以下情况时,Session 数据会丢失:
- IIS 应用程序池回收。
- Web 应用重启。
- 修改
web.config或部署文件。 bin目录发生变化。- 服务器内存压力导致进程终止。
- Web Farm 或负载均衡将回调请求分配到另一台服务器。
多实例部署应考虑使用 StateServer、SQLServer 或其他共享状态方案,不要依赖单机 InProc。例如,使用 ASP.NET SQL Session State 时,需要按照实际环境配置连接信息:
<configuration>
<system.web>
<sessionState
mode="SQLServer"
sqlConnectionString="data source=YOUR_SQL_SERVER;Initial Catalog=ASPState;Integrated Security=True"
cookieless="UseCookies"
timeout="30" />
</system.web>
</configuration>
数据库还需要使用相应的 .NET Framework 工具,并根据部署方式初始化 Session State 架构。连接字符串、权限和高可用方案都应按照实际环境确定。
在负载均衡环境中,还要检查:
- 是否启用了会话保持。
- 所有节点是否使用一致的配置。
- 身份验证相关的
machineKey是否一致。 - 所有节点能否访问共享 Session 服务。
会话保持可以减少节点切换引发的问题,但支付状态仍然不应只保存在 Session 中。
推荐的支付处理方式
发起支付前,先将订单保存到数据库,并生成不可猜测或符合支付平台规范的商户订单号:
public ActionResult CreatePayment(int productId)
{
var userId = GetCurrentUserId();
var order = new PaymentOrder
{
OrderNumber = Guid.NewGuid().ToString("N"),
UserId = userId,
ProductId = productId,
Status = "Pending",
CreatedAtUtc = DateTime.UtcNow
};
db.PaymentOrders.Add(order);
db.SaveChanges();
var paymentUrl = paymentGateway.CreatePaymentUrl(
order.OrderNumber,
order.Amount);
return Redirect(paymentUrl);
}
收到回调后,应使用商户订单号查询数据库,而不是从 Session 中读取订单:
[HttpPost]
public ActionResult PaymentNotify()
{
var callbackData = Request.Form;
if (!paymentGateway.VerifySignature(callbackData))
{
return new HttpStatusCodeResult(400, "Invalid signature");
}
var orderNumber = callbackData["merchant_order_no"];
var transactionId = callbackData["transaction_id"];
var paymentStatus = callbackData["payment_status"];
var order = db.PaymentOrders
.SingleOrDefault(x => x.OrderNumber == orderNumber);
if (order == null)
{
return HttpNotFound();
}
if (paymentStatus == "SUCCESS" && order.Status != "Paid")
{
order.Status = "Paid";
order.TransactionId = transactionId;
order.PaidAtUtc = DateTime.UtcNow;
db.SaveChanges();
}
return Content("success");
}
这里的参数名称和成功响应内容只是结构示例,必须替换为支付平台文档规定的字段和值。
浏览器返回页面同样应从数据库查询最终状态:
[HttpGet]
public ActionResult PaymentSuccess(string orderNo)
{
var order = db.PaymentOrders
.SingleOrDefault(x => x.OrderNumber == orderNo);
if (order == null)
{
return HttpNotFound();
}
return View(order);
}
如果订单与登录用户关联,还要验证当前用户是否有权查看该订单,不能只凭 URL 中的 orderNo 返回订单详情。
安全和可靠性注意事项
支付回调至少需要做到以下几点:
- 按照支付平台规范验证签名或消息认证码。
- 核对商户号、订单号、金额、币种和支付状态。
- 不把浏览器返回页面作为确认支付成功的唯一依据。
- 以服务端通知或主动查询支付平台的结果为准。
- 实现幂等处理,避免重复通知导致重复发货或重复记账。
- 不把支付结果、金额或用户身份只保存在 Session 中。
- 不在回调日志中记录密钥、完整银行卡信息或其他敏感数据。
- Session 丢失后允许用户重新登录,并根据数据库中的订单恢复支付结果。
保留 Session Cookie 可以改善用户支付后的体验,但支付流程本身不应依赖 Session。发起支付前将订单持久化,收到回调后验证签名并按订单号更新数据库,用户返回页面时再从数据库读取最终状态。
备注:内容仅供参考。
