引言

自托管评论系统指的是网站或应用程序中,评论功能由网站自身或者网站所有者管理和控制的系统。评论数据存储在网站的服务器上,评论的显示、审核、编辑和删除等功能由网站管理员或所有者负责执行,而非依赖于第三方服务或平台。

Wordpress、Typecho 等动态博客,由于其动态特性,一般交由网站所有者自行购买云服务器后进行部署,其自带的评论系统也可称为自托管评论系统。除此以外,也有其他的独立评论系统可供自部署和托管,如 IssoCommento++。与之相对的是第三方评论系统,如 Disqus、Valine、Gitalk,常用于静态博客。

自托管评论系统的优势在于其控制权和隐私保护、定制型和灵活性、低依赖风险。网站所有者能够定制其样式,使其更符合网站整体的设计风格。同时,网站所有者拥有对评论的完整的内容和管理权限,不因第三方评论服务的不稳定性(如故障、政策管控被墙、跑路参考多说)影响内容,甚至导致评论丢失。

图 1. 红极一时的多说,于 2017 年上半年中关闭

其劣势在于,除已经集成了评论模块的动态博客系统外,部署独立评论系统有一定的技术实施成本,包括时间成本和资源成本。同时,如何进行数据安全保护、评论数据结构设计、防垃圾评论等,需要网站所有者进行深入研究。本文即针对防垃圾评论的手段进行粗略的探索,并阐述本博客使用的反垃圾评论的手段。

垃圾评论来源

要进行垃圾评论的反制,首先要弄清楚站点的垃圾评论的来源。大部分情况下,垃圾评论的内容为广告,由自动化的评论机器人生成和发布。

Wordpress、Typecho 等广泛使用的博客/评论系统,其请求评论的地址、表单字段和内容校验方式基本是固定的,广告推广者可以针对性地编写能自动爬取、自动提交评论的机器人,一旦识别到目标网站采用预设的博客系统,即自动生成表单,并往目标地址提交垃圾内容。

判断是否是 WP
填充 comment/author/email/url 字段
提交
图 2. 一个可能的机器人流程

实际上,只需要结合网络空间搜索系统(Shodan、Fofa)等,很容易收集指定类型的站点并形成列表,评论机器人不需要一个一个站点地爬取,只需要针对不同类型的系统,实现评论填充,再读取对应的网址列表即可完成批量评论。

Doghouse 是自建系统,并非以上常见的通用博客系统,其评论地址、字段等内容都和其他评论系统有所区别,但依然遭受了不少垃圾评论的骚扰。这说明,通用机器人能够自动识别表单,对可能的填充内容进行自动填充,而非仅仅针对通用博客系统。

验证码

验证码是一种常见的反制机器人的措施,常见的评论验证码有图片验证码和算数验证码,以及两者的结合。网络上有较多为 Wordpress 等评论添加验证码的教程,对于自建博客开发者来说,添加验证码功能也不算困难。

使用验证码可能面临的问题是:过于简单的验证码可能会被机器人识别,复杂的验证码可以有效阻止机器人,但对正常用户也是一种阻碍。

一些验证码 meme

图 3. 一些验证码 meme

为防止对正常用户造成困扰,选择合适的验证码尤为重要。此处的“合适”不仅指验证码模式,还包括验证码难度。除此以外,可以利用 SESSION:当用户第一次访问并评论时,需要验证码;一旦验证码输入正确,在 SESSION 过期前,可以不要求用户再次输入。

第三方服务

Akismet

熟悉 Wordpress 的朋友应该了解一款叫做 Akismet 的 WP 插件,用以过滤垃圾评论。Akismet 的原理是使用在线接口判断评论是否属于垃圾评论。除了 WP 以外,开发者也可以通过自己编写程序,调用 Akismet API 来获取评论审核结果,以获得处理参考。Akismet 也允许开发者通过 API 向其提交过滤失败的垃圾评论,以进一步扩充其数据库。

调用 Akismet 的 API 需要注册成为用户,选择免费的 Personal 方案即可获得 API Key。

调用 Akismet 的 Comment check API 较为简单,向接口提供 api_key 作为鉴权、blog 作为你的网站地址和 comment_content 作为评论内容即可。你也可以追加评论者名称、邮件地址、IP、UserAgent 等内容。当返回 true 时,判定为垃圾评论。

Akismet 在中国大陆的访问尚可,但仍有不少地区的电信网络无法访问。若使用 Akismet 服务,请确保自托管服务器网络能正常访问。

rest.akismet.com测试情况

图 4. rest.akismet.com 测试情况,来源:ITDOG

使用 Akismet 要考虑的问题之一是阻塞问题:如果评论、审核和呈现的过程是非异步的,当评论者提交评论后,需要等待 Akismet API 的查询结果出来后,程序才会判断是否进行放行,最终呈现结果到用户。如果由于网络原因导致查询时间过长,会影响用户感官。解决方案是将其改为异步的:提交后返回呈现和查询评论同时进行,此时评论为待审状态,待异步查询结果出来后,再根据结果更改评论的审核结果。

问题之二是其识别准确率,并不是所有的推广内容都能被准确识别的。比如这两条,Akismet Personal 反馈结果都是 false

无法被 Akismet 过滤的评论

图 5. 无法被 Akismet 识别的评论
一条是“投资机会”推广,另一条是乌克兰盲眼老太婆占星

这是我在部署评论蜜罐之前的最后两条被标记为待审的评论。我自己并未部署 Akismet,拿出来给它识别,没想到两条都未能识别为垃圾。至于网络上各种文章号称其“基本上能识别出 99% 以上的垃圾评论”,请自行判断。

除此以外,Akismet Personal 似乎无法识别一些带有侮辱、谩骂或者种族主义色彩的句子。对于博客所有者,尤其是中文博客的所有者,还需要额外面对一些涉政的言论过滤,此时 Akismet 就派不上用场了。唯一的解决办法是自己制定一套关键词过滤规则来过滤评论,在匹配到规则时,进行后续操作。

其他内容审核接口

可选择其他厂商的商用内容审核接口,为避打广告之嫌,此处就不列举了。缺点是要氪金。

自定规则

关键词过滤

为了弥补 Akismet 的缺陷,我们可以自定关键词过滤规则。Wordpress 提供了关键词过滤的功能,Typecho 也如此。不少通用博客系统都提供了相应的增强插件来实现关键词过滤。Doghouse 也实现了关键词过滤:

关键词过滤

图 6. 不会骂人都写不出来这些关键词

关键词的优点是:你能从一些具有明显敏感词汇的评论中提取词汇来自定义自己的审核,弥补第三方服务的缺陷;缺点是:你需要自行寻找或者填充敏感词库,这可能是个不断完善的过程,同时一些敏感词过滤容易误伤,比如“这周日你妈回家吗?”,好像是个正常的句子?此时你可以将匹配到敏感词后的后续动作改为“标记为待审”,由你自己做进一步判断。

非中文过滤

对于中文博客,很少出现完全不带汉字的评论。你可以利用这一点来检查评论。使用正则表达式:.*?[\\u4E00-\\u9FA5]+.*?。如果没有匹配到任何内容,说明该评论没有出现汉字,对中文博客大概率是垃圾内容。

动态加载评论表单

根据 第三方服务 所述,评论机器人通过识别网页表单来进行进一步的评论投递。一些机器人只会载入页面,通过网页源代码识别表单,而非完全载入页面包含的 js 脚本。你可以将加载评论表单的逻辑写在 js 内,以此来阻隔大部分评论机器人。

除此以外,你也可以做一些细节上的精进:比如不将真实的 form action 写在 form 中,而是通过 js 来接管表单的 submit 事件,再指定 action 地址来完成投递。

使用评论蜜罐配合 fail2ban 封禁 IP

虽然到这一步,采用以上的手段已经能够阻隔大部分垃圾评论机器人了,但对于自托管博客而言,被非法机器人访问博客也会造成服务器压力和流量消耗。此时我们可以用评论蜜罐的方式,配合 fail2ban 来封禁评论机器人的 IP。

注意到评论机器人通过识别网页表单来进行进一步的评论投递,此时我们可以不隐藏评论表单,而是通过提供虚假的 action 地址,来诱骗机器人往该地址投递。既然是虚假的地址,自然无法达成评论。而对于正常用户,js 接管 submit 能确保用户能正常投递。

我们可以设计一个虚假的 action 地址作为蜜罐地址:/honeyReply。当机器人试图访问该地址时,会在 nginx 的 access.log 内留下相应的日志:

120.92.*.* - - [28/Jun/2024:15:39:32 +0800] "POST /p/260/honeyReply HTTP/1.1" 400 5365 "https://qwq.me/tweet" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586"

配合 fail2ban,编写匹配规则:

failregex =
       # honeyReply
       ^<HOST>.*?"POST .*?\/honeyReply HTTP\/1\.\d".*?$

即可实现 IP 的封禁。

理论上,蜜罐地址可以不真实存在:当你不需要进一步处理这些命中了蜜罐的垃圾评论的时候。但如果你想进行进一步处理,比如想向 Akismet 提供这些垃圾评论,你可以自己在后端编写蜜罐地址对应的路由方法,以获取机器人的评论内容、昵称、邮箱、IP、UserAgent 等信息,而后进行处理。

fail2ban 也可以配合关键词审核等使用,只需:在评论失败时,返回指定的状态码,这样通过编写匹配规则来匹配 access.log 中的具有指定状态码和地址的记录,就可以实现封禁。

如果你的博客系统部署在 fail2ban 同一服务器上,可以通过向文件系统输出日志、编写 fail2ban 的过滤规则,再通过 fail2ban 来监听日志,也可实现封禁。使用场景是非实时场景:比如一开始某些评论处于待审状态,此时 fail2ban 是不知道该评论者是否是评论机器人的。经管理员审核后,标记为垃圾,输出到日志被 fail2ban 捕获,就实现了事后预防。