PAYATHON 2026

如何为 Parse.com 构造支付成功后的 callback URL?

支付阿杰

结论

不要把 Parse 的 App ID、REST API Key 拼进支付回调 URL,Master Key 更不能这样处理。

如果支付服务商无法添加 Parse REST API 要求的请求头,就需要增加一个公开的服务端回调端点。这个端点负责接收支付通知、验证支付平台签名,再通过服务端 Parse SDK 或受保护的 REST 请求更新订单。

配置前还要分清支付平台提供的是哪一种 URL:

  • Success URL:支付完成后,用户浏览器会跳转到这个地址。它只适合展示支付结果。
  • Webhook / Notify URL:支付平台的服务器会主动向这个地址发送支付结果,适合用来修改订单状态。

如果平台只提供 Success URL,不能仅凭 URL 中的 orderId 或 status=success 判断付款成功,因为用户可以伪造这些参数。

推荐流程

完整流程如下:

  1. 创建订单时,生成不可预测的订单编号,并在 Parse 中将订单保存为“待支付”。
  2. 将一个公开的 HTTPS 地址配置为支付平台的 Webhook 或 Notify URL。
  3. 由回调端点读取支付平台发送的参数。
  4. 按照支付平台规定的方法验证签名。必要时,再调用平台的订单查询 API 核对交易。
  5. 检查金额、币种、商户号和订单编号。
  6. 通过 Parse Server SDK 或 Master Key 在服务端更新订单。
  7. 返回支付平台要求的确认响应,避免平台重复通知。
  8. Success URL 只用于向用户展示支付结果。页面应从后端重新查询订单状态。

回调 URL 的形式

回调 URL 通常只需要包含端点地址:

https://api.example.com/webhooks/payment-provider

如果需要区分环境,可以通过不敏感的路径来区分:

https://api.example.com/webhooks/payment-provider/production

不要把 URL 构造成这样:

https://parse.example.com/classes/Order/123
    ?appId=YOUR_APP_ID
    &restApiKey=YOUR_REST_API_KEY

即使某个接口支持从查询参数读取凭据,也不应该在支付回调中这样使用。URL 可能出现在支付平台记录、代理日志、访问日志、浏览器历史和监控系统里,比请求头中的内容更容易泄露。

Node.js 回调示例

下面的代码展示了推荐的结构。签名算法和字段名称必须按照支付平台的文档替换,不能直接套用示例中的 HMAC 计算方式。

import express from "express";
import crypto from "node:crypto";
import Parse from "parse/node.js";

const app = express();

// 必须根据支付平台实际发送的 Content-Type 配置。
// 有些平台验签要求使用未经解析的原始请求体。
app.use(
  "/webhooks/payment-provider",
  express.raw({ type: "application/json" })
);

Parse.initialize(process.env.PARSE_APP_ID);
Parse.serverURL = process.env.PARSE_SERVER_URL;
Parse.masterKey = process.env.PARSE_MASTER_KEY;

function verifySignature(rawBody, signature) {
  const expected = crypto
    .createHmac("sha256", process.env.PAYMENT_WEBHOOK_SECRET)
    .update(rawBody)
    .digest("hex");

  if (
    typeof signature !== "string" ||
    signature.length !== expected.length
  ) {
    return false;
  }

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

app.post("/webhooks/payment-provider", async (req, res) => {
  try {
    const signature = req.get("X-Payment-Signature");

    if (!verifySignature(req.body, signature)) {
      return res.status(401).send("invalid signature");
    }

    const event = JSON.parse(req.body.toString("utf8"));

    if (event.type !== "payment.succeeded") {
      return res.status(200).send("ignored");
    }

    const orderId = event.data.orderId;
    const transactionId = event.data.transactionId;
    const paidAmount = event.data.amount;
    const currency = event.data.currency;

    const query = new Parse.Query("Order");
    const order = await query.get(orderId, { useMasterKey: true });

    // 必须根据本地订单核对金额和币种,不能只相信回调中的状态。
    if (
      order.get("amount") !== paidAmount ||
      order.get("currency") !== currency
    ) {
      return res.status(400).send("payment data mismatch");
    }

    // 支付平台可能重复发送同一个事件,因此处理必须具备幂等性。
    if (order.get("transactionId") === transactionId) {
      return res.status(200).send("ok");
    }

    if (order.get("status") !== "pending") {
      return res.status(409).send("invalid order state");
    }

    order.set("status", "paid");
    order.set("transactionId", transactionId);
    order.set("paidAt", new Date(event.data.paidAt));

    await order.save(null, { useMasterKey: true });

    return res.status(200).send("ok");
  } catch (error) {
    console.error("Payment callback failed:", error);
    return res.status(500).send("temporary failure");
  }
});

app.listen(3000);

如果支付平台发送的是 application/x-www-form-urlencoded,或者签名覆盖原始查询字符串,就要相应调整请求体解析和验签方式。具体实现必须严格遵循该平台的签名文档。

为什么不能直接调用 Parse REST API

Parse REST API 通常要求通过请求头传递应用标识和密钥,例如:

X-Parse-Application-Id: YOUR_APP_ID
X-Parse-REST-API-Key: YOUR_REST_API_KEY

只支持配置固定 URL 的支付服务无法生成这些请求头,也未必能按照 Parse 要求的格式发送 POST 或 PUT 请求。因此,把支付平台的 Success URL 直接指向 Parse REST API,并不是可靠的集成方案。

App ID 或某些客户端 Key 即使不属于最高级别的秘密,也不能用来证明请求来自支付平台。判断请求是否可信,应依据支付平台的 webhook 签名、共享密钥、证书验证,或者服务器之间的交易查询结果。

Master Key 可以绕过 Class-Level Permissions 和 ACL,必须只保存在可信的服务端,绝不能写进 URL、客户端代码或公开仓库。

如果使用的是旧 Parse.com

托管版 Parse.com 已经停止服务。旧答案中涉及 parse.com、parseapp.com 或旧 Cloud Code 路由的做法,不一定适用于现在的 Parse Server。

如果项目使用的是迁移后的 Parse Server,可以采用以下方式:

  • 在现有 Node.js/Express 应用中增加 webhook 路由;
  • 单独部署一个轻量的 serverless function;
  • 使用支付平台官方支持的 webhook 接收服务,再从服务端调用 Parse;
  • 如果当前 Parse Server 部署支持自定义 Cloud Code HTTP 路由,也可以在那里接收回调。具体写法取决于部署方式和版本。

无论采用哪种方式,公开端点都只负责接收并验证支付通知。Parse 凭据和订单更新操作必须留在服务端。

需要特别注意

  • 不要把 Success URL 当作可信的支付通知。
  • 不要根据 URL 中的 success=true 直接发货或开通服务。
  • 回调地址必须使用 HTTPS。
  • 验证签名和时间戳。如果平台支持,还要阻止重放攻击。
  • 核对订单号、金额、币种和商户身份。
  • 按交易 ID 或事件 ID 进行幂等处理。
  • 保存回调所需的审计记录,但不要记录完整密钥或敏感支付数据。
  • 按照支付平台的要求返回正确的状态码或响应文本,否则平台可能持续重试。
  • 浏览器返回页面应向服务端查询订单状态,因为 webhook 和浏览器跳转的到达顺序并不固定。

备注:内容仅供参考。