安全

H3 安全工具。

#认证

#basicAuth(opts)

创建一个基本认证中间件。

示例:

import { H3, serve, basicAuth } from "h3";
const auth = basicAuth({ password: "test" });
app.get("/", (event) => `Hello ${event.context.basicAuth?.username}!`, [auth]);
serve(app, { port: 3000 });

#requireBasicAuth(event, opts)

为当前请求应用基本认证。

示例:

import { defineHandler, requireBasicAuth } from "h3";
export default defineHandler(async (event) => {
  await requireBasicAuth(event, { password: "test" });
  return `Hello, ${event.context.basicAuth.username}!`;
});

#会话

#clearSession(event, config)

清除当前请求的会话数据。

#getSession(event, config)

获取当前请求的会话。

没有会话的请求会在内存中初始化一个新会话——在通过 {@link updateSession} 存储内容之前,不会发送 Set-Cookie,因此读取会话(例如执行身份验证检查)不会为匿名访客启动会话。因此,只有在会话被写入后,其 id 才会在请求之间保持稳定;请使用 {@link useSession} 来主动启动会话。

#sealSession(event, config)

加密并签名当前请求的会话数据。

#unsealSession(_event, config, sealed)

解密并验证当前请求的会话数据。

#updateSession(event, config, update?)

更新当前请求的会话数据。

#useSession(event, config)

为当前请求创建一个会话管理器。

如果请求未携带会话,则启动一个会话并将其持久化,使其 id 在请求之间保持稳定。使用 {@link getSession} 可以读取会话而不启动会话。

#指纹

#getRequestFingerprint(event, opts)

获取传入请求的唯一指纹。

#跨域资源共享(CORS)

#appendCorsHeaders(event, options)

向响应中添加 CORS 头。

#appendCorsPreflightHeaders(event, options)

向响应中添加 CORS 预检请求头。

#handleCors(event, options)

处理传入请求的 CORS。

如果传入请求是 CORS 预检请求,将添加 CORS 预检请求头并发送 204 响应。

如果返回值不是 false,表示请求已被处理,无需进一步操作。

示例:

const app = new H3();
app.all("/", async (event) => {
  const corsRes = handleCors(event, {
    origin: "*",
    preflight: {
      statusCode: 204,
    },
    methods: "*",
  });
  if (corsRes !== false) {
    return corsRes;
  }
  // 你的代码在此
});

#isCorsOriginAllowed(origin, options)

检查来源是否被允许。

#isPreflightRequest(event)

检查传入请求是否为 CORS 预检请求。

#路径

#isCanonicalPath(path, opts?)

判断 path 在 opts 下是否已经是规范形式——也就是说,{@link resolveDotSegments} 对其解析后会原样返回。两个方向都完全一致:当且仅当 resolveDotSegments(path, opts) === path 时返回 true。

这是解析器自身的快速路径守卫。将其导出,是为了让在热路径上进行规范化的调用方(例如每请求作用域或规则匹配)可以跳过该调用,以及由此产生的所有工作,而无需自行维护一份解析器解码逻辑的副本。这样的副本会在不知不觉中过时,而在作用域检查中遗漏规范化属于绕过问题,而不是性能问题。

传入与后续 {@link resolveDotSegments} 调用相同的选项,或更严格的选项:decodeSlashes/mergeSlashes 只会增加触发条件,因此在两者都启用时得到的 true,意味着在所有模式下都为 true。在一种模式下检查,却在另一种模式下解析,会使这一保证失效。

接收一个不带其他部分的路径名。与解析器一样,它不识别查询字符串或哈希,而是将它们当作路径进行扫描,因此 /a?next=/../b 会被报告为非规范形式(并且会解析为 /b)。

#normalizeRoute(route)

将路由模式规范化为 h3 注册它时所使用的规范形式——与其进行匹配的 event.url.pathname 具有相同的形式。

app.on()、app.use(route, …)、app.mount() 和 removeRoute() 都会对接收到的模式执行此操作。当将模式注册到你自己的路由器(例如在构建时编译的 rou3 路由器)中,而该路由器随后要与 h3 的 event.url.pathname 进行匹配时,请使用此函数,以确保两侧使用相同的字符串——如果模式的规范化方式不同,那么某个路由可能仍然可访问,而使用相同源字符串注册的守卫却无法匹配任何内容。

如果缺少开头的 /,则会添加一个(about → /about);请求路径名始终以百分号编码形式携带的字符会被编码(/café/** → /caf%C3%A9/**);不必要的转义会按照 h3 对请求路径名的解码方式进行解码(/%40handle → /@handle;%2F 和 %25 会保持编码);并且会解析 ./.. 段(/a/b/../c → /a/c)。rou3 模式语法(?、{、}、^、\)会原样保留——要匹配字面意义上的这些字符,请将其写成百分号编码形式。

幂等。如果传入绝对 URL(http://…),则会抛出异常:路由模式是路径名,而不是 URL。

示例:

normalizeRoute("/について/**"); // "/%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/**"

#resolveDotSegments(path, opts?)

解析路径中的 . 和 .. 段,并且绝不会超出根目录 /。结果始终是一个带单个前导 / 的绝对路径,因此它不可能是协议相对形式(//host)。

同时还会在任意 %25 嵌套深度下解码百分号编码的点段(%2e、%252e、...),并将 \ 规范化为 /,因此编码形式或基于反斜杠的路径穿越(例如 %2e%2e/、..\..\)会与字面量 ../ 一样被捕获。

默认情况下,%2f/%5c(编码的路径分隔符)会保持不变——参见 {@link ResolveDotSegmentsOptions.decodeSlashes}。

只有 ./.. 解析以及上述解码操作会改变字符串;其他所有百分号编码(%20、非 ASCII 字符、%3A,以及任何不构成完整段的 %2e)都会保持不变,因此结果会与 event.url.pathname 保持相同的表示形式,并能一致地匹配路由和规则。末尾的 ./.. 会解析为目录,并保留其末尾斜杠(/a/b/.. -> /a/、/a/. -> /a/),这符合 RFC 3986 §5.2.4,也与 WHATWG/nginx 下游的解析结果一致——因此作用域检查看到的是目录形式,而不是与其对应的文件形式。内部的空段会被保留(/a//b 仍为 /a//b)——与 WHATWG 一样,这里不会合并斜杠,因此空段会保留,而不会被折叠。唯一的例外是开头连续出现的斜杠:它始终会被限制为单个 /(WHATWG 会保留 //host),因此只有开头的斜杠能够保证是单个斜杠,而执行精确前缀匹配的使用方应当以相同方式规范化其允许列表。要同时折叠内部连续的斜杠(即采用合并斜杠的下游解析方式),请参见 {@link ResolveDotSegmentsOptions.mergeSlashes}。

#路由参数

路由参数会以其在 URL 路径中的形式传递给处理程序——即百分号编码形式。getRouterParams(event, { decode: true })(以及使用相同选项的 getValidatedRouterParams)会执行一次解码,而不是完整的规范化:

  • 编码的路径分隔符(%2f、%5c,以及任意 %25 嵌套深度下的形式:%252f、%25252f、……)永远不会被解码。被路由器匹配为单个段的参数中不可能出现原始 / 或 \,因此参数不会悄然获得路由和中间件从未看到的路径边界。
  • 其他所有转义序列都会精确解码一层。由于 %25 本身就是一个转义序列,%25XX 会解码为字面文本 %XX——因此结果中仍可能包含百分号转义序列。
app.get("/files/**:rest", (event) => {
  // GET /files/%252e%252e/x
  getRouterParams(event); // { rest: "%252e%252e/x" }
  getRouterParams(event, { decode: true }); // { rest: "%2e%2e/x" }

  // GET /files/%2500
  getRouterParams(event, { decode: true }); // { rest: "%00" }

  // GET /files/a%252fb  — separators stay encoded at every depth
  getRouterParams(event, { decode: true }); // { rest: "a%252fb" }
});

Important

不要再次解码返回的值。第二次调用 decodeURIComponent 会将 %2e%2e/x 转换为 ../x,并将 %00 转换为 NUL 字节——这些路径穿越和控制字符对路由或任何基于路径名的中间件而言都是不可见的。请按返回时的形式验证该值;如果要将其用作文件系统路径或上游路径,请使用 resolveDotSegments 解析,而不是继续解码。