PAYATHON 2026

从 payment gateway 重定向后用户登录状态丢失

支付阿杰

结论

Payment Gateway 使用 HTTPS,不会因为页面从 HTTP 跳转到 HTTPS 后再返回,就主动清除应用站点的 Cookie 或 Session。

更常见的原因包括:

  • 支付网关通过跨站 POST 返回,受浏览器的 SameSite Cookie 策略限制,登录 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 就允许访问订单详情。

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() 或注销逻辑;
  • 返回地址没有进入另一套应用、虚拟目录或身份认证配置。

如果项目使用 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 穿过支付网关,而应采用以下设计:

  1. 全站启用 HTTPS;
  2. 使用数据库中的订单记录关联用户和支付交易;
  3. 验证支付通知签名,并按幂等方式处理通知;
  4. 不依赖登录 Cookie 或 Session 判断付款是否成功;
  5. 分开处理浏览器返回页面和服务端支付通知。

备注:内容仅供参考。