如何在 ASP.NET Session 过期前延长 timeout
结论
可以在用户跳转到支付网关前,通过 Session.Timeout 临时延长当前用户的 Session,例如从 20 分钟延长到 60 分钟。不过,ASP.NET 没有可靠的机制可以“暂停 Session 过期”。用户停留在第三方支付页面时,网站收不到请求,Session 仍会继续计时。
订单、座位锁定状态和支付结果不应只保存在 Session 中。应将待支付订单持久化到数据库,并通过订单号关联支付请求、同步返回和异步通知。这样,即使 Session 已经过期,系统仍能确认订单状态。
临时延长当前 Session
跳转到支付网关前,可以设置 Session.Timeout。该属性以分钟为单位,只影响当前 Session:
protected void PayButton_Click(object sender, EventArgs e)
{
string orderId = CreatePendingOrder();
Session["PendingOrderId"] = orderId;
// 临时将当前 Session 的超时时间延长到 60 分钟
Session.Timeout = 60;
string paymentUrl = BuildPaymentUrl(orderId);
Response.Redirect(paymentUrl);
}
如果用户成功返回网站,并且原 Session 还没有过期,可以恢复默认值:
protected void PaymentReturn_Load(object sender, EventArgs e)
{
string orderId = Request.QueryString["orderId"];
VerifyAndProcessPayment(orderId);
// 恢复当前 Session 的默认超时时间
Session.Timeout = 20;
}
这种做法只能降低支付期间 Session 过期的概率,无法保证 Session 一直存在。应用程序重启、应用程序池回收,以及使用 InProc 模式时服务器进程异常退出,都可能导致 Session 提前丢失。
global.asax 能做什么
可以在 Session_Start 中设置默认超时时间:
protected void Session_Start(object sender, EventArgs e)
{
Session.Timeout = 20;
}
这种方式适合设置所有 Session 的初始值,但无法判断用户是否正在第三方支付页面。用户离开网站后,global.asax 不会持续收到其状态,也不能暂停 Session 计时。
理论上,可以在部分请求事件中根据当前路径调整 Session.Timeout。但最后一次请求结束后,ASP.NET 仍然只能等待设定的时间,无法判断用户正在支付、已经关闭浏览器,还是离开了网站。
也不要依赖 Session_End 释放座位或取消订单。它只会在部分 Session 存储模式下触发,不能作为可靠的业务事件通知。
推荐的支付流程
跳转到支付网关前,应先创建一条持久化订单记录,其中可以包含:
- 不可猜测的订单号
- 用户标识
- 电影场次和座位
- 应付金额
Pending、Paid、Cancelled等订单状态- 座位保留截止时间
- 支付网关的交易号
- 创建时间和支付完成时间
流程示例如下:
public ActionResult StartPayment()
{
PendingOrder order = orderService.CreatePendingOrder(
userId: GetCurrentUserId(),
showId: GetSelectedShowId(),
seats: GetSelectedSeats()
);
// Session 只用于改善用户体验,不作为订单的唯一存储位置
Session["PendingOrderId"] = order.Id;
Session.Timeout = 60;
string paymentUrl = paymentGateway.CreatePaymentUrl(
orderId: order.Id,
amount: order.Amount,
returnUrl: Url.Action(
"PaymentReturn",
"Payment",
null,
Request.Url.Scheme
)
);
return Redirect(paymentUrl);
}
处理支付返回时,应使用支付网关返回的订单号重新查询数据库,不能只读取 Session:
public ActionResult PaymentReturn(string orderId)
{
PendingOrder order = orderService.GetById(orderId);
if (order == null)
{
return HttpNotFound();
}
PaymentResult result = paymentGateway.VerifyPayment(Request);
if (result.IsSuccessful &&
result.OrderId == order.Id &&
result.Amount == order.Amount)
{
orderService.MarkAsPaid(order.Id, result.TransactionId);
}
if (Session != null)
{
Session.Timeout = 20;
}
return View(orderService.GetById(order.Id));
}
签名字段和验签方式取决于具体的支付网关。不能只相信浏览器查询参数中的“支付成功”标志,金额、订单号和交易状态都需要在服务端验证。
还需要处理异步支付通知
用户完成支付后不一定会返回网站,也可能直接关闭支付页面。因此,最好通过支付网关的服务器端回调或 Webhook 确认支付结果:
[HttpPost]
public ActionResult PaymentCallback()
{
PaymentResult result = paymentGateway.VerifyCallback(Request);
if (!result.IsSuccessful)
{
return new HttpStatusCodeResult(400);
}
PendingOrder order = orderService.GetById(result.OrderId);
if (order == null || order.Amount != result.Amount)
{
return new HttpStatusCodeResult(400);
}
// 应实现幂等处理,避免重复通知造成重复扣库存或重复出票
orderService.MarkAsPaid(order.Id, result.TransactionId);
return Content("OK");
}
注意事项
延长 Session.Timeout 只能作为辅助措施。支付和订票涉及关键业务状态,必须持久化,并设置独立的订单或座位保留期限,例如“创建订单后 15 分钟内完成支付”。该期限应由数据库时间和后台任务控制,不能取决于 Session 是否存在。
如果 Session 使用 InProc 模式,应用程序池回收会清空全部 Session。需要提高普通 Session 的可用性时,可以根据部署环境考虑 StateServer、SQL Server Session State 或其他分布式存储。即使采用这些方案,也不能把 Session 当作支付订单的唯一数据源。
备注:内容仅供参考。
