我给博客评论做了一次身份改造

今晚我在后台翻评论,看到两条挨得很近的留言。

同一个邮箱,两个昵称。

第一条叫「阿远」,语气像个老读者,熟门熟路地接着上一篇的话题往下聊;

第二条叫「远哥」,内容也正常,但看起来像另一个人。

可系统不这么认为——头像一样,读者页把两条留言算在同一个人头上,资历标签从「新朋友」变成了「回头客」,回复通知也都进了同一个收件箱。

那一刻我突然意识到:我的博客一直没有一个真正的「评论者身份」。

它只有昵称,和一条随手填的邮箱。

所以动手改动了一些逻辑,往下看;
我尽量写详细,小伙伴们也可以参看。


一、问题不在昵称,在身份

最早做评论功能的时候,我的想法很简单:昵称让人填,邮箱用来收通知,头像用邮箱哈希去 Gravatar 或 QQ 头像里取。够用了。

后来功能一点点加上来:

  • 读者页要按人聚合留言
  • 排行榜要统计「谁来得最勤」
  • 回复邮件要称呼对方
  • 评论要显示「新朋友 / 回头客 / 常客」
  • 后台要判断这条评论是不是博主自己发的

这些功能都在问同一个问题:这个人是谁?

而我当时的答案,其实只是一条 author 字段。

于是同一邮箱换昵称之后,系统里出现了一堆互相矛盾的现象:

位置 看到的结果
评论列表 两个不同的昵称,像两个人
头像 同一个头像,像同一个人
读者页 合并成一个读者,名字取最近一条评论
资历标签 按邮箱累计,第二次就是「回头客」
回复邮件 发给同一个邮箱,但称呼的是当时那条评论的昵称

更麻烦的是,昵称和邮箱都没有验证。

任何人填一个邮箱,都能评论;任何人换个昵称,都能变成「另一个人」;甚至只要知道我的联系邮箱,把它填进评论框,就能触发一条我当时写的「自动过审」规则。

这不是昵称的问题。

是我从来没有认真定义过:邮箱、昵称、读者,这三者到底是什么关系。


二、我想要的规则

想清楚之后,我给自己定了四条规则:

  1. 邮箱是身份,昵称是展示名。
    同一个邮箱,就是同一个读者。昵称可以变,但身份不变。
  2. 首次评论自由,后续改名要付一点成本。
    第一次来,想叫什么就叫什么;但要换名字,得证明这个邮箱是你的。
  3. 所有展示口径必须统一。
    评论区、读者页、排行榜、邮件称呼,不能各叫各的。
  4. 身份可信,不等于内容可信。
    邮箱验证只能证明「这个邮箱是你的」,不能证明「这条评论没问题」。内容该审核还是要审核。

这四条看起来简单,但真做起来,牵扯到数据库、接口、前端交互、邮件模板,还有后台的审核流程。


三、第一版想法:把昵称锁死

我最开始的方案特别粗暴:同一个邮箱,第一次填什么昵称,以后就固定用什么。

技术上很好实现,查一下邮箱哈希,有记录就把 author 覆盖成历史昵称。

但我很快否掉了。

因为人会变。

有人一开始随手起了个中二昵称,过两年想换成真名;有人换了个马甲,想重新开始;也有人只是打错了一个字,难道要一辈子背着?

如果系统不允许改名,用户就会换一个邮箱。

而换邮箱的代价,是读者页、资历、历史留言全部断掉。这比「一人多名」更糟。

所以我需要的是:允许改名,但要让改名这件事变得可追溯、可验证。


四、最终方案:确认 + 验证

现在的流程是这样的。

1. 用户提交评论时,先做一次身份检测

前端在提交前,会带着邮箱和昵称去问后端一句:

GET /api/comment/identity?email=test@example.com&author=新昵称

后端查这张邮箱对应的身份记录:

  • 没有记录:首次评论,直接放行
  • 昵称一致:老读者,直接放行
  • 昵称不一致:返回冲突,前端弹窗

2. 弹窗只给两个选择

检测到该邮箱之前使用过昵称「阿*」。

  • 继续使用原昵称
  • 修改为新昵称(需要邮箱验证)

这里有个小细节:曾用名是打码的。

因为如果完整显示,任何人输入一个邮箱地址,就能查到别人曾经用过什么昵称。这算是一种隐私泄露。

3. 继续使用原昵称

用户点「继续使用原昵称」,前端提交一个隐藏字段:

identity_confirmed=1

后端收到后,不信任前端传回来的昵称,而是直接查身份表,把作者名覆盖成当前昵称。

这一点很重要。如果后端只是「相信用户点了确认」,那攻击者改一下请求参数,还是能随便换名字。

4. 修改昵称:邮箱验证码

如果用户选择改名,流程会长一点:

  1. 点击「发送验证码」
  2. 后端生成 6 位数字,存哈希,发到该邮箱
  3. 用户输入验证码
  4. 验证通过,签发一个短时 token
  5. 提交评论时带上 token
  6. 后端校验 token 绑定的邮箱和新昵称,通过后才允许改名

token 是一次性的,5 分钟有效。

改名成功后,系统会做两件事:

  • 更新身份表的当前昵称
  • 同步更新该邮箱下所有历史评论的昵称

这样读者页、排行榜、评论列表和邮件称呼,才会真正统一。


五、数据表:把「身份」单独拿出来

我没有直接在 comments 表上继续堆字段,而是拆了两张表。

1. 评论者身份表

CREATE TABLE comment_identities (
    email_hash      TEXT PRIMARY KEY,
    email           TEXT NOT NULL,
    current_author  TEXT NOT NULL,
    previous_author TEXT DEFAULT '',
    verified_at     TEXT DEFAULT '',
    created_at      TEXT NOT NULL,
    updated_at      TEXT NOT NULL
);

email_hash 是主键,和评论表里的哈希一致。

这样做的原因是:身份是长期的,评论是单次的。

一条评论可以删,可以拒绝,可以改状态,但「这个邮箱曾经叫什么名字」应该留下来。

2. 邮箱验证码表

CREATE TABLE email_codes (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    email_hash  TEXT NOT NULL,
    email       TEXT NOT NULL,
    code_hash   TEXT NOT NULL,
    new_author  TEXT NOT NULL,
    purpose     TEXT NOT NULL,
    expires_at  INTEGER NOT NULL,
    attempts    INTEGER DEFAULT 0,
    used_at     TEXT DEFAULT '',
    ip          TEXT DEFAULT '',
    created_at  TEXT NOT NULL
);

验证码只存哈希,不存明文。

每次发送前,会先删掉该邮箱旧的未使用验证码,保证「只有最新一封邮件有效」。

3. 评论表增加两个字段

ALTER TABLE comments ADD COLUMN identity_action TEXT DEFAULT '';
ALTER TABLE comments ADD COLUMN identity_note TEXT DEFAULT '';
  • identity_action:first / keep / rename
  • identity_note:给后台看的说明

我特意没有把身份说明塞进 ai_meta。

因为 AI 审核会整段覆盖 ai_meta:

ai_meta = json.dumps({"decision": ..., "reason": ...})

如果身份说明也放在里面,AI 一跑就没了。

不同来源的数据,应该有不同的字段。


六、验证码的细节,比想象中多

验证码看起来很简单,但真正写起来,细节最多。

1. 限频

我设了四道限制:

维度 限制
同一邮箱 60 秒 1 次
同一邮箱 10 分钟 5 次
同一邮箱 24 小时 10 次
同一 IP 1 小时 10 次

不是为了防黑客,更多是为了防「手滑」和「骚扰」。

否则任何人输入别人的邮箱,都能反复给人家发验证码。

2. 只允许已有评论记录的邮箱发送

这一点是我后来加的。

如果任何邮箱都能发,那这个接口就成了一个免费的邮件发送器。虽然有限频,但依然不合适。

现在只有「这个邮箱确实在本站评论过,而且新昵称和旧昵称不一样」时,才允许发送。

3. 验证码本身

  • 6 位数字
  • 10 分钟有效
  • 最多尝试 5 次
  • 使用后立即作废
  • 哈希存储

这些参数不复杂,但少了任何一个,都会留下口子。

4. 邮件模板

验证码邮件没有单独写一套 HTML。

它复用了现有的邮件外壳:

def verify_code_notice(*, blog_name, site, code, ttl_minutes=10):
    body = (...)
    page = _shell(
        headline="验证你的邮箱",
        meta_html="",
        body=body,
        foot_note=...,
    )
    return text, page

标题、分割线、内容卡、页脚、发件人署名,全部和评论通知一致。

我不想让用户收到一封「看起来不像这个博客发来的」验证码邮件。


七、改名之后,展示要真正统一

改名成功后,执行一条更新:

UPDATE comments
SET author = ?
WHERE email_hash = ?
  AND is_owner = 0
  AND is_ai = 0

为什么不动态展示时再替换?

因为动态替换意味着:评论列表、读者页、排行榜、邮件、后台,每一处都要记得查身份表。只要漏掉一处,就会出现「评论区叫新名字,读者页叫旧名字」的割裂。

直接更新评论表,最简单,也最不容易出错。

当然,旧昵称没有丢。身份表里留了 previous_author,后台可以看到改名历史。


八、顺手修掉一个更危险的问题

写这套功能的时候,我回头看了一眼原来的审核逻辑,发现了一行很危险的代码:

status = "approved"

本文采用 CC BY-NC-SA 4.0 许可协议

评论 1

豪子
豪子 博主 重庆市

大家对于,邮箱评论是如何处理的?可以一起探讨!

写下你的想法… 写评论 收起评论框
0 / 2000