运行时包管理器打包器测试运行器代码检查格式化指南参考博客反馈

CSRF 防护

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

Bun.CSRF 生成和验证 CSRF(跨站请求伪造)令牌。Bun 使用 HMAC 对令牌签名。每个令牌都包含签发时间戳和过期时长。

csrf.ts
// 生成一个与请求者会话绑定的令牌
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

Bun.CSRF.generate()

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

generate.ts
const token = Bun.CSRF.generate("my-secret-key");

参数:

  • secret(string,可选)— 用于签署令牌的密钥。如果未提供,Bun 会生成一个随机的内存中默认密钥(每个线程唯一)。
  • options(object,可选):
选项类型默认值描述
expiresInnumber86400000令牌过期前的毫秒数。默认为 24 小时。
encodingstring"base64url"令牌编码格式:"base64"、"base64url" 或 "hex"。
algorithmstring"sha256"HMAC 算法:"sha256"、"sha384"、"sha512"、"sha512-256"、"blake2b256" 或 "blake2b512"。
sessionIdstring(无)将令牌绑定到请求主体(会话 ID、用户 ID 或等效标识符)。只有在调用 verify() 时传入相同的 sessionId,令牌才能通过验证。

返回: string — 编码后的令牌。

generate-options.ts
// 绑定到请求者会话的令牌,1 小时后过期,编码为 hex
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。

verify.ts
const isValid = Bun.CSRF.verify(token, { secret: "my-secret-key" });

参数:

  • token(string,必需)— 要验证的令牌。
  • options(object,可选):
选项类型默认值描述
secretstring(自动)用于签署令牌的密钥。如果未提供,则使用与 generate() 相同的内存中默认密钥。
maxAgenumber86400000令牌的最大有效时长(毫秒),独立于令牌自身的 expiresIn。
encodingstring"base64url"必须与调用 generate() 时使用的编码相匹配。
algorithmstring"sha256"必须与调用 generate() 时使用的算法相匹配。
sessionIdstring(无)必须与调用 generate() 时使用的 sessionId 相匹配。绑定到一个主体的令牌无法通过其他主体的验证。生成时未提供 sessionId 的令牌,在验证时提供了 sessionId 则无法通过验证。

返回: boolean

verify-options.ts
// 验证一个绑定到请求者会话的令牌
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 传递给这两次调用,这样令牌只对签发给它的用户有效。

server.ts
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">发送</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("无效的 CSRF 令牌", { status: 403 });
        }

        return new Response("成功");
      },
    },
  },
});

console.log(`监听于 ${server.url}`);

默认密钥

如果在 generate() 和 verify() 中都省略 secret 参数,Bun 会使用每个线程生成一次的随机密钥。默认密钥适用于单线程应用,但令牌无法跨服务器或 worker 验证,也无法在重启后验证。

default-secret.ts
// 这两个调用在此运行时上下文中使用相同的每线程默认密钥。
const token = Bun.CSRF.generate();
const isValid = Bun.CSRF.verify(token); // true

对于生产环境,请始终提供跨基础设施共享的显式密钥。


TypeScript

types.ts
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;
}