页面加载中
博客快捷键
按住 Shift 键查看可用快捷键
ShiftK
开启/关闭快捷键功能
ShiftA
打开/关闭中控台
ShiftD
深色/浅色显示模式
ShiftS
站内搜索
ShiftR
随机访问
ShiftH
返回首页
ShiftL
友链页面
ShiftP
关于本站
ShiftI
原版/本站右键菜单
松开 Shift 键或点击外部区域关闭
互动
最近评论
暂无评论
标签
寻找感兴趣的领域
暂无标签
    0
    文章
    0
    标签
    8
    分类
    10
    评论
    128
    功能
    深色模式
    标签
    JavaScript12TypeScript8React15Next.js6Vue10Node.js7CSS5前端20
    互动
    最近评论
    暂无评论
    标签
    寻找感兴趣的领域
    暂无标签
      0
      文章
      0
      标签
      8
      分类
      10
      评论
      128
      功能
      深色模式
      标签
      JavaScript12TypeScript8React15Next.js6Vue10Node.js7CSS5前端20
      随便逛逛
      博客分类
      文章标签
      复制地址
      深色模式
      AnHeYuAnHeYu
      Search⌘K
      博客
        暂无其他文档

        评论个人信息表单自动化填充与匿名评论功能实现

        文章摘要
        本文介绍了一个名为“auto-identity.js”的脚本,旨在在不泄露用户隐私的情况下,为网站提供匿名评论功能。该脚本通过收集浏览器的特征(如userAgent、语言、屏幕尺寸等)生成唯一的数字指纹,以此作为用户的身份标识。脚本提供了指纹模式和随机模式两种身份生成方式,并通过Canvas指纹技术提高指纹的稳定性。用户信息(如昵称、邮箱)由脚本自动生成,并存储在localStorage中以便下次复用。此外,脚本还支持自定义配置,如选择器、域名、按钮文字等,并采用无侵入式DOM操作实现匿名评论按钮的插入。该脚本旨在保护用户隐私,无需网络请求,支持离线生成,适用于希望匿名留言的用户。
        January 8, 202614 分钟 阅读180 次阅读

        碎碎念

        前几天在 Twikoo 群里,有朋友问能不能做个不用填昵称和邮箱的评论框?我第一反应是:如果不填,怎么区分不同的用户呢?这让我开始思考一个有意思的问题:如何在保护隐私的同时,还能识别不同的访客?

        于是我就写了这个小脚本,今天就跟大家分享一下我的思路和实现过程。

        功能展示

        配置

        我首先考虑的是要让这个工具足够灵活,能适应不同的网站。所以定义了一个配置对象 DEFAULT_CONFIG:

        js
        const DEFAULT_CONFIG = {
          storageKey: 'autoIdentity',           # 存在 localStorage 里的键名
          stableSalt: 'offline-stable-v1',      # 加在指纹里的“盐”,增加唯一性
          # 理论上来说autoApplyOnLoad和autoInjectAnonymous是二选一的
          autoApplyOnLoad: false,               # 页面加载后是否自动填充
          autoInjectAnonymous: true,            # 是否自动插入“匿名评论”按钮
          selectors: {                          # 昵称和邮箱输入框的CSS选择器,你可以将其改为类选择器或者是id选择器
            name: 'input[name="nick"]',
            email: 'input[name="mail"]',
          },
          identityMode: 'fingerprint',          # 身份模式 fingerprint或random
          domainOverride: null,                 # 强制使用的邮箱域名,null则自动获取
          anonymousParentSelector: 'div.tk-row.actions',  # 按钮要插入到哪个父容器里
          anonymousLabelText: '匿名评论',        # 按钮上显示的文字
          anonymousInsert: {                    # 按钮插入的位置
            anchorSelector: 'div.tk-row.actions > .el-button:nth-of-type(1)', # 锚点元素
            position: 'before',                 # 插入在锚点的 'before' 或 'after'
          },
        };

        选择身份模式

        核心问题来了:怎么生成一个“稳定”的身份?我设计了两种模式:

        指纹模式(默认)

        当 identityMode 是 fingerprint时,通过collectFingerprint() 函数收集浏览器特征:我将浏览器的 userAgent、语言、屏幕尺寸、时区、域名、Canvas 指纹等特征拼成一个字符串,再加上 stableSalt,用 fnv1a() 哈希函数生成一个唯一的数字。同一台设备、同一个浏览器,这个数字基本是稳定的。

        js
        # 默认指纹模式
        const DEFAULT_CONFIG = {
          # ...其他配置
          identityMode: 'fingerprint',  # ← 指纹模式为默认设置
          # ...
        };
        
        function normalizeIdentityMode(mode) {
          return mode === 'random' ? 'random' : 'fingerprint';  # ← 标准化函数,默认返回fingerprint
        }
        # ...
        # collectFingerprint收集浏览器特征
        function collectFingerprint() {
          if (fingerprintCache) return fingerprintCache;
          const nav = window.navigator || {};
          const scr = window.screen || {};
          const intl = Intl.DateTimeFormat().resolvedOptions?.() || {};
          const uaData = nav.userAgentData;
        
          # 收集userAgent
          const ua = uaData?.brands
            ? uaData.brands.map(({ brand, version }) => `${brand}:${version}`).join(',')
            : nav.userAgent;
          const platform = uaData?.platform || nav.platform;
        
          # 拼接各种特征
          const parts = [
            ['ua', ua],
            ['lang', nav.language],
            ['platform', platform],
            ['screen', `${scr.width}x${scr.height}`],
            ['tz', new Date().getTimezoneOffset()],
            ['tzname', intl.timeZone],
            ['domain', window.location.hostname || 'offline'],
            ['salt', CONFIG.stableSalt],  # ← 添加stableSalt
          ];
        
          # 添加Canvas指纹
          const canvasHash = canvasFingerprint();
          if (canvasHash) parts.push(['canvas', canvasHash]);
        
          fingerprintCache = parts.map(([k, v]) => `${k}:${v ?? ''}`).join('|');
          return fingerprintCache;
        }

        随机模式

        有时候用户可能想要完全随机的身份。这时候就用makeRandomSeed()函数生成一个随机数(或者回退到 Math.random()),这样每次都会生成全新的身份。
        这一串数字既决定昵称,也参与邮箱前缀的生成,既稳又不需要服务器。

        js
        #  随机种子生成
        function makeRandomSeed() {
          try {
            const arr = new Uint32Array(1);
            window.crypto.getRandomValues(arr);  # ← 使用加密安全的随机数
            return arr[0];
          } catch (err) {
            # 如果crypto API不可用,回退到Math.random()
          }
          return Math.floor(Math.random() * 0xffffffff);  # ← 回退方案
        }
        # 随机模式的身份生成逻辑
        function makeIdentity() {
          # ...其他代码
          
          const hashSource = CONFIG.identityMode === 'random'
            ? `${makeRandomSeed()}|${Date.now()}|${CONFIG.stableSalt}`  # ← 随机模式
            : `${collectFingerprint()}|${window.location.hostname}`;
          
          const hash = fnv1a(hashSource);  # ← 生成哈希数字
          # ...
        }

        使用Canvas指纹提高识别稳定性

        为了提高指纹的稳定性,我加了一个canvasFingerprint()函数。原理很简单:在不同的设备、浏览器上,Canvas 渲染出来的图像会有微小差异。所以我在离屏canvas上画几块矩形、文字和混合模式,拿到 toDataURL 和局部像素,再哈希一次。如果浏览器不支持 Canvas 或者用户禁用了,这个函数会静默失败,不影响主要功能。

        js
        function canvasFingerprint() {
          # 创建一个离屏 Canvas
          const canvas = document.createElement('canvas');
          const ctx = canvas.getContext('2d');
          
          # 画一些图形和文字
          ctx.fillRect(0, 0, 120, 50);
          ctx.fillText('auto-id', 8, 10);
          # ... 更多绘制操作
          
          # 获取数据并进行哈希
          const dataURL = canvas.toDataURL();
          canvasHashCache = fnv1a(dataURL).toString(36);
          return canvasHashCache;
        }

        昵称和邮箱的生成

        有了指纹数字,接下来就是生成人类可读的身份信息。

        昵称生成

        我准备了两个词库:adjectives(形容词)和 animals(动物)。通过哈希值选出一个形容词 + 一个动物,再拼一个 100~999 的数字,比如 brisk-otter-428。

        js
        # 昵称生成
        function buildName(hash) {  # ← 接收哈希数字作为参数
          const adj = pick(adjectives, hash);  # ← 用哈希选择形容词
          const noun = pick(animals, hash >>> 5);  # ← 用哈希移位后选择动物名
          const number = (hash % 900) + 100;  # ← 用哈希生成数字部分
          return `${adj}-${noun}-${number}`;
        }

        邮箱生成

        邮箱要处理域名问题。我写了个getRootDomain()函数,能正确处理像xxx.co.uk这样的二级域名后缀,当然也支持 domainOverride 强行指定。邮箱本地部分用 slugifyName 清洗昵称,再附加哈希的后四位如:brisk.otter.428.1xk9@example.com。这样既可读,又能减少撞名。

        js
        # 获取根域名
        function getRootDomain(hostname) {
          if (!hostname) return 'example.com';
          const parts = hostname.split('.').filter(Boolean);
          if (parts.length <= 1) return hostname;
          const tld = parts[parts.length - 1];
          const sld = parts[parts.length - 2];
          const commonSecond = ['co', 'com', 'net', 'org', 'gov', 'edu', 'ac'];
        
          # 处理像xxx.co.uk这样的二级域名后缀
          if (tld.length === 2 && commonSecond.includes(sld) && parts.length >= 3) {
            return parts.slice(-3).join('.');  # ← 取后三段,如 xxx.co.uk
          }
          return parts.slice(-2).join('.');  # ← 正常取后两段
        }
        # 清洗昵称
        function slugifyName(name) {
          return name
            .toLowerCase()
            .replace(/[^a-z0-9]+/g, '.')  # ← 非字母数字替换为点
            .replace(/\.+/g, '.')         # ← 合并多个点
            .replace(/^\.+|\.+$/g, '');   # ← 去掉首尾的点
        }
        # 邮箱前缀生成
        function makeIdentity() {
          # ...之前代码
          
          const hash = fnv1a(hashSource);
          const name = buildName(hash);
          const domain = CONFIG.domainOverride || getRootDomain(window.location.hostname);
          const shortHash = hash.toString(36).slice(-4);  # ← 从哈希中提取短标识
          const emailLocal = `${slugifyName(name)}.${shortHash}`;  # ← 邮箱本地部分包含哈希
          const email = `${emailLocal}@${domain}`;
          
          # ...
        }

        身份持久化

        生成好的身份会通过 safeWrite 存到 localStorage,下次直接复用,确保同一浏览器看到的是同一个“马甲”。

        js
        function safeWrite(identity) {
          try {
            if (isValidIdentity(identity)) {
              window.localStorage.setItem(CONFIG.storageKey, JSON.stringify(identity));
            }
          } catch (err) {
            # 隐私模式或存储已满时静默失败
          }
        }

        使用DOM操作实现无侵入式注入

        这一部分最难的可能就是是如何在不破坏原有页面的情况下,添加“匿名评论”按钮。

        找到合适的位置

        通过injectAnonymousEntry()函数,我可以在评论表单的操作区域插入按钮:

        js
        # 查找父容器和锚点元素
        function injectAnonymousEntry(options = {}) {
          const {
            parentSelector = CONFIG.anonymousParentSelector,
            # ...其他选项
            anchorSelector = CONFIG.anonymousInsert.anchorSelector,
            # ...
          } = options;
        
          # 查找父容器
          const parent = typeof parentSelector === 'string'
            ? document.querySelector(parentSelector)  # ← 通过选择器查找父元素
            : parentSelector;
          if (!parent) return null;
        
          # 查找锚点元素
          const anchor = resolveAnchorChild(parent, anchorSelector);  # ← 在父容器内查找锚点
          # ...
        }
        # 解析锚点子元素
        function resolveAnchorChild(parent, anchorSelector) {
          if (!parent) return null;
          if (anchorSelector) {
            const el = parent.querySelector(anchorSelector);  # ← 通过选择器查找锚点
            if (el) return el;
          }
          return null;
        }

        克隆现有按钮

        为了保持样式一致,我选择克隆页面上已有的按钮,克隆的div的样式会和锚点元素保持一致:

        js
        # 选择模板节点
        function pickTemplateNode(parent, anchor) {
          # 优先使用锚点元素本身或内部的按钮/链接
          if (anchor) {
            if (anchor.tagName === 'BUTTON' || anchor.tagName === 'A') return anchor;
            const inside = anchor.querySelector('button, a');
            if (inside) return inside;
            if (anchor.firstElementChild) return anchor.firstElementChild;
          }
          
          # 回退方案:在父容器中查找第一个按钮或链接
          const firstButton = parent.querySelector('button, a');
          if (firstButton) return firstButton;
          
          # 最后使用父容器的第一个子元素
          return parent.firstElementChild;
        }

        修改文字并绑定事件

        js
        # 查找文本叶节点
        function findTextLeaf(root) {
          const queue = [root];
          while (queue.length) {
            const el = queue.shift();
            if (!el || el.nodeType !== 1) continue;
            const hasChildren = el.children && el.children.length > 0;
            const text = (el.textContent || '').trim();
            if (text && !hasChildren) return el;  # ← 找到有文本内容且无子元素的叶子节点
            if (hasChildren) queue.push(...el.children);
          }
          return root;
        }
        const labelEl = findTextLeaf(clone) || clone;
        labelEl.textContent = CONFIG.anonymousLabelText;  # 改为“匿名评论”
        
        clone.addEventListener('click', (event) => {
          event.preventDefault();
          const identity = makeIdentity();  # 生成身份
          applyIdentity({ identity });      # 填充表单
        });

        智能插入DIV

        为了避免侵入式改动,我做了几个防护:用 findTextLeaf 在克隆的按钮里找到叶子节点,安全地覆盖文案。
        pickTemplateNode会尽量挑选按钮/链接作为模板,如果找不到就兜底用第一个子节点。
        插入位置支持 before/after,没找到锚点时就插在父节点开头,确保页面不会破坏原有布局。
        所有事件都绑定在克隆节点上,原按钮保持原状,样式也继承父元素已有的 CSS。

        如何接入

        最简单的用法就是在页面里引入脚本(如 index.html 已经做的那样):

        html
        <script src="./auto-identity.js"></script>

        如果需要自定义配置,可以在它前面写配置:

        html
        <script>
        window.AUTO_IDENTITY_CONFIG = {
          identityMode: 'random',          # 使用随机模式
          autoApplyOnLoad: true,           # 自动填充(适合纯匿名场景)
          selectors: {
            name: '#comment-author',       # 根据你的页面调整选择器
            email: '#comment-email',
          },
          domainOverride: 'myblog.dev',    # 自定义邮箱域名
          anonymousLabelText: '隐身评论',   # 自定义按钮文字
        };
        </script>
        <script src="./auto-identity.js"></script>

        一些设计思考

        为什么要在前端生成?

        那么首当其冲的是,因为想要保护自己的隐私才会去使用这个随机昵称和邮箱,所以所有信息都在用户浏览器里处理,不发送到服务器;其次是不需要网络请求,瞬间完成,即使断网也能生成身份。

        关于指纹的争议

        我知道浏览器指纹涉及隐私问题,所以我:提供了随机模式作为替代,所有指纹数据都在本地处理,不上传,用户可以随时清除localStorage,只能说是最大程度的去保护浏览用户的隐私。

        写在最后

        这段小脚本是为了给 Twikoo 群的群友写的,可以让访客在想匿名留言时多了一个一键入口。实现上尽量保持无侵入、可配置、离线生成,希望能给同样折腾评论区的朋友一点参考。后续如果还有需求(比如生成头像 seed、或接入服务端白名单),再继续迭代吧。
        代码下载

        最后更新于 January 17, 2026
        On this page
        暂无目录