关于 telegram-miniapps
telegram-miniapps 是指导 Telegram Mini App 开发与安全验证的技能,面向实现 WebView 应用的开发者。它重点说明如何验证签名 initData、处理设备视口与安全存储,以及将结果返回机器人。
主要功能
- 01验证 HMAC 与 Ed25519 签名
- 02检查 initData 新鲜度与重复字段
- 03处理视口、安全区与主题
- 04说明应用向机器人传递结果
- 05提供可运行的 Python 验证器
适用场景
- 01实现 Telegram Mini App 登录验证
- 02审查 initData 校验与会话交换
- 03修复手机 WebView 布局问题
- 04将 Mini App 操作结果发送给机器人
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ ssheleg/ telegram-dev --skill telegram-miniapps安装后在 Claude Code 中输入/telegram-miniapps确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
fixtures/verify_initdata.pyreferences/app-to-bot.mdinitdata-verification.mdviewport-and-platform.md详细介绍
Telegram Mini Apps — 一个经过签名的数据块就是全部依据
Mini App 是一个附带查询字符串的 WebView 网页。**这个查询字符串是证明用户身份的唯一依据,并且由你的 bot 令牌签名。**在服务器上验证它,你的身份验证强度就与 bot 本身相当;信任 SDK 交给你的解析后对象,则毫无保障。
对照 core.telegram.org/bots/webapps 阅读,日期为 2026-08-25,Bot API 10.3。
深入内容,按需加载:
| 阅读 | 适用场景 |
|---|---|
references/initdata-verification.md | 实现或审查验证逻辑 — 两种算法,三种语言,以及失败场景 |
references/app-to-bot.md | 向 bot 返回结果、内联查询,以及在应用内付款 |
references/viewport-and-platform.md | 应用在真机上显示异常 — 视口、安全区域、全屏、主题 |
可运行,并随本文件一同提供:
fixtures/verify_initdata.py — 可直接复制的验证器,
以及九项检查,确保它会拒绝遭篡改的用户 ID、过期的 auth_date、重新序列化的 user 字段和顺序颠倒的密钥派生。python3 fixtures/verify_initdata.py 只需标准库即可运行。
initDataUnsafe 这个名字是有意为之
// The client hands you both. One is evidence, the other is a convenience.
const raw = window.Telegram.WebApp.initData; // the signed string — SEND THIS
const unsafe = window.Telegram.WebApp.initDataUnsafe; // already parsed — display only
initDataUnsafe.user.id 是一个用户可以随意设置的数字,因为它来自运行在用户设备上的网页。**关于你是谁、你拥有什么、你能看到什么,所有这些决定都必须在服务器上根据原始字符串重新作出。**将 initData 发送到你的后端,在那里验证它,并根据验证结果确定身份。
这项技能旨在防止的唯一缺陷:接受客户端传入 { userId } 的端点。它能正常工作,也能用于演示,但会让任何人都能读取任何人的数据。
验证,务必做到准确
import hashlib, hmac, time
from urllib.parse import parse_qsl
MAX_AGE = 300 # seconds; a decision, not a constant handed down
def verify(init_data: str, bot_token: str) -> dict:
pairs = dict(parse_qsl(init_data, strict_parsing=True)) # already url-decoded
received = pairs.pop("hash", None)
if not received:
raise ValueError("no hash")
pairs.pop("signature", None) # the OTHER protocol's input — see below
check = "\n".join(f"{k}={pairs[k]}" for k in sorted(pairs))
secret = hmac.new(b"WebAppData", bot_token.encode(), hashlib.sha256).digest()
expected = hmac.new(secret, check.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, received):
raise ValueError("bad signature")
if time.time() - int(pairs["auth_date"]) > MAX_AGE:
raise ValueError("stale")
return pairs
以下五种情况都会导致验证失败,但仍会让某些人的代码返回“有效”:
- 网上一半的代码片段都把密钥派生方向写反了。 密钥是
HMAC_SHA256(key="WebAppData", msg=bot_token)——常量是密钥,令牌是消息。交换两者会得到一个永远无法匹配的稳定哈希,而常见的“修复方法”就是不再检查。 - 按键排序,用
\n连接,并使用收到的原始值——只进行一次 URL 解码,不重新编码,也不重新序列化。user是 JSON 字符串,必须保持收到时的原样;解析后再重新转储会改变键的顺序并破坏哈希。 - 在构造字符串前移除
hash,signature也要移除——但要出于正确的原因。signature不是垃圾数据:它是 Ed25519 路径的输入,属于一个独立协议,采用独立的规范化方式(以{bot_id}:WebAppData为前缀,然后使用不含hash或signature的排序键值对)。应将两种规范化逻辑拆分成两个函数——fixtures/verify_initdata.py中的hmac_check_string和ed25519_check_string——因为一个供两者共用的辅助函数可能会让规范化错误通过自身验证;而 fixture 中的黄金测试向量(手动计算,绝不由待测逻辑生成)正是用来捕获此类错误的:带有signature的 initData 只有通过正确路径才能验证成功。 - 在进行任何计算前,拒绝重复字段和无效数据。
parse_qsl返回的是列表;将其折叠成字典会悄悄保留最后一个重复项,而user=innocent&user=admin会引发关于 HMAC 覆盖了哪个副本的争议,验证器不该遇到这种情况。应直接拒绝重复字段和无法解析的数据,并给auth_date设置一个未来时间上限(时钟偏差,以秒计),因为未来的时间戳只会变得更新。 auth_date不能省略。 如果不设有效期窗口,被截获的initData就会成为永久的持有者令牌。选定一个窗口并明确说明,超出窗口的内容应视为未通过身份验证,而不是记下错误后继续处理。- 使用恒定时间比较。 对十六进制摘要使用
==会形成计时预言机;语言提供的compare_digest等效函数几乎没有额外开销。
不要在每次请求时都验证
initData 用于登录,不是会话。验证一次,签发自己的短期令牌,其余 API 请求都使用该令牌。
POST /auth/telegram { initData } → verify → your session JWT (short TTL)
GET /api/... Authorization: Bearer <your token>
每次调用都重新验证,会迫使你把 MAX_AGE 设得足以覆盖较长的会话,这和完全不检查 auth_date 没有区别。
没有 bot 令牌时的第三方验证路径
外部服务无法持有 bot 令牌,因此 Telegram 会用第二种方式对 initData 签名:使用可通过 Telegram 公钥验证的 Ed25519 signature。签名字符串有所不同——前缀为 <bot_id>:WebAppData,后跟换行符,再接上移除 hash 和 signature 后相同的排序 key=value 键值对。测试环境和生产环境使用的公钥不同。
当验证方不是 bot 的所有者时使用此方式。如果你确实持有令牌,不要用它替代 HMAC——HMAC 路径更简单,需要更换的密钥也更少。两种方式及其代码见:
references/initdata-verification.md。
SDK 的包名已变更,但组织主页仍显示旧名称
- 当前使用的包:
@telegram-apps/sdk(截至 2026-08-25 为 3.11.8),旁边还有@telegram-apps/sdk-react。 Telegram-Mini-AppsGitHub 组织仍主推tma.js和@tma.js/sdk,最新版本为 3.3.0——这是较早的名称,落后了八个次版本。- 该组织中的模板(
reactjs-template、nextjs-template、vuejs-template、solidjs-template)值得一读;那里还有四个模板已归档并标记为不受支持,Agent 复制搜索结果时,有约一半概率会选中其中一个。
这两个库都不是必需的。Mini App 接口是 window.Telegram.WebApp,由 telegram-web-app.js 提供;SDK 带来的是类型化访问和平台适配层,而非额外能力。
设备不是你的笔记本电脑
Mini App 会在 WebView 中渲染,键盘、标题栏和主页指示条都会占据屏幕空间。请读取视口字段,而不是想当然:
viewportHeight、viewportStableHeight、safeAreaInset、
contentSafeAreaInset、isFullscreen、isActive。在两个平台上,100vh 都不正确,而且错法各不相同。
主题来自 themeParams 和 colorScheme,会随着用户设置变化,而不是由你控制——应绑定这些值,而不是硬编码调色板。
references/viewport-and-platform.md。
宿主位于你的页面之上,设备也是如此
以下论文和文档的阅读日期为 2026-10-19。
威胁模型:Telegram 客户端是可信的,因为页面里的任何东西都无法拒绝它。 Priyanka 等人发表于 Don't Trust the Super-App: A Case Study of
Russia's Max(arXiv:2609.11814,2026-09-10)的研究以 MAX 超级应用为例,指出宿主可以捕获 Mini App 的 UI、读取和写入其本地存储、向其运行时注入任意 JavaScript、介入其网络流量,并以可实现静默冒充的方式控制其身份验证上下文;作者认为,这些能力源于超级应用在设计上天然拥有的权限。该研究针对的是 MAX,而不是 Telegram;请将其视为这类边界的示例。签名后的 initData 只能证明客户端为其内容背书,不能证明客户端是诚实的。因此,请将这一假设写入威胁模型,把秘密和决策留在服务端,并确保页面持有的数据有效期较短。
设备上的秘密应存入 SecureStorage,绝不要放入可读取的存储中。 Ciccotelli、Zappone 和 Di Pietro 在 TENET: Telegram Mini App
(in)security(arXiv:2608.17538)中分析了 37 个 Mini App:其中 30 个存在三类漏洞(明文存储、可恢复的加密、可重放的令牌),存储内容包括会话令牌和钱包助记词。自 Bot API 9.0 起,Telegram.WebApp.SecureStorage 使用 iOS Keychain 和 Android Keystore,静态加密,每位用户每个 bot 最多可存储 10 项;DeviceStorage 类似于 localStorage(每位用户每个 bot 为 5 MB),不适合存储秘密。短期会话令牌应存入前者,助记词则两者都不应存。
TON Connect 5.0 只是重新编号,但从 3.0 升级的变化可不止于此。 2026-10-11,@tonconnect/ui 和 @tonconnect/ui-react 升至 5.0.0,以便与 @tonconnect/sdk 共用一个主版本号;其代码与 ui 3.1.0 相同(5.0.1 是视觉修复版)。3.1.0 改变了错误处理程序收到的内容:UI 错误类会在 error.name 中报告其类名(以前是 "Error"),而已知的钱包拒绝操作会以类型化 SDK 错误(例如 UserRejectsError)传递给应用,而不再是 TonConnectUIError: Unhandled error。Mini App 检测功能(isInTMA、getTmaPlatform 等)已移入 @tonconnect/sdk,移动端模态框也会将 Telegram 和 Gram Wallet 显示为独立按钮。从 3.0.x 升级前,请先针对类型化错误进行匹配。
将结果返回给 bot
sendData(data)——用于通过键盘按钮打开的应用。它会关闭应用,并向 bot 发送web_app_data。bot 仍必须将其视为用户输入,因为它确实来自用户。answerWebAppQuery——用于通过内联按钮打开的应用,需使用已验证的initData中的query_id。该标识的有效期很短;如果服务端先执行耗时操作,就会发现它已经过期。- 付款通过 bot 使用 Stars 完成,走的是
telegram-bots所述的同一条pre_checkout_query→successful_payment流程。 用户在 Mini App 中点击付款;确认款项并不是 Mini App 的职责。
发布前检查
- 服务端从经过验证的
initData中获取身份,而不是使用客户端提供的 ID(§initDataUnsafe这个名称是有原因的)。 - 根据明确说明的时间窗口检查
auth_date,并将initData换成你自己的会话(§ 准确验证方式、§ 不要在每次请求时都验证)。 - 哈希比较采用恒定时间算法,并在构建校验字符串前移除
hash和signature。 - SDK 是
@telegram-apps/*,从组织复制的任何模板都不是已归档模板(§ SDK 的包名已更改)。 - 布局读取安全区域和视口字段,而不是使用
100vh(§ 设备不是你的笔记本电脑)。 - 威胁模型明确将宿主列为可信方,页面保存的任何机密都存放在
SecureStorage中,而不是localStorage或DeviceStorage(§ 宿主位于你的页面之上)。
触发词
更多会将你引导至此处的短语:"miniapp"、"WebAppData"、"@telegram-apps/sdk"。
从 0.5.4 版本的描述移至此处:"telegram web app"、"телеграм веб-апп"、"валидация initData"。