首次访问产品时如何通知调用方需要完成额外设置?
结论
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 返回结构化错误,说明用户需要先完成产品设置。错误类型和设置地址都属于应用层协议,需要服务端与调用方共同约定。
推荐的浏览器处理流程
流程可以这样设计:
- 用户访问 product-xyz 中的目标页面。
- product-xyz 检查用户是否已经登录。
- 如果没有登录,则进入现有的 SSO authentication flow。
- 登录完成后,检查当前客户是否已完成 product-xyz 的初始化。
- 如果尚未完成,保存原始目标地址,并重定向到设置页面。
- 用户提交 correspondence address 等必要信息。
- 服务端创建 data tenancy,并在事务中将初始化状态标记为完成。
- 将用户重定向回原始目标页面。
- 目标页面再次检查配置状态,检查通过后正常返回内容。
例如:
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 状态。
备注:内容仅供参考。
