re: 从零开始的电报 CAPTCHA Bot 实验报告

跟风写了个时下最流行 (?) 的 Telegram 验证码机器人……
感觉还是能拿出来写点东西,所以就有了这篇实验报告。

实验目的

前段时间在基地台上开了个投票,没想到挺多人喜欢基于 Google reCAPTCHA 的验证方案。刚巧自己觉得有个能够满足 Telegram 使用人群隐私(怪)要求的方案,于是就……开坑了。

实验仪器设备

这次开坑也有体验 AWS Lambda 类函数计算平台以及 NodeJS 的意图,因此自然是使用跑在 Lambda 上的 NodeJS 啦。(不过后来实在是需要 State 我又懒得想办法兼容 Lambda 了……于是就 Docker 化了一下,跑在了小霖大佬给的一台到 Telegram Bot API 只需 3ms 的服务器上)

Lambda: AWS 提供的 Serverless 函数计算平台,可以通过一系列事件触发预置代码。
它按使用量计费,而不是像传统服务器一样持续计费且需要自己维护环境。

实验步骤

那么现在 lwl 要抓一个幸运的小孩来听我讲 BOT 幕后的技术细节(

设计交互方案

其实此前就已经有人做过 reCAPTCHA Bot 了,不过基本都是带与用户交互的后端实现,即让用户跳转进一个带有 reCAPTCHA 的页面,验证通过后将验证串直接通过用户浏览器传递到后端,后端验证后为用户解除封禁。

这样的流程固然相对流畅,但会出现一个问题——后端是完全可以拿到用户进行验证时的 IP 地址的,这无形中增加了一些隐私风险。为了规避这些问题,本文设计的 Bot 需要满足以下条件:

  • 用户 UID 不能在任何一环与用户 IP 相关联
  • 尽可能使用有公信力的服务,避免将用户信任建立在信任 Bot 运行者的基础上
  • 便于搭建,便于用户自己建立 Bot 实例和代码审计

根据上述需求,lwl 设计了这样的流程:
reCAPTCHA Bot 交互流程图
在该流程中,可以获取用户 IP 地址的相关方是 Github Page (Backend)reCAPTCHAPastebin,由于 Bot 后端通过 Telegram 中转服务器与用户交流,双方都无法获取对方端点信息,因此我们只需要阻止前述三方获取用户的 UID 就算是达到设计目标了。

对于 reCAPTCHA,大家大概都还算信任 Google,所以显然它有足够的公信力保证不会去获取我们发送到前端的 UID;而剩下两个相关方则需要稍微做一些处理。

数据传输

在开始这个项目的前几天,lwl 刚在知乎上看到了一个关于 JWT (Json Web Token) 技术的问题,当时我也没想到这么快就派上用场了。

JWT 的本质是一个字符串,包含信息头,Base64 后的有效荷载以及最后的签名。通过 JWT,我们得以将信息交由用户保存,免去服务端维护状态的麻烦,而同时签名也保证用户无法篡改服务端签署的信息。在本项目中,lwl 使用 JWT 来传递完成用户验证所必需的信息。

下面是 Bot 生成的一个 JWT 有效荷载部分解码之后的样子:

{
  "exp": 1546621718,
  "data": {
    "mid": 510,
    "uid": "10000",
    "gid": "-1001347897565",
    "gname": "reCAPTCHA%20Test"
  },
  "iat": 1546621118
}

对 JWT 技术有兴趣的话,可以试试 JWT.IO

嗯……就是一串 JSON,除了 data 是我们写入的数据外,还自动附上了签发时间 iat (Issued at) 和过期时间 ‘exp’ (Expiration Time) 这两个声明。因为 Lambda 平台无状态的需求,我们无法验证这个 JWT 是否已经被使用过,从而如果有效时间过长,在有效期内若用户被管理封禁,是可以通过 Bot 重新验证来强行解除限制的。因此 lwl 将 JWT 的过期时间设置在十分钟内,尽可能的避免发生这种情况。

可以看到,JWT 的 data 里包含了 miduidgidgname,这些参数分别对应消息 ID、用户 ID、群组 ID 和群组名。接下来,将 JWT 与机器人 Username 和 reCAPTCHA Sitekey 一起作为 URL Fragment 传递到前端页面,也就是前文提到的 Github Page。由于 Fragment (也就是 URL 里 # 后面的部分)仅指导浏览器动作,因此在浏览器发出请求时,这部分内容不会被作为查询串的一部分发送到 Github 后端,至此 Github 后端将无法获取 JWT,也就不存在关联 IP 和 UID 的可能性了。

随后前端页内 JS 将获取我们传递的信息,并动态渲染页面。待用户完成验证后,JWT 将原封不动的与 reCAPTCHA 回应的验证串合并成新的 JSON。此时前端页面会提供两类信息:直接将 JSON Base64 编码后形成的 /verify 指令,以及将 JSON 串以用户 UID 生成的密钥进行 AES 加密后上传到 LuckPerms 获取的 Pastebin 返回的剪贴板 ID。由于在这里我们使用了 UID 对 JSON 进行加密,因此 Pastebin 也将无法关联用户的 IP 和 UID。

最后,页面尝试以 Pastebin ID 为启动参数,拉起用户 Telegram 客户端,用户点击 /start 后,后端对 JSON 串内两项信息分别验证,若无误则利用 JWT 内的信息解除用户限制并删掉验证消息(防止群内被大量验证消息刷屏)。

一些坑

在制作 Bot 的过程中其实遇到了不少的坑。其中无状态设计牺牲了一些本来可以做到的功能,另外 Telegram 的 ‘/start’ 参数不能过长,使用 Pastebin 算是为了用户体验做出的妥协。

最大的坑莫过于前端页面自动拉起 Telegram 客户端了。虽然已经是照搬了 Telegram 官方 t.me 的代码,然而在 Android 上还是存在大量的无法拉起情况(前端菜鸡也修不好了所以就这样吧)。

另外,Github Pages 的前缀居然是不能自选的……于是无奈新开了一个组织用来占前缀 _(:з」∠)_

邀请

嗯……不知道为啥虽然上文的内容已经完全实现了 lwl 的预期目标,但是后来我还是给这 Bot 加了个邀请功能。邀请功能将验证预置在了用户加群以前,在群内使用 /invite 有效天数 就可以生成一个邀请链接,用户需要通过邀请链接验证才能获得真实群组 ID。

实验结果

前文也提到了,最后因为 Telegram Webhook Bot API 的延迟过大,以及新加的邀请功能对 State 的要求,lwl 最后还是给这个项目做了 Docker 化。目前这个项目已经开源在 Github,你可以使用 repo 内附带的 Docker Compose 或使用 Readme 内的方法在 Lambda 上搭建一个自己的 Bot 实例。

当然也可以用 lwl 架的实例 @reCAPTCHAxtooooon_Bot,嗯……当然这个不保证 SLA。

那么这篇实验报告到这里就结束啦(lwl 终于记起了他还是个技术博主的事),希望能给你未来的程序设计带来一些启发,有兴趣的话也欢迎来完善这个项目(虽然感觉上没啥可以加的了)。

- EOF -

18 条评论

昵称
  1. zkl2333

    围观巨佬

  2. Andy

    lwl tql (☆ω☆)

  3. FlyingSky

    tql
    测手速bot(不是)

  4. Indexyz

    lwl 好强啊(跑

    1. lwl12

      @Indexyz 铁头大佬才强(逃

  5. Troy

    关闭前十的大门

  6. gehhedj

    好强啊

  7. Otstar Lin

    大佬的更新变频繁了,好耶ヾ(≧∇≦*)ゝ

    1. lwl12

      @Otstar Lin 是错觉,刚好最近事情多而已(

  8. metowolf

    实验合格,在表格上签名就可以了(

    1. lwl12

      @metowolf 拉起汪爪按爪印(φ( ̄∇ ̄o)

  9. Axton

    tqltql

  10. 派兹

    关闭前三的大门

    1. 派兹

      @派兹 然而好像并没有关上(

  11. kn007

  12. Zohar

    LWL巨佬!

  13. johnpoint

    沙发沙发

  14. Sukka

    嗷呜~