news 2026/10/15 6:29:32

H3 完全上手指南:极简 HTTP 框架如何内置安全边界

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
H3 完全上手指南:极简 HTTP 框架如何内置安全边界

H3 完全上手指南:极简 HTTP 框架如何内置安全边界

【免费下载链接】h3⚡️ Minimal H(TTP) framework built for high performance and portability项目地址: https://gitcode.com/GitHub_Trending/h31/h3

H3 是一个可组合、运行时无关的极简 HTTP 框架:核心只负责事件、路由与中间件编排,请求体解析、响应构造、Cookie、会话、代理、JSON-RPC 全部以独立工具函数形式按需取用,且每个函数都内置了明确的安全默认值。

它解决什么问题

写 HTTP 服务时,痛点很少是"少一个功能",而是"功能之间谁替我守边界"。请求体多大算超限?超限是预缓冲还是边读边算?multipart 要不要自动嗅探?x-forwarded-for第一个值能不能信?路由参数解码一次还是两次?这些问题在 Express 时代大多要自己查 RFC 自己判断,写错一个就是漏洞。

H3 的思路是把每个边界写进源码并在 JSDoc 里讲清楚理由:readBody绝不从请求头自动探测 multipart,assertBodySize拒绝同时携带Content-Length和Transfer-Encoding的请求,getRouterParams解码后也永远不会还原出/。你不用先当半个安全专家,才能安全地写一个 handler。

30 秒跑起来

import { H3, serve, readBody, assertBodySize } from "h3"; const app = new H3(); // 轻量实例,路由由 rou3 提供 app.get("/", () => "hello world"); // 返回字符串即 200 text/plain app.post("/", async (event) => { assertBodySize(event, 10 * 1024 * 1024); // 声明 10MB 上限,读体超了抛 413 const data = await readBody(event); // 默认按 JSON 解析 return { ok: true, data }; // 返回对象自动序列化为 JSON }); serve(app); // 绑定 app.fetch 到运行时服务器

所有工具函数都从包入口导出,清单见 src/index.ts。Node、Bun、Deno、Cloudflare 各有适配入口(如 src/_entries/node.ts 里的serve与toNodeHandler),同一份应用代码可直接换运行时。

场景一:接住请求——先验边界再取数据

请求体超限怎么拦

assertBodySize(event, limit)不是预缓冲,而是把event.req换成 srvx 的limitRequestBody限流代理:字节边读边计数,累计超过limit的瞬间抛413。它还有两道前置防御(src/utils/body.ts):

  • Content-Length诚实超标 → 处理前直接413,不碰流;
  • 同时带Content-Length与Transfer-Encoding→400,这是请求走私向量(RFC 7230)。

注意语义边界:限流绑定"消费",chunked 请求的超限只在 handler 真正读体时触发;从未读取的 body 不计入。想要中间件形式就用bodyLimit(limit)(src/utils/middleware.ts),语义完全相同。

请求体怎么解析、何时校验

readBody(event, options?)默认按 JSON 解析;仅当Content-Type为application/x-www-form-urlencoded时回退 URL 编码。multipart/form-data必须显式传type: "formData",源码注释直接点明原因(src/utils/body.ts):

// 反例:依赖 Content-Type 自动嗅探 multipart // → 不可信请求可以把应用拖进昂贵的 multipart 解析 // 正例:显式 opt-in,解析失败抛 400 const body = await readBody(event, { type: "formData" });

解析后立即校验用readValidatedBody(event, validate, { onError? }):validate可以是普通函数,也可以是 Standard-Schema 兼容库(zod、valibot);onError可接收issues自定义statusText与汇总信息。查询参数同理:getQuery(event)取解析对象(嵌套解析用event.url.search拿原始串),getValidatedQuery(event, schema)读完即校验(src/utils/request.ts)。

场景二:发出响应——流式输出与清理

流式响应怎么发

iterable(source)把生成器/异步迭代器变成流式响应,chunk 限字符串或 Buffer。关键行为:首个 chunk 在响应创建前被 await——也就是说第一个 chunk 之前你设置event.res.status和 headers 都还生效,第一个 chunk 之后设置一律被忽略,因为头已经上线路了(src/utils/response.ts)。生成器return的值与yield值同等对待。

return iterable(async function* work() { yield "<ol>"; for (let i = 0; i < 10; i++) { await delay(1000); yield `<li>job #${i} done</li>`; } return "</ol>"; // return 值同样发出 });

HTML 输出用标签模板html

${userName}
,插值自动转义& < > " ';已有转义标记的字符串用raw包起来绕过转义。**raw` 只接受你信任的已转义片段,把用户输入传进去等于重新引入 XSS。**

重定向与资源清理

redirect(location, status?)默认302,响应体附带 meta refresh 页面兜底忽略头的老客户端。redirectBack(event, { fallback?, allowQuery? })基于referer返回上一页:默认只取 pathname(剥掉 query 与 hash),跨源或缺失时落回fallback(默认"/")。fallback必须是硬编码路径,传用户输入就是开放重定向。

onDispose(event, cb)注册"事件彻底结束"回调:响应体流完、客户端断开或出错的任意一种都算结束,按注册顺序在全局onResponse之后执行。注意它表示h3 处理完了,不是"客户端已收到";要在产出响应过程中响应客户端断开(比如中止上游 fetch),监听event.req.signal。

app.get("/task", (event) => { const timer = setInterval(tick, 1000); onDispose(event, () => clearInterval(timer)); // 流结束/断开即清理 return iterable(stream); });

场景三:守住安全边界——路径、来源与会话

路由参数怎么解码

getRouterParams(event, { decode: true })只解码一层,且编码的路径分隔符(%2f、%5c及其任意%25嵌套)永远保持编码(src/utils/request.ts)。这意味着:

// GET /files/%252e%252e/x getRouterParams(event); // { rest: "%252e%252e/x" } 原始形态 getRouterParams(event, { decode: true }); // { rest: "%2e%2e/x" } —— 到此为止 // 反例:decodeURIComponent(params.rest) → "../x",路径穿越 // 正例:把返回值当最终值校验;要落文件系统先过 resolveDotSegments

解码一次的结果仍可能含%XX(%252e%252e→%2e%2e),二次decodeURIComponent会把它还原成../,还顺带还原%00NUL 字节。

路径穿越怎么防御

resolveDotSegments(path, opts?)解析./..段且永远不会逃逸到根/之上,结果恒为单一前导/的绝对路径(杜绝//host协议相对形态)。它按任意%25嵌套深度解码点段(%2e、%252e…)并把\归一为/,所以%2e%2e/、..\..\与字面../一样被捕获(src/utils/path.ts)。尾部./..按目录处理并保留结尾斜杠(RFC 3986 §5.2.4),内部空段保留(/a//b不变)。

两个可选参数面向下游差异:decodeSlashes让范围检查也能捕获编码分隔符(下游 nginx 会自己把%2f解回/);mergeSlashes模拟合并空段的下游。配isCanonicalPath(path, opts?)做热路径快速守卫——文档说得很直接:scope 检查里漏掉一次规范化,是绕过漏洞而非性能问题。

请求来源怎么判定

getRequestIP(event)默认取event.req.ip(连接对端,或服务器配置信任代理后解析的地址)。xForwardedFor: true改为取x-forwarded-for链的第一个条目——这正是客户端能自行写入的位置。

// 反例:getRequestIP(event, { xForwardedFor: true }) 用于限流/白名单 // → 任何调用者可自报 IP,白名单、限流、审计全部击穿 // 正例:让服务器侧信任代理(srvx trustProxy 从右往左解析),选项保持关闭

getRequestHost/getRequestURL/getRequestProtocol同理,x-forwarded-*选项一律 opt-in。由Host头推导的 origin 不能用于 CSRF/origin 校验、缓存键或发给其他用户的绝对链接,除非上游已对 Host 做白名单固定。

会话与 CORS

useSession(event, config)基于 iron 加密(src/utils/session.ts、src/utils/internal/iron-crypto.ts):会话写入加密签名后的 Cookie,默认secure、httpOnly、sameSite=lax、path=/;password必须至少 32 字符且要来自密码学随机源。maxAge是绝对寿命,idleTimeout是滑动窗口(通过重 seal Cookie 前移,节流到每半个窗口一次)。getSession/updateSession/clearSession与sealSession/unsealSession分别负责读写与加解密。

handleCors(event, options)处理完整 CORS:预检请求直接附加预检头并返回204,返回值非false即已处理。origin默认"*";若传正则,注意正则不锚定——/example\.com/会匹配https://example.com.evil.test,必须写成/^https?:\/\/([a-z0-9-]+\.)?example\.com$/这种带锚点转义的形态(src/utils/cors.ts)。

场景四:代理请求——内部子请求与外部转发

三个函数层层递进(src/utils/proxy.ts):

  • fetchWithEvent(event, url, init?):url以/开头 → 经event.app.fetch()派内部子请求,继承过滤后的头与运行时元数据(ip、waitUntil),永不出进程;外部 URL → 原生fetch(url, init)原样发送,事件的头与上下文一律不继承(向任意主机转发 Cookie/Authorization 不安全)。
  • proxyRequest(event, target, opts):把入站请求代理出去。请求体流式透传、不缓冲,所以此前读过 body(readBody()、readFormData()或读体中间件)会锁死流导致失败——需要"先检查再代理"就从event.req.clone()读:
app.all("/proxy", async (event) => { const body = await event.req.clone().json(); // 从克隆体检查 // ... return proxyRequest(event, target, { filterHeaders: ["cookie", "authorization"], // 对不完全信任的上游剥离凭据 }); // 原请求流完好 });

默认行为值得记住:入站的Cookie与Authorization原样转发(同信任的反向代理这是对的);上游 3xx 默认透传不跟随;xfwd默认false,开启后也只把服务端视角的值追加/覆写进x-forwarded-*,客户端值永远不赢。

  • proxy(event, target, opts):与proxyRequest的关键差异是默认不转发入站头,只发调用方通过opts.headers显式传的头,且opts.filterHeaders对它无效。

共同红线:不要把未净化的用户输入直接当url/target——主机白名单、内部路径拦截、协议强制都是调用方的职责;/开头的内部 target 会绕过外部安全层(反向代理认证、IP 白名单、mTLS)。

场景五:WebSocket 与 JSON-RPC 通道

defineJsonRpcHandler({ methods, ... })生成实现 JSON-RPC 2.0 的 H3 处理器,内置三条安全默认(src/utils/json-rpc.ts):

  • 强制 JSONContent-Type(否则415):不强制的话,攻击者页面的 HTML 表单就构成 CORS "simple request",可带受害者 Cookie 免预检到达;
  • 默认拒绝跨源请求(带不匹配Origin的请求403;无Origin的 CLI/服务端调用放行);
  • 批处理默认上限 50 条:每条并发派发,不封顶等于一次请求扇出成任意次方法调用,限流与配额全部失效。
app.post("/rpc", defineJsonRpcHandler({ methods: { sum: ({ params }, event) => (params as any).a + (params as any).b, }, }));

defineJsonRpcWebSocketHandler(methods, hooks?)走 WebSocket:它不校验Origin——WebSocket 升级不受 CORS 约束,任意源页面都能携带访客 Cookie 建连。请在upgrade钩子里自己校验Origin,必要时抛Response中止。defineWebSocket(hooks)+defineWebSocketHandler(http?)提供纯 WS 场景,非升级请求默认426。

原理深潜:两个代表性实现

为什么限流绑定"消费"而不是预缓冲

assertBodySize的取舍见 src/utils/body.ts。预缓冲看似简单,但会把整个 body 留在内存里、阻塞流式 handler,而且拦不住撒谎偏小的Content-Length。H3 的做法是三步:先处理"诚实"的超标声明(处理前413),再拒绝走私组合(400),最后把event.req替换成limitRequestBody包装的代理,createError注入的是标准HTTPError,body 读取器与错误管线无需任何特殊映射就能把它当作已处理的413。

代价是明确写出来的:chunked/未知长度的超限只在 handler 读体时浮现,而不是进 handler 前;未被读取的 body 不计数。这是"字节精确 + 零额外内存"与"入口拦截"之间的选择,H3 选了前者,并在 JSDoc 里把两个后果都写明白。

内部子请求如何堵死//evil.com

proxy/fetchWithEvent用target[0] === "/"判定内部目标,但 URL 解析器会把更长的前导分隔符序列读成域名://evil.com/x甚至/\evil.com/x(\对特殊协议也是分隔符)。若照原样解析,一个"内部"子请求会拿着外部 origin 出发。src/utils/proxy.ts 的createSubRequest用一条正则先折叠前导序列:

// 分隔符本身(\ 也算)+ 解析前会被剥掉的 C0 字符(\t \n \r) const LEADING_SEPARATOR_RUN_RE = /^(?:[/\\]|[\t\n\r])+/; const url = new URL(path.replace(LEADING_SEPARATOR_RUN_RE, "/"), event.url);

正则特意覆盖了 C0 字符,因为/\t/evil.com会被 URL 解析器先剥掉制表符、再读成//evil.com——不堵这里就是绕过。解析时以event.url为基址,内部 target 因此永远落在应用自己的 origin 上。

坑与安全边界

集中列几条最常见的误用,每条都给正确写法:

  1. 代理前读了 body:readBody()之后再proxyRequest会因流被消费而失败。正确:await event.req.clone().json()检查,原流留给代理。
  2. 路由参数二次解码:decodeURIComponent再解一遍会把%2e%2e变回../。正确:getRouterParams(event, { decode: true })的返回值当最终值,落文件系统前过resolveDotSegments。
  3. 信任x-forwarded-*第一个值:任何xForwardedFor: true类配置都等于允许调用者自报 IP。正确:服务器侧配置信任代理(srvxtrustProxy),读event.req.ip。
  4. 开放重定向:redirect(query.next)或redirectBack(event, { fallback: query.next })都是开放重定向。正确:location/fallback走白名单或硬编码路径;"回上一页"优先redirectBack的 pathname 默认行为。
  5. 凭据外流:向不完全信任的上游proxyRequest会原样转发Cookie与Authorization。正确:filterHeaders: ["cookie", "authorization"];或者改用proxy/fetchWithEvent,它们默认就不继承事件头。
  6. raw接用户输入 / 正则 origin 不锚定:前者重开 XSS,后者让*.evil.test混进 CORS 白名单。正确:raw只包可信标记,CORS 正则^…$锚定并转义点号。

生态与延伸

  • 文档导航索引:docs/2.utils/0.index.md,按 Request、Response、Cookie、Security、Proxy、MCP、More、Community 八类组织;完整 API 清单即 src/index.ts 的 Utils 段。
  • 可运行示例:examples/cookies.mjs、examples/cors.mjs、examples/query-params.mjs、examples/server-sent-events.mjs、examples/websocket.mjs、examples/nested-app.mjs。
  • 行为基准:test/ 下与 utils 一一对应的用例,如 test/cookies.test.ts、test/proxy.test.ts、test/security.test.ts、test/json-rpc.test.ts,不确定某个默认行为时先看它们。
  • 社区兼容库(Apitally、unjwt、Clear Router 等)收录于 docs/2.utils/99.community.md。

结语

H3 的价值不在功能数量,而在每个默认值都可追溯:multipart 的 opt-in 解析、限流绑定消费、路由参数单层解码、代理凭据的转发策略、内部子请求的 origin 钉死——这些边界写在源码注释与 JSDoc 里,而不是留给你踩。拿到new H3()这个轻量实例后,按需取用工具函数,就能用很少的代码跨 Node、Bun、Deno、Cloudflare 部署一个安全基线完整的服务。下一步建议通读 docs/2.utils/ 的八篇文档,再对照test/里的用例把每个默认行为记牢。

【免费下载链接】h3⚡️ Minimal H(TTP) framework built for high performance and portability项目地址: https://gitcode.com/GitHub_Trending/h31/h3

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 6:28:47

PyTorch核心机制与工程实践:从张量自动求导到模型部署

1. 为什么大家都在聊 PyTorch&#xff1a;它在解决什么问题这几年只要聊到深度学习&#xff0c;PyTorch 几乎是绕不开的名字。不管你是做计算机视觉、自然语言处理&#xff0c;还是搞语音识别、推荐系统&#xff0c;PyTorch 都像是一个默认选项。我自己是从静态图框架时代一路用…

作者头像 李华
网站建设 2026/10/15 6:28:31

TensorFlow工程化核心:计算图、SavedModel与全栈部署实践

1. 这不是“学个框架”那么简单&#xff1a;TensorFlow到底在解决什么问题&#xff1f;很多人第一次听说TensorFlow&#xff0c;是在某次技术分享会上听到“谷歌开源的深度学习框架”&#xff0c;或者在招聘JD里看到“熟悉TensorFlow者优先”。但如果你真去翻它的GitHub仓库首页…

作者头像 李华
网站建设 2026/10/15 6:28:18

TensorFlow核心逻辑与工程实践:从数据管道到模型部署

最近翻后台留言&#xff0c;发现问 TensorFlow 的人依然不少。尤其是有 Python 基础但没接触过深度学习的同学&#xff0c;最容易把 TensorFlow 当成一个“背 API 的工具”——今天查几行模型代码&#xff0c;明天跑一个 Demo&#xff0c;折腾几天发现什么都没学到。其实 Tenso…

作者头像 李华
网站建设 2026/10/15 6:27:57

RAR归档文件解压全指南:从格式识别到批量处理与避坑

简介&#xff1a;GC-PowerStation 2019中文版是面向表面贴装工艺工程师的坐标导出与光绘文件处理工具&#xff0c;支持导入电子表格、文本等多种坐标文件格式&#xff0c;并完成多层光绘文件的预览、合并与校验&#xff0c;帮助电子制造从业者在生产前检查设计准确性&#xff0…

作者头像 李华
网站建设 2026/10/15 6:27:23

多时间尺度源储荷协调调度中的储能特性分布建模与Matlab实现

把储能电站接入电网以后&#xff0c;调度这件事突然就从“单一电源跟跑负荷”变成了“多角色同台博弈”。我在实际项目里跑过不少调度模型&#xff0c;最直观的感受是&#xff1a;储能不是一台机器&#xff0c;而是一群特性参差不齐的机器&#xff0c;如果只用平均参数去描述&a…

作者头像 李华