> For the complete documentation index, see [llms.txt](https://docs.goassay.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.goassay.ai/developers/zh/shang-jia-ni-de-api-jin-jie/run-in-production.md).

# 在生产环境运行验证

在多副本、密钥变更和代理环境下保持签名验证可靠。

先确保[代码内验证器](/developers/zh/shang-jia-ni-de-api-ru-men/verify-in-your-code.md)或 [Worker](/developers/zh/shang-jia-ni-de-api-ru-men/cloudflare-worker.md)正常工作。添加副本前，让每个实例使用共享重放存储，并检查代理是否保留 Assay 签名的请求。

## 共享重放防护

重放存储会记住签名请求的随机数，即一次性标识符。检查随机数和插入记录必须是同一个原子操作。先读取再写入可能让并发请求都通过。

保护同一端点的所有实例应使用同一存储和键前缀。记录应保留至签名到期时间加上时钟容差。为存储配置足够容量，确保整段时间都能保留记录；记录被提前淘汰，或重启丢失记录，都可能让重放通过。

每个库都提供 Redis 重放存储：它使用 `SET NX EX`，将存活时间向上取整到秒，并向外传递 Redis 错误，因此存储故障会导致请求被拒绝。以下示例为[在代码中验证](/developers/zh/shang-jia-ni-de-api-ru-men/verify-in-your-code.md)中的服务器添加共享存储。请在部署的机密配置中设置 `REDIS_URL`。在生产环境中，请锁定你测试过的软件包版本。

{% tabs %}
{% tab title="Node" %}
需要 Node 20 或更新版本。

```sh
npm install @goassay-ai/provider-verify express redis
```

该适配器适用于 node-redis v4 或更新版本，以及 ioredis。保存为 `server.mjs`，然后运行 `node server.mjs`：

```js
import express from "express";
import { createClient } from "redis";
import { assayVerify } from "@goassay-ai/provider-verify/express";
import { redisReplayStore } from "@goassay-ai/provider-verify/redis";

if (!process.env.REDIS_URL) throw new Error("REDIS_URL is required");
const redis = createClient({ url: process.env.REDIS_URL });
redis.on("error", () => console.error("replay_store_connection_error"));
await redis.connect();

const app = express();
app.use(assayVerify({
  authority: "api.example.com", // Replace with the public host Assay calls.
  replayStore: redisReplayStore(redis, "assay:nonce:"),
}));
app.post("/v1/search", (req, res) => res.json({ ok: true }));
app.use((err, req, res, next) => {
  console.error("verification_or_handler_error");
  res.status(503).json({ error: "service_unavailable" });
});
app.listen(8080, "127.0.0.1");
```

{% endtab %}

{% tab title="Python" %}
需要 Python 3.9 或更新版本。在虚拟环境中运行：

```sh
python -m pip install "assay-provider-verify[flask,redis]"
```

`redis_replay_store` 适用于 redis-py 客户端。保存为 `server.py`，然后运行 `python server.py`：

```python
import os
import redis
from flask import Flask, jsonify
from assay_provider_verify import AssayVerifyWSGI, redis_replay_store

client = redis.Redis.from_url(os.environ["REDIS_URL"], socket_timeout=2)

app = Flask(__name__)
app.wsgi_app = AssayVerifyWSGI(
    app.wsgi_app,
    authority="api.example.com",  # Replace with the public host Assay calls.
    replay_store=redis_replay_store(client, "assay:nonce:"),
)

@app.post("/v1/search")
def search():
    return jsonify(ok=True)

if __name__ == "__main__":
    app.run("127.0.0.1", 8080)
```

{% endtab %}

{% tab title="Go" %}
需要 Go 1.21 或更新版本。

```sh
go get github.com/assay-ai/assay-provider-verify/go github.com/redis/go-redis/v9
```

Go 接受任何带 `CheckAndStore(ctx, nonce, ttl)` 的 `ReplayStore`。下面的实现封装了 go-redis 的 `SetNX`。保存为 `main.go`，然后运行 `go run .`：

```go
package main

import (
    "context"
    "io"
    "log"
    "net/http"
    "os"
    "time"

    assayverify "github.com/assay-ai/assay-provider-verify/go"
    "github.com/redis/go-redis/v9"
)

type redisStore struct {
    client *redis.Client
    prefix string
}

// CheckAndStore returns Redis errors, so verification fails closed.
func (s redisStore) CheckAndStore(ctx context.Context, nonce string, ttl time.Duration) (bool, error) {
    if ttl < time.Second {
        ttl = time.Second
    }
    return s.client.SetNX(ctx, s.prefix+nonce, "1", ttl).Result()
}

func main() {
    opt, err := redis.ParseURL(os.Getenv("REDIS_URL"))
    if err != nil {
        log.Fatal(err)
    }
    verify, err := assayverify.Middleware(assayverify.Options{
        Authority:   "api.example.com", // Replace with the public host Assay calls.
        ReplayStore: redisStore{client: redis.NewClient(opt), prefix: "assay:nonce:"},
    })
    if err != nil {
        log.Fatal(err)
    }
    mux := http.NewServeMux()
    mux.HandleFunc("/v1/search", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPost {
            w.WriteHeader(http.StatusMethodNotAllowed)
            return
        }
        w.Header().Set("Content-Type", "application/json")
        _, _ = io.WriteString(w, `{"ok":true}`)
    })
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", verify(mux)))
}
```

{% endtab %}
{% endtabs %}

将处理函数替换为你的工具，并部署在 HTTPS 后方。为 Redis 客户端和基础设施设置连接及命令超时，避免存储故障让请求一直等待。存储失败时应继续拒绝请求。

Cloudflare Worker 可以改用其 `NONCE_STORE` Durable Object；参阅[用 Cloudflare Worker 验证](/developers/zh/shang-jia-ni-de-api-ru-men/cloudflare-worker.md)。

## 保持密钥轮换正常

使用默认公钥集（JWKS）：`https://api.goassay.ai/.well-known/assay-request-keys.json`。允许到该主机的出站 HTTPS。按签名中的 `keyid` 选择密钥；不要在应用代码中固定当前的单个密钥。

| 密钥获取设置       | 库的行为                    |
| ------------ | ----------------------- |
| 缓存有效期        | 一小时                     |
| 未知密钥         | 距上次成功加载至少 60 秒后，可触发重新获取 |
| 获取超时         | 5 秒                     |
| 获取失败         | 暂停 5 秒再重试；现有缓存密钥仍可使用    |
| 静态 `jwks` 选项 | 禁用自动获取；部署方必须提供密钥更新      |

复用验证器实例及其缓存。重新获取间隔内，不熟悉的密钥可能持续被视为未知。请对持续的 `UNKNOWN_KEY` 失败设置告警，并恢复密钥获取。不要在密钥变更或网络故障时绕过验证。

## 保留公开请求

| 边界           | 生产环境规则                                                             |
| ------------ | ------------------------------------------------------------------ |
| 代理或负载均衡器     | 验证前保留方法、原始路径、查询参数、调用 ID、签名请求头和请求体字节。                               |
| 重写 `Host`    | 配置已知的公开 `authority`（Go 中为 `Authority`），包括非默认端口。不要从任意客户端提供的转发请求头读取。 |
| JSON 解析器或解压缩 | 修改签名请求体前先验证。                                                       |
| 重定向          | 在 Assay 注册最终 HTTPS URL，让签名请求直接到达目标。                                |
| 网关后的源站       | 限制直接访问，避免请求绕过验证器。                                                  |
| 代理重试         | 不要重发已接受的签名。其随机数已被使用。                                               |

## 时钟和超时

保持服务器时钟同步。库允许 60 秒时钟容差，并拒绝超过 300 秒的签名窗口。增大容差也会延长重放记录所需保留时间。不要在生产环境保留固定夹具时钟。

获取密钥、访问重放存储和运行处理函数都会消耗调用方的时间预算。映射测试器使用上架条目的超时，最多 30 秒。请根据实测端到端延迟设置处理函数和基础设施超时。验证器适配器会缓冲请求体，默认上限为 5 MiB；请求体上限不等于读取超时。

## 日志：保留什么

结构化记录接收时间、验证结果或原因、延迟和服务实例。对已接受调用，保留已验证的调用 ID 和密钥 ID，以便与 Assay 关联日志。拒绝请求中的 ID 应视为不可信，记录前先做净化处理。

避免在常规日志中记录 API 密钥、授权请求头、完整签名请求头、随机数、请求体或包含用户数据的查询字符串。如果为调试保留载荷证据，请限制访问并自行规定保留期限。这些是运维建议；验证器不会代你安装日志或保留策略。

## 确认是否成功

在受控集成测试中，将同一有效签名请求发送到不同副本：应只有第一次通过。测试共享存储故障，确认处理函数完全不运行。通过实际代理检查有效调用，并确认无签名调用不能到达处理函数或绕过网关。

## 错误

| 现象                        | 处理方式                         |
| ------------------------- | ---------------------------- |
| 同一签名调用在不同副本上通过            | 检查是否使用同一存储和前缀，且记录未提前淘汰。      |
| 大量调用出现 `EXPIRED`          | 检查时钟同步和队列延迟。                 |
| 只有经过代理时出现 `BAD_SIGNATURE` | 比较代理前后的公开 authority 和原始请求目标。 |
| 存储失败或请求缓慢                 | 恢复存储并执行客户端超时限制。故障时继续拒绝请求。    |

## 延伸阅读

* [拒绝原因](/developers/zh/shang-jia-ni-de-api-ru-men/reject-reasons.md)。
* 上架你的 API（可选）：[上架目录中的工具](https://docs.goassay.ai/list-your-api/zh/list-a-catalogue-tool)。

## 下一步

* [自行实现验证](/developers/zh/shang-jia-ni-de-api-jin-jie/write-your-own-verifier.md)。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.goassay.ai/developers/zh/shang-jia-ni-de-api-jin-jie/run-in-production.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
