> 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-ru-men/verify-in-your-code.md).

# 在代码中验证（Node、Python、Go）

使用 Node、Python 或 Go 库，在服务内部添加签名验证。

如果能修改服务，可以用它替代 [Cloudflare Worker](/developers/zh/shang-jia-ni-de-api-ru-men/cloudflare-worker.md)。中间件会在处理函数运行前验证原始请求，然后让应用可以读取请求体。

## 工作原理

库会从 `https://api.goassay.ai/.well-known/assay-request-keys.json` 获取 Assay 公钥，检查签名、请求体摘要、时间窗口和随机数。随机数是一次性标识符，用于检测重复的签名请求。

请在解析请求体或重写请求目标前验证。公开端点必须使用 HTTPS，并直接响应，不能重定向。如果代理会修改 `Host`，请用公开主机名配置验证器的 authority 选项，必要时包含非默认端口。

## 示例

这些最小服务器会保护所有路由，对 `/v1/search` 的有效调用返回 `{"ok":true}`。请将响应替换为工具的实现。它们在本地监听 `8080` 端口；部署时放在 HTTPS 后方，并保留签名覆盖的请求部分。

{% tabs %}
{% tab title="Node" %}
需要 Node 20 或更新版本。在应用目录中运行：

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

保存为 `server.mjs`，然后运行 `node server.mjs`：

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

const app = express();
app.use(assayVerify()); // Before body parsers and handlers.
app.post("/v1/search", (req, res) => {
  res.json({ ok: true });
});
app.listen(8080, "127.0.0.1");
```

中间件会设置 `req.rawBody`。对于 JSON 内容类型，它还会解析 `req.body`；其他请求体保持为 `Buffer`。如果代理重写主机，请使用 `assayVerify({ authority: "api.example.com" })`。
{% endtab %}

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

```sh
python -m pip install assay-provider-verify
```

保存为 `server.py`，然后运行 `python server.py`。此示例使用 Python 本地 WSGI 服务器；部署时请用生产 WSGI 服务器运行包装后的应用。

```python
from wsgiref.simple_server import make_server
from assay_provider_verify import AssayVerifyWSGI

def handler(environ, start_response):
    if environ["REQUEST_METHOD"] != "POST" or environ["PATH_INFO"] != "/v1/search":
        start_response("404 Not Found", [("Content-Type", "application/json")])
        return [b'{"error":"not_found"}']
    start_response("200 OK", [("Content-Type", "application/json")])
    return [b'{"ok":true}']

application = AssayVerifyWSGI(handler)

if __name__ == "__main__":
    with make_server("127.0.0.1", 8080, application) as server:
        server.serve_forever()
```

包装器会恢复 `wsgi.input` 中的请求体。如果代理重写主机，请使用 `AssayVerifyWSGI(handler, authority="api.example.com")`。
{% endtab %}

{% tab title="Go" %}
需要 Go 1.21 或更新版本。在新应用目录中运行：

```sh
go mod init example.com/assay-provider
go get github.com/assay-ai/assay-provider-verify/go@latest
```

保存为 `main.go`，然后运行 `go run .`：

```go
package main

import (
    "io"
    "log"
    "net/http"

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

func main() {
    verify, err := assayverify.Middleware(assayverify.Options{})
    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)))
}
```

中间件会恢复 `r.Body`。如果代理重写主机，请设置 `assayverify.Options{Authority: "api.example.com"}`。
{% endtab %}
{% endtabs %}

### 其他框架

每个适配器都接受相同的选项（`authority`、`replayStore` 或 `replay_store` 等）。

| 框架                  | 用法                                                                                                                                                         |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Fastify             | `import { assayVerifyPlugin } from "@goassay-ai/provider-verify/fastify"`，然后 `app.register(assayVerifyPlugin, {})`。适用于所有路由。                                |
| Hono                | `import { assayVerify } from "@goassay-ai/provider-verify/hono"`，然后 `app.use("*", assayVerify())`。                                                         |
| Next.js 路由处理函数      | `import { withAssayVerify } from "@goassay-ai/provider-verify/next"`，然后 `export const POST = withAssayVerify(async (req) => Response.json({ ok: true }))`。 |
| FastAPI 或 Starlette | `python -m pip install "assay-provider-verify[fastapi]"`，然后 `app.add_middleware(AssayVerifyMiddleware)`。                                                   |
| Flask               | `python -m pip install "assay-provider-verify[flask]"`，然后 `app.wsgi_app = AssayVerifyWSGI(app.wsgi_app)`。                                                  |

Python 的 `AssayVerifyMiddleware` 和 `AssayVerifyWSGI` 都从 `assay_provider_verify` 导入。

## 在本地检查设置

部署前，先向中间件发送一个有效的签名请求。验证器仓库在 `test-vectors/v1.json` 中发布了签名测试请求。下面的 Node 脚本使用该文件中的测试密钥和时钟挂载 Express 中间件，然后发送三个请求。

在安装了 `@goassay-ai/provider-verify` 和 `express` 的目录中运行：

```sh
curl -fsSLO https://raw.githubusercontent.com/assay-ai/assay-provider-verify/main/test-vectors/v1.json
```

保存为 `local-check.mjs`，然后运行 `node local-check.mjs`：

```js
import { readFileSync } from "node:fs";
import express from "express";
import { assayVerify } from "@goassay-ai/provider-verify/express";

const v = JSON.parse(readFileSync("v1.json", "utf8"));
const c = v.cases[0]; // A signed POST /v1/search?q=a%20b

const app = express();
app.use(assayVerify({
  // Test-only: the public fixture key, its fixed clock and its host.
  jwks: { keys: [v.publicKeyJwk] },
  now: () => c.params.created,
  authority: "api.example.test",
}));
app.post("/v1/search", (req, res) => res.json({ ok: true, got: req.body }));

const server = app.listen(0, "127.0.0.1", async () => {
  const { pathname, search } = new URL(c.request.url);
  const url = `http://127.0.0.1:${server.address().port}${pathname}${search}`;
  const signed = {
    method: c.request.method,
    headers: { ...c.request.headers, ...c.expected.headers, "content-type": "application/json" },
    body: c.request.body,
  };
  for (const [label, init] of [
    ["signed", signed],
    ["same request again", signed],
    ["unsigned", { method: "POST", body: c.request.body }],
  ]) {
    const res = await fetch(url, init);
    console.log(label, res.status, await res.text());
  }
  server.close();
});
```

预期输出：

```
signed 200 {"ok":true,"got":{"q":"a b"}}
same request again 401 {"error":"invalid_assay_signature","reason":"REPLAYED"}
unsigned 401 {"error":"invalid_assay_signature","reason":"MISSING"}
```

要检查你实际的处理函数，请将 `/v1/search` 路由替换为你的路由。Python（`jwks=`、`now=`、`authority=`）和 Go（`JWKS`、`Now`、`Authority`）也接受相同的测试选项。

{% hint style="danger" %}
测试夹具密钥是公开的，固定时钟会关闭有效期检查。只能在测试中使用它们。线上服务器必须使用默认密钥集和真实时钟。
{% endhint %}

## 连接并确认是否成功

1. 登录 Assay，打开上架条目的 **Connect & test** 步骤。
2. 将 **Your API's address** 设为运行中间件的公开 HTTPS 端点。
3. 在 **Authentication** 下选择 **Assay signature (recommended)**。如果选择 **Both**，服务还必须执行密钥检查；请按 [API 密钥设置步骤](/developers/zh/shang-jia-ni-de-api-ru-men/api-key-or-signature.md)操作。
4. 填写工具测试输入，点击 **Save and run test** 或 **Run the test again**。

签名调用应通过，且不出现 **Signature not enforced** 警告。对同一受保护路由的无签名调用必须返回 `401`，响应为 `{"error":"invalid_assay_signature","reason":"MISSING"}`。

## 使用测试向量测试

获取一份[验证器仓库](https://github.com/assay-ai/assay-provider-verify)。在仓库根目录运行相应测试套件：

{% tabs %}
{% tab title="Node" %}

```sh
npm --prefix node ci
npm --prefix node test -- test/vectors.test.ts
```

{% endtab %}

{% tab title="Python" %}
在虚拟环境中运行：

```sh
python -m pip install './python[test]'
python -m pytest python/tests/test_vectors.py
```

{% endtab %}

{% tab title="Go" %}

```sh
cd go
go test -run 'Test(Positive|Negative)Vectors' ./...
```

{% endtab %}
{% endtabs %}

`test-vectors/v1.json` 包含有效签名请求。测试套件将每个用例的请求头与 `expected.headers` 合并，使用 `publicKeyJwk` 作为密钥集，并将时钟固定到 `params.created`。

`test-vectors/negative.json` 为每个请求提供 `now` 和 `expectedReason`。每个用例都从全新的重放存储开始。`sequence` 会在各步骤之间保留同一存储，因此重放用例首次接受，随后返回 `REPLAYED`。即使在此文件中，`OK` 也表示接受。

测试自己的集成时，请使用相同输入和原始 UTF-8 请求体字节。

## 详情

| 检查项或设置  | 默认值                                                                    |
| ------- | ---------------------------------------------------------------------- |
| 签名有效窗口  | 最多 300 秒，前后各允许 60 秒时钟容差。                                               |
| 缓冲请求体上限 | 5 MiB（5,242,880 字节）。更大的请求体会在验证前收到 `413 {"error":"payload_too_large"}`。 |
| 获取密钥    | 5 秒超时；密钥缓存一小时。未知密钥最多每 60 秒触发一次重新获取；获取失败后暂停 5 秒再重试。                     |
| 重放存储    | 内存中保存。Node 中间件拥有自己的存储；Python 和 Go 使用进程级默认存储。请挂载并复用中间件。不同实例不共享内存。       |

多个服务实例运行时，请为每个验证器提供共享重放存储。如果存储失败，应继续拒绝请求，不能回退为接受。

## 错误

| 现象                                               | 解决方法                                 |
| ------------------------------------------------ | ------------------------------------ |
| Express 返回 `500` 和 `assay_verify_misconfigured`。 | 在请求体解析器之前挂载 `assayVerify()`。         |
| `DIGEST_MISMATCH`                                | 在解析器或代理修改字节前验证原始字节。                  |
| 代理后出现 `BAD_SIGNATURE`                            | 保留请求目标；如果 `Host` 改变，请配置公开 authority。 |

## 延伸阅读

* 上架你的 API（可选）：[或者：发布你自己的 API](https://docs.goassay.ai/list-your-api/zh/publish-your-own-api)。

## 下一步

* [拒绝原因](/developers/zh/shang-jia-ni-de-api-ru-men/reject-reasons.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-ru-men/verify-in-your-code.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.
