从 payment gateway 重定向后用户登录状态丢失
结论
Payment Gateway 使用 HTTPS,不会因为页面从 HTTP 跳转到 HTTPS 后再返回,就主动清除应用站点的 Cookie 或 Session。
更常见的原因包括:
- 支付网关通过跨站
POST返回,受浏览器的SameSiteCookie 策略限制,登录 Cookie 没有随请求发送; - 返回地址的域名、子域名、端口或协议与发起支付时不一致;
- 支付结果通知由网关服务器发起,并非用户浏览器的回跳请求;
- 应用重启、负载均衡或
machineKey不一致,导致原 Cookie 无法解密; - Session 保存在进程内存中,而回调请求落到了另一台服务器,或者原工作进程已经重启。
支付回调不应依赖当前用户的登录状态或 Session。应用应通过订单号和一次性关联标识恢复支付上下文,并把支付状态持久化到数据库。用户登录状态则应通过 HTTPS 和正确的 Cookie 配置来维护。
为什么返回后 User.Identity 会变成空
ASP.NET MVC 5 通常将身份状态保存在 Forms Authentication Cookie 或 OWIN Authentication Cookie 中。只有服务器收到并成功验证该 Cookie,才能重建 User.Identity。
支付过程一般会在以下站点之间跳转:
shop.example.com
-> gateway.example.com
-> shop.example.com/payment/return
浏览器会把这两个域名视为不同站点。跨站请求中,某些 Cookie 可能不会被发送,尤其是在支付网关通过表单 POST 返回时。
这不一定表示 Cookie 已被删除。Cookie 可能仍保存在浏览器中,只是没有随当前请求发送。服务器因此创建了新的 Session,并将用户视为未登录。
还需要区分两类通知:
- 浏览器返回地址:用户付款后,由浏览器跳回商城。
- 服务端通知地址:支付网关的服务器直接请求商城。
服务端通知不会包含用户浏览器中的登录 Cookie,所以此时 User.Identity 为空是正常的。
推荐的处理流程
1. 发起支付前持久化订单关系
创建支付请求前,将订单、用户和随机生成的一次性标识写入数据库,不要只保存在 Session 中。
public ActionResult StartPayment(int orderId)
{
var userId = User.Identity.Name;
var order = db.Orders.Single(x =>
x.Id == orderId &&
x.UserId == userId);
var returnToken = Guid.NewGuid().ToString("N");
order.PaymentReturnTokenHash = HashToken(returnToken);
order.PaymentStatus = "Pending";
db.SaveChanges();
var callbackUrl = Url.Action(
"Return",
"Payment",
new { orderId = order.Id, token = returnToken },
protocol: Request.Url.Scheme);
var gatewayRequest = BuildGatewayRequest(order, callbackUrl);
return Redirect(gatewayRequest.PaymentUrl);
}
实际项目应保存令牌的哈希值,并为令牌设置有效期和一次性使用限制。订单金额、用户标识和商品信息必须从数据库读取,不能信任回跳参数中的相关数据。
2. 回调时验证支付网关签名
支付结果必须通过网关提供的签名、消息认证码或服务器查询接口进行验证。不能只根据 status=success 之类的参数,就把订单标记为已支付。
[AllowAnonymous]
[HttpPost]
public ActionResult Notify(PaymentNotification notification)
{
if (!paymentGateway.VerifySignature(Request, notification))
{
return new HttpStatusCodeResult(400);
}
var order = db.Orders.SingleOrDefault(
x => x.GatewayOrderNumber == notification.OrderNumber);
if (order == null)
{
return HttpNotFound();
}
if (notification.Amount != order.Amount ||
notification.Currency != order.Currency)
{
return new HttpStatusCodeResult(400);
}
if (paymentGateway.IsSuccessful(notification))
{
// 应设计为幂等操作,防止网关重复通知。
MarkOrderAsPaid(order, notification.TransactionId);
db.SaveChanges();
}
return Content("OK");
}
Notify 是服务端通知示例,不应要求用户处于登录状态。具体字段、验签方式和响应内容必须遵循所用支付网关的协议。
3. 浏览器返回页面只负责展示结果
浏览器回跳后,通过订单号和一次性标识找到订单,再从数据库读取最终支付状态。
[AllowAnonymous]
[HttpGet]
public ActionResult Return(int orderId, string token)
{
var order = db.Orders.SingleOrDefault(x => x.Id == orderId);
if (order == null ||
!FixedTimeEquals(order.PaymentReturnTokenHash, HashToken(token)))
{
return new HttpStatusCodeResult(400);
}
// 以数据库记录或网关查询结果为准,不相信 URL 中的支付状态。
return View("PaymentResult", order);
}
如果用户的认证 Cookie 仍然有效,可以继续显示其登录状态。即使用户没有登录,也可以安全地展示信息受限的支付结果。不要只凭 orderId 就允许访问订单详情。
检查 Cookie 配置
Forms Authentication
MVC 5 使用 Forms Authentication 时,可以检查 web.config:
<system.web>
<authentication mode="Forms">
<forms
name=".ASPXAUTH"
loginUrl="~/Account/Login"
timeout="60"
path="/"
requireSSL="false"
slidingExpiration="true" />
</authentication>
<sessionState
cookieless="UseCookies"
timeout="20" />
</system.web>
requireSSL="false" 仅表示允许通过 HTTP 发送认证 Cookie,并不表示这种做法是安全的。
还应检查以下配置和行为:
- 发起支付和返回时使用同一个主机名,例如不要混用
example.com和www.example.com; - Cookie 的
Path通常应设为/; - 回调 Action 中没有调用
Session.Clear()、Session.Abandon()或注销逻辑; - 返回地址没有进入另一套应用、虚拟目录或身份认证配置。
OWIN Cookie Authentication
如果项目使用 ASP.NET Identity/OWIN,可以检查类似配置:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
CookieName = ".AspNet.ApplicationCookie",
CookiePath = "/",
CookieSecure = CookieSecureOption.SameAsRequest
});
不同版本的 Katana 对 SameSite 的支持和具体行为有所差异,需要结合项目实际使用的 Microsoft.Owin 包版本确定配置。
SameSite 是最常见的问题
如果支付网关通过跨站 POST 返回,设置为 SameSite=Lax 的 Cookie,或者被浏览器默认按 Lax 处理的 Cookie,通常不会随该 POST 一起发送。条件允许时,应优先让浏览器通过顶层 GET 跳回商城,再通过单独的服务端通知确认支付结果。
如果跨站 POST 必须携带 Cookie,一般需要:
SameSite=None; Secure
现代浏览器要求 SameSite=None 与 Secure 同时使用,而 Secure Cookie 只能通过 HTTPS 发送。因此,如果完全不启用 HTTPS,就无法可靠地让现代浏览器在跨站支付 POST 中携带认证 Cookie。
不要通过关闭浏览器安全策略或削弱 Cookie 保护来解决这个问题。
检查 Session 和部署环境
如果认证 Cookie 已经发送,但 Session 仍发生变化,就需要继续检查服务器环境。
使用默认的 InProc Session 时,以下情况都会导致 Session 丢失:
- IIS 应用程序池回收;
- 应用重新启动;
web.config或程序集发生变化;- 多服务器部署中,请求被转发到另一台服务器。
多实例环境应考虑使用 SQL Server Session State、State Server 或其他共享状态存储。各实例还必须使用一致且固定的 machineKey,否则一台服务器生成的认证 Cookie 可能无法通过另一台服务器的验证。
<system.web>
<machineKey
validationKey="固定且安全生成的密钥"
decryptionKey="固定且安全生成的密钥"
validation="HMACSHA256"
decryption="AES" />
</system.web>
不要直接使用示例密钥,也不要将生产密钥提交到公共代码仓库。
是否可以坚持使用 HTTP
从技术上说,同一域名下的非 Secure Cookie 可以通过 HTTP 发送,但这无法可靠解决跨站 POST、服务端通知、域名变化以及浏览器 SameSite 策略带来的问题。
HTTP 下的登录 Cookie、订单信息和页面内容还可能被窃听或篡改。即使银行卡信息由第三方支付网关处理,电子商务网站仍应全站使用 HTTPS,并启用安全 Cookie:
Secure; HttpOnly; SameSite=Lax
如果支付流程确实要求跨站 POST 携带认证 Cookie,则需要根据实际流程评估以下设置:
Secure; HttpOnly; SameSite=None
最终方案不应试图让 Session 穿过支付网关,而应采用以下设计:
- 全站启用 HTTPS;
- 使用数据库中的订单记录关联用户和支付交易;
- 验证支付通知签名,并按幂等方式处理通知;
- 不依赖登录 Cookie 或 Session 判断付款是否成功;
- 分开处理浏览器返回页面和服务端支付通知。
备注:内容仅供参考。
