支付网关 POST 到 Success.aspx 时 Session 为空
结论
支付网关向 Success.aspx 提交请求时,只会发送请求参数,不会自动带上商户网站原有的 ASP.NET Session。
ASP.NET 通常通过浏览器中的 ASP.NET_SessionId Cookie 识别 Session。由支付网关服务器直接发起的请求不会携带用户浏览器中的 Cookie,因此 ASP.NET 会为它创建新的 Session。即使请求由浏览器从支付网关跨站 POST 回来,也可能因为 Cookie、SameSite、域名或协议变化而无法恢复原 Session。
支付结果不应通过 Session 关联。应使用订单号或一次性交易标识,并将交易状态保存在数据库等持久化存储中。
为什么 Session 会变成空值
支付回调通常有两种形式。
支付网关服务器直接 POST
请求路径类似:
Payment Gateway Server -> POST -> Success.aspx
请求由支付网关服务器发出,不会包含用户浏览器保存的:
ASP.NET_SessionId
ASP.NET 无法判断请求属于哪个用户,只能创建新的 Session。因此:
Session["OrderId"]
会得到 null。
用户浏览器跨站 POST 返回
请求路径类似:
Browser -> Payment Gateway -> POST -> Success.aspx
此时能否恢复 Session,取决于浏览器是否发送 Cookie。以下情况可能导致 Cookie 缺失:
- 返回地址使用了不同域名,例如
www.example.com和example.com。 - 支付前后在 HTTP 与 HTTPS 之间切换。
- Session Cookie 的
Secure、域或路径配置不匹配。 - 浏览器对跨站 POST 应用
SameSiteCookie 限制。 - 用户停留时间过长,Session 已经过期。
- 应用程序池回收,且使用进程内 Session。
- 多服务器部署没有共享 Session,或者负载均衡没有正确配置。
如果请求跳转到了登录页,也可能是身份验证 Cookie 丢失,并非 Session 丢失。ASP.NET Forms Authentication Cookie 和 ASP.NET_SessionId 是两套不同的 Cookie,需要分别检查。
推荐的处理方式
发起支付前先创建订单记录,并把业务所需信息保存到数据库:
string orderId = Guid.NewGuid().ToString("N");
CreatePendingOrder(new Order
{
OrderId = orderId,
UserId = CurrentUserId,
Amount = amount,
Status = "Pending"
});
将 orderId 写入支付网关支持的商户订单号、参考号或自定义字段。具体名称以支付网关的接口文档为准,不能假设该字段一定叫 orderId。
收到支付网关回调后,从 Request.Form 读取这个标识:
protected void Page_Load(object sender, EventArgs e)
{
string responseCode = Request.Form["responsecode"];
string orderId = Request.Form["orderid"];
string transactionId = Request.Form["transactionid"];
string signature = Request.Form["signature"];
if (string.IsNullOrWhiteSpace(orderId))
{
Response.StatusCode = 400;
Response.End();
return;
}
if (!VerifyGatewaySignature(Request.Form))
{
Response.StatusCode = 400;
Response.End();
return;
}
Order order = GetOrderById(orderId);
if (order == null)
{
Response.StatusCode = 404;
Response.End();
return;
}
// 应同时校验金额、币种、商户号和交易标识。
if (responseCode == "SUCCESS")
{
MarkOrderAsPaid(orderId, transactionId);
}
else
{
MarkOrderAsFailed(orderId, responseCode);
}
Response.StatusCode = 200;
}
这里的 SUCCESS、orderid、transactionid 和签名字段只是为了展示处理结构。实际字段名和成功状态值必须按照 First Cardinal Commerce 提供的协议确定。
回调页面不要依赖登录状态
支付网关的回调地址通常需要允许匿名访问,否则认证模块可能在业务代码运行前将请求重定向到登录页。
可以在 Web.config 中只为回调页面开放匿名访问:
<configuration>
<location path="Success.aspx">
<system.web>
<authorization>
<allow users="*" />
</authorization>
</system.web>
</location>
</configuration>
允许匿名访问不代表可以直接信任请求。页面必须验证支付网关提供的签名、摘要或消息认证码,并核对:
- 商户号
- 订单号
- 支付金额
- 币种
- 交易状态
- 交易流水号
- 签名或 MAC
- 是否已经处理过该回调
更新支付状态时还要保证幂等性,防止网关重试造成重复记账。
将后台通知与用户返回页面分开
如果支付网关同时提供服务器通知地址和用户返回地址,建议分别使用两个页面:
PaymentNotify.aspx 接收服务器通知并更新订单状态
PaymentReturn.aspx 向用户展示支付结果
PaymentNotify.aspx 不依赖 Session,也不要求用户登录,只验证通知并更新数据库。
PaymentReturn.aspx 可以从查询参数或一次性令牌中获取订单号,再到数据库读取实际支付状态:
protected void Page_Load(object sender, EventArgs e)
{
string orderId = Request.QueryString["orderId"];
Order order = GetOrderById(orderId);
if (order == null || order.UserId != CurrentUserId)
{
Response.StatusCode = 404;
return;
}
lblStatus.Text = order.Status;
}
不能仅凭浏览器返回页面中的 responsecode 把订单标记为已付款。浏览器提交的参数可能被修改,支付结果应以通过签名验证的后台通知或主动查询支付网关得到的结果为准。
如果确认是浏览器返回请求
可以检查返回请求中是否包含以下 Cookie:
ASP.NET_SessionId
.ASPXAUTH
还要确保开始支付和返回网站时使用完全一致的 HTTPS 主机名。较新的浏览器处理跨站 POST 时,可能还需要正确配置 SameSite=None; Secure。旧版 .NET Framework 对 SameSite 的支持取决于具体补丁级别,不能只根据 ”.NET Framework 4.0” 判断配置方式,需要结合服务器实际安装的 .NET 更新版本验证。
即使调整 Cookie 后可以恢复 Session,支付结果处理也不能假定 Session 一定存在。用户可能禁用 Cookie、等待过久或直接关闭页面,但支付网关发出的服务器通知仍需得到正确处理。
其他容易混淆的问题
如果 Success.aspx 确实需要使用 Session,应确认页面没有禁用 Session:
<%@ Page Language="C#" EnableSessionState="true" %>
这项设置只能允许页面使用 Session,不能让第三方请求获得用户原来的 Session。
在 Web Farm 或应用程序池频繁回收的环境中,InProc Session 也可能丢失。使用 State Server 或 SQL Server Session State 可以改善应用内部的 Session 持久性,但无法解决支付网关请求没有携带原 Session ID 的问题。
不要把 Session ID 放入支付参数或 URL 中来 “恢复” 用户 Session,这可能引发 Session 固定、会话劫持和敏感信息泄露。应传递随机、不可猜测且用途受限的订单标识或一次性令牌,并通过数据库和网关签名完成身份关联与支付确认。
备注:内容仅供参考。
