PAYATHON 2026

首次访问产品时如何通知调用方需要完成额外设置?

支付老李

结论

HTTP 没有通用的 4xx Setup Required 状态码,也没有类似 WWW-SetupInfo 的标准响应头。401 Unauthorized 和 WWW-Authenticate 只用于身份验证挑战,不适合产品的首次配置流程。402 Payment Required 同样不合适,因为这个场景与支付无关。

对于浏览器页面,可以采用下面的处理方式:用户通过 SSO 完成身份验证后,由 product-xyz 检查该客户的产品配置状态。如果尚未配置,则返回 302 Found 或 303 See Other,把用户重定向到设置页面。设置完成后,再安全地跳回用户最初访问的页面。

对于非浏览器 API,可以使用 application/problem+json 返回结构化错误,说明用户需要先完成产品设置。错误类型和设置地址都属于应用层协议,需要服务端与调用方共同约定。

推荐的浏览器处理流程

流程可以这样设计:

  1. 用户访问 product-xyz 中的目标页面。
  2. product-xyz 检查用户是否已经登录。
  3. 如果没有登录,则进入现有的 SSO authentication flow。
  4. 登录完成后,检查当前客户是否已完成 product-xyz 的初始化。
  5. 如果尚未完成,保存原始目标地址,并重定向到设置页面。
  6. 用户提交 correspondence address 等必要信息。
  7. 服务端创建 data tenancy,并在事务中将初始化状态标记为完成。
  8. 将用户重定向回原始目标页面。
  9. 目标页面再次检查配置状态,检查通过后正常返回内容。

例如:

GET /reports/123 HTTP/1.1
Host: product-xyz.ourdomain.com
Cookie: session=...

尚未配置时可以返回:

HTTP/1.1 303 See Other
Location: /setup?return_token=eyJhbGciOi...
Cache-Control: no-store

设置完成后返回:

HTTP/1.1 303 See Other
Location: /reports/123
Cache-Control: no-store

这里更适合使用 303 See Other,因为它明确要求客户端通过 GET 请求访问 Location 指定的页面。普通页面导航也经常使用 302 Found,但它的语义没有 303 清楚。

不要直接信任 return URL

设置完成后通常要让用户回到原先访问的页面,但服务端不能直接接受任意外部 URL:

/setup?return_url=https://attacker.example/

如果服务端不做验证就重定向到该地址,会产生 open redirect 漏洞。

一种做法是只允许站内相对路径:

function safeReturnPath(value) {
  if (typeof value !== "string") {
    return "/";
  }

  if (!value.startsWith("/") || value.startsWith("//")) {
    return "/";
  }

  return value;
}

更稳妥的办法是生成一个短期有效且带签名的 return_token,其中记录:

  • 原始站内路径
  • 用户或客户标识
  • 产品标识
  • 过期时间
  • 随机数或流程 ID

设置完成后,服务端先验证 token,再执行重定向。这样既能防止目标地址被篡改,也不会把内部状态直接暴露在查询参数中。

服务端实现示例

下面是一个简化的 Express 风格示例:

app.get("/reports/:id", requireAuthentication, async (req, res, next) => {
  const tenant = await tenantService.findByCustomerId(req.user.customerId);

  if (!tenant || tenant.status !== "ACTIVE") {
    const returnToken = tokenService.sign({
      customerId: req.user.customerId,
      path: req.originalUrl,
      expiresAt: Date.now() + 10 * 60 * 1000
    });

    res.set("Cache-Control", "no-store");
    return res.redirect(303, `/setup?return_token=${encodeURIComponent(returnToken)}`);
  }

  return next();
});

提交设置数据时,服务端需要重新验证所有条件:

app.post("/setup", requireAuthentication, async (req, res) => {
  const returnToken = tokenService.verify(req.body.return_token);

  if (returnToken.customerId !== req.user.customerId) {
    return res.status(400).send("Invalid return token");
  }

  await tenantService.initializeOnce({
    customerId: req.user.customerId,
    correspondenceAddress: req.body.correspondenceAddress
  });

  res.set("Cache-Control", "no-store");
  return res.redirect(303, returnToken.path);
});

initializeOnce 必须具备幂等性。即使用户重复提交、刷新页面或同时打开多个标签页,也不能创建多个 tenancy。通常可以使用数据库唯一约束和事务来保证这一点。

API 调用方如何处理

如果调用方不是浏览器,而是程序化 API client,直接重定向到 HTML 设置页面通常不合适。许多 HTTP client 会自动跟随重定向,最后得到一段无法处理的 HTML。

此时可以返回结构化错误。RFC 9457 定义了 application/problem+json 格式,并允许应用添加自定义字段。例如:

HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Cache-Control: no-store
{
  "type": "https://product-xyz.ourdomain.com/problems/setup-required",
  "title": "Product setup required",
  "status": 403,
  "detail": "This customer must complete product setup before accessing this resource.",
  "setup_url": "https://product-xyz.ourdomain.com/setup",
  "instance": "/reports/123"
}

这里的 type 和 setup_url 并不是 HTTP 标准定义的”首次设置协议”,而是应用自己的 API contract。调用方需要明确实现对此错误类型的处理。

可以根据接口语义选择状态码:

  • 403 Forbidden:调用方已经通过身份验证,但当前还不满足访问该资源的条件。
  • 409 Conflict:请求与客户当前的产品初始化状态冲突,完成设置后可以重试。

这两个状态码都不是专门为”首次设置”定义的。与其寻找一个看起来最接近的状态码,不如统一状态码的使用方式,并通过 application/problem+json 提供机器可读的错误类型。

如果同一个地址同时服务浏览器和 API,最好使用独立路由或明确的内容协商来区分处理方式,不要只依赖并不可靠的 User-Agent 判断。

为什么其他状态码不合适

401 Unauthorized

401 表示请求缺少有效的 authentication credentials,并要求响应包含适用的 WWW-Authenticate challenge。如果用户已经通过 SSO 登录,只是尚未完成产品配置,就不应返回 401,否则客户端可能错误地重新发起登录。

402 Payment Required

402 不能表示一般性的”还缺少某些条件”。即使它目前没有广泛统一的处理语义,把它用于非支付场景仍会误导客户端、网关和后续维护人员。

428 Precondition Required

428 主要用于要求客户端提交条件请求,例如使用 If-Match 防止 lost update。它描述的是 HTTP 请求需要满足的条件,而不是客户账户或产品租户尚未初始化,因此不适用于这个场景。

自定义 4xx 状态码

不建议创建 4xx Setup Required。浏览器、代理、SDK、监控系统和 API gateway 都无法识别这个状态码,最终仍要依靠自定义协议解释它。标准状态码配合结构化响应体具有更好的兼容性。

自定义响应头

在受控系统中,可以使用类似 Setup-Required 的私有响应头,但不能把它当成行业标准。主要业务信息更适合放在响应体中,因为响应体更便于记录和扩展,也更容易生成 API schema。响应头适合承载简短的响应元数据。

需要特别注意的边界情况

不要把普通 POST 请求直接重定向到设置页面,也不要在设置完成后自动重放原请求。原始操作可能涉及付款、创建记录或发送通知,自动重放很容易产生重复副作用。

对于有副作用的操作,可以采用以下方式:

  • 在用户执行操作前检查初始化状态。
  • 将待执行操作保存为短期 workflow,设置完成后再由用户确认是否继续。
  • 为原始操作提供 idempotency key。
  • 不要在 URL 中保存敏感表单数据。

初始化检查必须由服务端执行,不能只看前端页面是否展示了设置向导。所有依赖 tenancy 的受保护接口都要验证 tenancy 状态,否则用户可能绕过页面,直接调用接口。

这两种状态需要分别处理:用户未登录时使用标准的 authentication flow;用户已经登录但尚未初始化时,浏览器通过 303 重定向到设置页面,API 则使用标准 4xx 状态码,并通过 application/problem+json 描述应用层的 setup-required 状态。

备注:内容仅供参考。