> ## Documentation Index
> Fetch the complete documentation index at: https://bun.ll1025.cn/llms.txt
> Use this file to discover all available pages before exploring further.

# CSRF 保护

> 使用 Bun 内置 API 生成和验证 CSRF 令牌

`Bun.CSRF` 生成和验证 [CSRF（跨站请求伪造）](https://owasp.org/www-community/attacks/csrf)令牌。令牌使用 HMAC 签名并包含过期时间戳。

```ts title="csrf.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
// 生成一个绑定到请求者会话的令牌
const token = Bun.CSRF.generate("my-secret", { sessionId: "user-session-id" });

// 验证它
const isValid = Bun.CSRF.verify(token, { secret: "my-secret", sessionId: "user-session-id" });
console.log(isValid); // true
```

<Callout type="warning">
  始终向 `generate()` 和 `verify()` 传递一个 `sessionId`（请求者的会话标识符或用户 ID）。如果没有它，令牌仅绑定到密钥 — 服务器曾经颁发的任何令牌对每个用户都有效，因此攻击者可以在自己的会话中获取令牌，然后在受害者的浏览器中伪造跨站请求进行重放。
</Callout>

***

## `Bun.CSRF.generate()`

生成一个 CSRF 令牌。令牌包含一个加密随机数、一个时间戳和一个 HMAC 签名，编码为字符串。

```ts title="generate.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
const token = Bun.CSRF.generate("my-secret-key");
```

**参数：**

* `secret`（字符串，可选）— 用于签署令牌的密钥。如果未提供，Bun 会生成一个随机的内存默认密钥（每个线程唯一）。
* `options`（对象，可选）：

| 选项          | 类型       | 默认值           | 说明                                                                                       |
| ----------- | -------- | ------------- | ---------------------------------------------------------------------------------------- |
| `expiresIn` | `number` | `86400000`    | 令牌过期前的毫秒数。默认为 24 小时。                                                                     |
| `encoding`  | `string` | `"base64url"` | 令牌编码格式：`"base64"`、`"base64url"` 或 `"hex"`。                                               |
| `algorithm` | `string` | `"sha256"`    | HMAC 算法：`"sha256"`、`"sha384"`、`"sha512"`、`"sha512-256"`、`"blake2b256"` 或 `"blake2b512"`。 |
| `sessionId` | `string` | （无）           | 将令牌绑定到请求主体（会话 ID、用户 ID 或等效项）。令牌仅在将相同 `sessionId` 传递给 `verify()` 时才验证通过。                  |

**返回：** `string` — 编码后的令牌。

```ts title="generate-options.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
// 绑定到请求者会话的令牌，1 小时后过期，编码为十六进制
const token = Bun.CSRF.generate("my-secret", {
  sessionId: "user-session-id",
  expiresIn: 60 * 60 * 1000,
  encoding: "hex",
});

// 使用不同的算法
const token2 = Bun.CSRF.generate("my-secret", {
  sessionId: "user-session-id",
  algorithm: "sha512",
});
```

***

## `Bun.CSRF.verify()`

验证一个 CSRF 令牌。如果令牌有效且未过期，返回 `true`，否则返回 `false`。

```ts title="verify.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
const isValid = Bun.CSRF.verify(token, { secret: "my-secret-key" });
```

**参数：**

* `token`（字符串，必需）— 要验证的令牌。
* `options`（对象，可选）：

| 选项          | 类型       | 默认值           | 说明                                                                                                             |
| ----------- | -------- | ------------- | -------------------------------------------------------------------------------------------------------------- |
| `secret`    | `string` | （自动）          | 用于签署令牌的密钥。如果未提供，则使用与 `generate()` 相同的默认内存密钥。                                                                   |
| `maxAge`    | `number` | `86400000`    | 令牌的最大年龄（毫秒），独立于令牌自身的 `expiresIn`。                                                                              |
| `encoding`  | `string` | `"base64url"` | 必须与 `generate()` 期间使用的编码匹配。                                                                                    |
| `algorithm` | `string` | `"sha256"`    | 必须与 `generate()` 期间使用的算法匹配。                                                                                    |
| `sessionId` | `string` | （无）           | 必须与 `generate()` 期间使用的 `sessionId` 匹配。绑定到一个主体的令牌对其他任何主体的验证都会失败，而没有 `sessionId` 生成的令牌在提供了 `sessionId` 的验证中也会失败。 |

**返回：** `boolean`

```ts title="verify-options.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
// 验证绑定到请求者会话的令牌
const isValid = Bun.CSRF.verify(token, {
  secret: "my-secret",
  sessionId: "user-session-id",
});

// 强制执行比令牌生成时更短的最大年龄
const isValid2 = Bun.CSRF.verify(token, {
  secret: "my-secret",
  sessionId: "user-session-id",
  maxAge: 60 * 1000, // 拒绝超过 1 分钟的令牌
});
```

***

## 与 `Bun.serve()` 配合使用

典型模式是：在渲染表单时生成一个令牌，将其嵌入隐藏字段，并在提交表单时验证它。将请求者的会话标识符作为 `sessionId` 传递给两个调用，以便令牌仅对为其颁发的用户有效。

```ts title="server.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
const SECRET = process.env.CSRF_SECRET || "my-secret";

// 从会话 cookie 中解析请求者的会话标识符。当访问者还没有会话时返回
// null — 切勿回退到共享占位符，否则每个无会话的访问者将共享一个令牌绑定。
function getSessionId(req: Request): string | null {
  return req.headers.get("cookie")?.match(/(?:^|;\s*)session=([^;]+)/)?.[1] ?? null;
}

const server = Bun.serve({
  routes: {
    "/form": req => {
      // 在发放表单之前为每个访问者创建会话，以便令牌
      // 绑定到此访问者而非其他人。
      let sessionId = getSessionId(req);
      const headers = new Headers({ "Content-Type": "text/html" });
      if (!sessionId) {
        sessionId = crypto.randomUUID();
        headers.append("Set-Cookie", `session=${sessionId}; HttpOnly; SameSite=Lax; Path=/`);
      }

      const token = Bun.CSRF.generate(SECRET, { sessionId });

      return new Response(
        `<form method="POST" action="/submit">
          <input type="hidden" name="_csrf" value="${token}" />
          <input type="text" name="message" />
          <button type="submit">Send</button>
        </form>`,
        { headers },
      );
    },

    "/submit": {
      POST: async req => {
        const sessionId = getSessionId(req);
        const formData = await req.formData();
        const csrfToken = formData.get("_csrf");

        if (!sessionId || typeof csrfToken !== "string" || !Bun.CSRF.verify(csrfToken, { secret: SECRET, sessionId })) {
          return new Response("Invalid CSRF token", { status: 403 });
        }

        return new Response("OK");
      },
    },
  },
});

console.log(`Listening on ${server.url}`);
```

***

## 默认密钥

如果您在 `generate()` 和 `verify()` 中都省略了 `secret` 参数，Bun 会使用每个线程生成一次的随机密钥。这对于单线程应用很方便，但令牌不会跨服务器、worker 或在重启后验证。

```ts title="default-secret.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
// 在此运行时上下文中，两个调用都使用相同的每线程默认密钥。
const token = Bun.CSRF.generate();
const isValid = Bun.CSRF.verify(token); // true
```

对于生产使用，请始终提供在您的基础设施中共享的显式密钥。

***

## TypeScript

```ts title="types.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
type CSRFAlgorithm = "blake2b256" | "blake2b512" | "sha256" | "sha384" | "sha512" | "sha512-256";

interface CSRFGenerateOptions {
  expiresIn?: number;
  encoding?: "base64" | "base64url" | "hex";
  algorithm?: CSRFAlgorithm;
  sessionId?: string;
}

interface CSRFVerifyOptions {
  secret?: string;
  encoding?: "base64" | "base64url" | "hex";
  algorithm?: CSRFAlgorithm;
  maxAge?: number;
  sessionId?: string;
}

namespace Bun.CSRF {
  function generate(secret?: string, options?: CSRFGenerateOptions): string;
  function verify(token: string, options?: CSRFVerifyOptions): boolean;
}
```
