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,就会:
- 创建一个新 Session;
- 生成新的
SessionID; - 无法识别原来的登录身份;
- 将用户重定向到登录页,或者显示交易失败。
所以,出现“新 Session”并不表示旧 Session 被支付网关清除了。外部网站也无法直接删除其他域名下的 Cookie。
先确认是哪个 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 内容消失,应检查应用池、开发服务器是否重启,以及
InProcSession 是否丢失。 - 如果 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。SecureCookie 只通过 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 或应用域重启。
- 应用使用
InProcSession,应用池回收后所有 Session 数据随之丢失。 - 多台服务器没有共享 Session,负载均衡也没有启用会话粘滞。
- 多个本地应用使用相同的 Cookie 名称和域名,造成 Cookie 相互覆盖。
StateServer、SQL Server Session 或分布式 Session 可以防止应用进程重启造成的数据丢失,但无法解决浏览器没有发送 Session Cookie 的问题。
最终需要两层保障:一是正确配置 Session Cookie 和认证 Cookie,让用户正常返回时仍然保持登录;二是持久化支付交易并向网关核验结果,避免偶发的 Session 丢失影响付款记录的完成。
备注:内容仅供参考。
