PAYATHON 2026

如何在return_url中回传加密的登录信息?

支付小周

结论

不要通过 return_url 直接传递用户名、密码、Cookie、Access Token 等登录凭证。即使使用 Base64、AES 或 RSA 处理,也不建议这样做。

建议采用以下方式:

  1. 用户发起支付时,由服务端生成一个随机、短期有效且只能使用一次的 state。
  2. 将 state 与用户、订单、原登录会话关联,并保存在 Redis 或数据库中。
  3. return_url 只携带不含业务信息的 state。
  4. 支付完成并跳回商户页面后,服务端先验签、校验订单,再通过 state 恢复登录上下文。
  5. 处理完成后,重定向到不带查询参数的业务页面。

如果支付前后的页面属于同一站点,通常不必传递 state,继续使用现有的 HttpOnly Session Cookie 即可。

为什么不应在 URL 中传递登录信息

浏览器和链路中的其他系统都能看到 return_url 的查询参数。这些参数可能出现在:

  • 浏览器地址栏和历史记录
  • Web 服务器、CDN、网关及监控日志
  • 用户复制或分享的链接
  • 页面跳转时的 Referer
  • 截图、录屏或第三方统计脚本中

加密可以降低内容被直接读取的风险,却无法阻止密文被复制和重放。Base64、URL 编码和 Hex 编码则根本不属于加密。

支付宝返回参数中的 sign 仅用于验证支付宝返回的数据是否遭到篡改,不会保护商户自行附加的登录信息。

推荐方案:一次性 state

1. 创建支付订单时生成随机值

下面以 Node.js 和 Redis 为例,其他语言也可以采用相同思路:

import crypto from "node:crypto";

const state = crypto.randomBytes(32).toString("base64url");

await redis.set(
  `payment:return:${state}`,
  JSON.stringify({
    userId: currentUser.id,
    outTradeNo,
    redirectPath: "/payment/result"
  }),
  {
    EX: 600,
    NX: true
  }
);

生成的 state 应满足以下条件:

  • 至少包含 128 位随机性
  • 不携带用户信息或登录凭证
  • 有效期较短,例如 5~10 分钟
  • 使用后立即删除,防止重放

2. 将 state 放入回跳地址

const returnUrl =
  `https://www.example.com/alipay/return?state=${encodeURIComponent(state)}`;

能否在 return_url 中附加自定义查询参数,要以当前使用的支付宝产品接口和 SDK 行为为准。如果接入方式不能可靠保留自定义参数,可以在订单记录中保存用户与 out_trade_no 的关联,回跳后再通过商户订单号查询。

不要将敏感信息拼接到 URL 中:

# 不推荐
https://www.example.com/alipay/return?username=alice&token=...

# 同样不推荐
https://www.example.com/alipay/return?loginInfo=<encrypted-data>

3. 回跳后先验证支付宝签名

验签应使用支付宝 SDK,并传入支付宝返回的原始参数集合。商户自己的 state 不应交给支付宝验签逻辑处理。

app.get("/alipay/return", async (req, res) => {
  const { state, ...alipayParams } = req.query;

  const signatureValid = verifyAlipaySignature(alipayParams);

  if (!signatureValid) {
    return res.status(400).send("签名验证失败");
  }

  // 后续继续校验订单和 state
});

verifyAlipaySignature 只是示意名称。实际项目应调用对应语言的支付宝 SDK 验签方法,不要自行实现 RSA2 验签和参数排序。

4. 校验订单并原子消费 state

app.get("/alipay/return", async (req, res) => {
  const { state, ...alipayParams } = req.query;

  if (!verifyAlipaySignature(alipayParams)) {
    return res.status(400).send("签名验证失败");
  }

  const key = `payment:return:${state}`;

  // 生产环境应使用 GETDEL 或 Lua 脚本保证读取和删除的原子性
  const contextText = await redis.getDel(key);

  if (!contextText) {
    return res.status(400).send("回跳凭证无效、已使用或已过期");
  }

  const context = JSON.parse(contextText);

  if (context.outTradeNo !== alipayParams.out_trade_no) {
    return res.status(400).send("订单号不匹配");
  }

  const order = await findOrder(context.outTradeNo);

  if (
    !order ||
    order.userId !== context.userId ||
    order.amount !== alipayParams.total_amount ||
    alipayParams.app_id !== process.env.ALIPAY_APP_ID ||
    alipayParams.seller_id !== process.env.ALIPAY_SELLER_ID
  ) {
    return res.status(400).send("订单信息校验失败");
  }

  // 不要把 return_url 当作支付成功的唯一依据
  await confirmPaymentFromTrustedResult(order);

  await establishLoginSession(res, context.userId);

  // 清除地址栏中的支付参数和 state
  return res.redirect(303, context.redirectPath);
});

如果当前 Redis 版本不支持 GETDEL,可以使用 Lua 脚本原子地读取并删除数据,避免同一个 state 被并发使用多次。

支付结果不能只看 return_url

return_url 是浏览器的同步跳转地址。用户可能关闭页面、阻止跳转,也可能重复访问该地址,因此不能只凭浏览器回跳就将订单标记为支付成功。

服务端还需要:

  1. 验证支付宝返回签名。
  2. 校验 app_id、seller_id、out_trade_no 和 total_amount。
  3. 以 notify_url 异步通知或支付宝服务端订单查询结果作为支付状态依据。
  4. 保证订单更新具有幂等性,避免重复入账或发货。

return_url 更适合用来展示支付结果,不应承担最终记账职责。

已有登录会话时的处理

如果支付前后的页面都在同一个 HTTPS 域名下,浏览器通常会自动携带站点的 Session Cookie。此时只需正确配置 Cookie:

Set-Cookie: session=<opaque-session-id>; Path=/; HttpOnly; Secure; SameSite=Lax

在跨站点顶级导航回到商户页面时,SameSite=Lax 通常允许浏览器携带 Cookie。不过,具体表现仍要结合支付跳转方式、浏览器策略和域名结构进行测试。

Cookie 中只保存随机的会话 ID,真实登录状态放在服务端 Session 中。不要将用户密码、Access Token 或完整用户资料写入 Cookie。

如果确实必须传递自包含密文

只有无法使用服务端状态存储时,才考虑采用经过认证的加密方案,例如 AES-256-GCM。密文至少应包含:

  • userId
  • 关联的 outTradeNo
  • 过期时间 exp
  • 唯一随机标识 jti

解密后仍要校验订单绑定关系和有效期,并记录 jti 的使用情况,防止重放。密钥只能保存在服务端,不能写入前端代码或 URL。

即使使用 AES-GCM,也不要封装密码、Session Cookie 或长期 Access Token。URL 中的密文仍可能泄漏并被重放,因此一次性随机 state 更稳妥。

注意事项

  • 全程使用 HTTPS。
  • 不要将完整回跳 URL、sign、state 或令牌写入日志。
  • redirectPath 应使用服务端白名单,避免开放重定向漏洞。
  • 不要接受客户端传入的任意回跳域名。
  • state 必须由密码学安全随机数生成器生成,不能使用时间戳或自增 ID。
  • 登录恢复和支付确认需要分别校验,不能因为支付参数验签通过就直接信任用户身份。
  • 处理完成后,立即通过 303 重定向到干净地址,清除地址栏中的敏感查询参数。

备注:内容仅供参考。