惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
小众软件
小众软件
MongoDB | Blog
MongoDB | Blog
Hugging Face - Blog
Hugging Face - Blog
Jina AI
Jina AI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Stack Overflow Blog
Stack Overflow Blog
L
LangChain Blog
大猫的无限游戏
大猫的无限游戏
量子位
A
About on SuperTechFans
G
Google Developers Blog
雷峰网
雷峰网
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
IT之家
IT之家
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园_首页
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Vercel News
Vercel News
V
Visual Studio Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
U
Unit 42
Apple Machine Learning Research
Apple Machine Learning Research

水拍石

赛力斯和华为分手了 《当代政治哲学》简单梳理 – 水拍石 未来与命运 – 水拍石 为你的发言付费 – 水拍石 全球高利率及高主权债务下的经济压力 – 水拍石 股市横盘 – 水拍石 博客动静的选择 – 水拍石 休谟《人性论》导读手册 – 水拍石 《现代世界体系》简评 – 水拍石 能力天花板与命运 – 水拍石 博客与生活 – 水拍石 中国股市稳定调控核心机构梳理报告 – 水拍石 阿里财报AI相关数据及新闻报道的梳理 – 水拍石 美国债务简记 – 水拍石 信仰之爱与理性之爱 – 水拍石 出售了savouer.com域名 – 水拍石 分层 – 水拍石 大憨 – 水拍石 冠冕与王 – 水拍石 盲从 – 水拍石 新疆自驾游略记 – 水拍石 近日杂记之20260723 – 水拍石 AI的转变期终于来了 – 水拍石 错位 – 水拍石 癫狂 – 水拍石 第十章 超越主义之争:经济学与人类合作的未来 – 水拍石 第九章 算法、债务与新垄断:21世纪经济的新问题 – 水拍石 第八章 国家里面的市场:中国制内市场的历史逻辑 – 水拍石 第七章 资本主义的镜子:马克思主义的洞见与歧途 – 水拍石 第六章 交易为什么这么难:制度经济学与真实世界 – 水拍石
wordpress评论增加邮箱验证与中文检测 – 水拍石
作者 · 2026-09-07 · via 水拍石

作为个人站长,最烦的就是闲的蛋疼刷流量,或刷垃圾评论行为。

博友们用尽各种验证方式想挡住机器人刷评论,但都效果一直不令人满意,很难杜绝。我跟着大佬们的脚步,亦步亦趋,填字母、算数验证码、图片验证码都用过,还是挡不住这些闲人。所有验证方法只是给垃圾评论者增加了麻烦,并不能给他们装上锁,如果他们不怕麻烦,或者有高超技术绕过这些麻烦,那垃圾评论就又来了。

前短时间忘了在那位大佬的博客上看到了首次评论时邮箱需要验证的功能,觉得很好,于是今天让AI给加上了。这种验证方法虽然麻烦点,但好处是只需要验证一次,成功后永远不用再验证,这样其实比填验证码或拖动图片更友好。

增加首次邮箱验证的同时,也增加了评论中文检测功能,这是纯防国外机器人的。

当然,以上两种障碍也不一定能挡住垃圾评论,只要让它们减少点我就心满意足了。

在这里向各位道个歉,给大家添麻烦了,克服下填完验证码,以后将享受没有垃圾的评论。

下面是AI整理的开发经验,对于懂代码的大佬可能价值很低,但对于像我这样完全不懂代码的人来说,或许可以不用重复踩坑,也能省点token。


一、功能总览

功能 作用 拦防对象 对真实用户打扰 默认状态
邮箱首次验证 新邮箱首次评论须输入邮箱验证码,通过后该邮箱永久免验 批量灌水脚本、SEO 群发、随机邮箱 bot 仅第一次,约 30 秒 后台开关,默认关
评论必须含中文 评论内容无汉字则无法提交 纯外文/乱码灌水 几乎为零(正常中文评论无感) 后台开关,默认开

为什么邮箱验证比算术验证码强:算术码 bot 用正则/OCR 几行代码就解;邮箱验证要求”真实邮箱 + 收信 + 提取码 + 回填”,灌水脚本不会为个人博客做这套。加上”首次验证、终身免验”,真人成本一次、bot 每次都要新邮箱走全流程。

二、邮箱首次评论验证

2.1 核心设计思路

  1. 不新建自定义表,全部用 WP 原生 option + transient(自动过期,免清理);
  2. 发码在提交拦截钩子里做pre_comment_on_post),发码前先检查中文(另一个钩子优先级更高);
  3. 评论内容不丢失:首次提交时把昵称/邮箱/网址/评论内容存成”草稿”(transient),验证面板用 hidden 字段回显,验证通过后回填 $_POST 让 WP 正常入库;
  4. 邮箱不暴露:回跳 URL 只带随机 token,不出现邮箱;页面显示掩码邮箱 l***@qq.com
  5. 验证码存哈希wp_hash() 存储,校验用 hash_equals(),防数据库侧读明文;
  6. 回跳统一带锚点 + 一次性状态提醒:所有评论提交路径(验证/默认流程)回跳 文章地址#comment-form-area,提醒用 transient 传递,URL 干净、不被页面缓存污染。

2.2 整体流程图

flowchart TD
    A[访客填写评论表单
点击提交] --> B{前端 JS
是否含中文?}
    B -- 否 --> B1[气泡提示
必须包含中文
内容不丢失]
    B -- 是 --> C[POST wp-comments-post.php]
    C --> D{后端中文检测
pre_comment_on_post pri 1}
    D -- 无中文 --> D1[wp_die 提示
不发码不浪费邮件]
    D -- 含中文 --> E{邮箱验证开启
且未登录?}
    E -- 否 --> F[WP 默认入库]
    E -- 是 --> G{该邮箱
已验证过?}
    G -- 是 --> F
    G -- 否 --> H{表单带
email_code?}
    H -- 否 --> I[发6位验证码邮件
存验证码+草稿 transient]
    I --> J[302 回跳
?email_verify=token#comment-form-area]
    J --> K[评论区渲染验证面板
标题+防骚扰说明+验证码输入]
    K --> L[用户输入验证码提交]
    H -- 是 --> M{校验 hash_equals}
    M -- 错 --> N[扣次数
剩0次则作废]
    N --> J
    M -- 对 --> O[回填草稿到 $_POST
记入已验证名单
删除 transient]
    O --> F
    F --> P[comment_post 钩子
/ comment_post_redirect]
    P --> Q[写一次性状态提醒 transient
approved/hold]
    Q --> R[302 回跳 文章地址#comment-form-area]
    R --> S[评论区顶部显示
评论已发布 / 已提交待审核]

2.3 数据存储设计

存储 内容 有效期
option nickel_verified_emails email => 验证时间戳 的关联数组(已验证名单) 永久
transient nickel_vc_{md5(email)} 验证码 hash、发送时间 sent_ts(60s 冷却)、token 10 分钟
transient nickel_vp_{token} 评论草稿:post_id/name/email/url/comment/parent/attempts(5)/created 10 分钟
transient nickel_comment_notice_{post_id} 提交后状态提醒:state(approved/hold)、t 10 分钟,读取即删

2.4 关键钩子一览

钩子 优先级 函数 作用
pre_comment_on_post 1 (匿名) 中文检测(先于发码)
pre_comment_on_post 5 nickel_ev_check 未验证邮箱→发码/校验;已验证→放行
comment_post 5 nickel_ev_after_post 验证成功路径:写提醒 + 清缓存 + 回跳
comment_post_redirect 10 nickel_ev_comment_redirect 默认流程全部路径:写提醒 + 统一回跳评论区
template_redirect 5 nickel_ev_resend ?resend=1 重新发送验证码(60s 冷却)
(渲染) nickel_ev_panel_html comments.php 在待验证状态输出验证面板

2.5 核心实现代码

(1)发码 + 建草稿(首次提交拦截)

add_action( 'pre_comment_on_post', 'nickel_ev_check', 5 );
function nickel_ev_check( $post_id ) {
  if ( ! nickel_ev_enabled() || is_user_logged_in() ) return;

  $email = strtolower( trim( isset( $_POST['email'] ) ? sanitize_email( $_POST['email'] ) : '' ) );
  if ( ! $email ) return;                        // 空邮箱交给 WP 自带报错
  if ( nickel_ev_is_verified( $email ) ) return; // 已验证 → 直接放行(每次仅一次数组比对)

  $code = isset( $_POST['email_code'] ) ? trim( (string) $_POST['email_code'] ) : '';
  if ( '' !== $code ) {
    nickel_ev_try_verify( $post_id, $email, $code ); // 第二次提交:校验验证码
    return;
  }
  nickel_ev_send_and_pending( $post_id, $email );    // 首次提交:发码 + 建草稿
}

function nickel_ev_send_and_pending( $post_id, $email ) {
  $key  = 'nickel_vc_' . md5( $email );
  $prev = get_transient( $key );
  $now  = time();

  // 60 秒冷却:刚发过 → 回待验证页并提示
  if ( $prev && ! empty( $prev['sent_ts'] ) && $now - $prev['sent_ts'] < 60 ) {
    wp_safe_redirect( nickel_ev_redirect_url( $post_id, array( 'email_verify' => $prev['token'], 'error' => 'cooldown' ) ) );
    exit;
  }

  $code = (string) wp_rand( 100000, 999999 );
  if ( ! nickel_ev_send_code( $email, $code ) ) {   // 发信失败
    wp_safe_redirect( nickel_ev_redirect_url( $post_id, array( 'email_verify' => 'mailfail' ) ) );
    exit;
  }

  $token = wp_generate_password( 20, false );       // 随机 token,放 URL,不暴露邮箱
  set_transient( $key, array( 'hash' => wp_hash( $code ), 'sent_ts' => $now, 'token' => $token ), 600 );

  // 评论草稿:原样保存(不剥 HTML),面板 hidden 回显,入库时由 WP 统一清洗
  $pending = array(
    'post_id' => $post_id,
    'name'    => isset( $_POST['author'] ) ? wp_unslash( $_POST['author'] ) : '',
    'email'   => $email,
    'url'     => isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : '',
    'comment' => isset( $_POST['comment'] ) ? wp_unslash( $_POST['comment'] ) : '',
    'parent'  => isset( $_POST['comment_parent'] ) ? absint( $_POST['comment_parent'] ) : 0,
    'attempts'=> 5,
  );
  set_transient( 'nickel_vp_' . $token, $pending, 600 );

  wp_safe_redirect( nickel_ev_redirect_url( $post_id, array( 'email_verify' => $token ) ) );
  exit;
}

(2)校验验证码(第二次提交)

function nickel_ev_try_verify( $post_id, $email, $code ) {
  $key     = 'nickel_vc_' . md5( $email );
  $stored  = get_transient( $key );
  $token   = isset( $_POST['nickel_ev_token'] ) ? preg_replace( '/[^a-zA-Z0-9]/', '', (string) $_POST['nickel_ev_token'] ) : '';
  $pending = $token ? nickel_ev_get_pending( $token ) : false;

  // 状态缺失/过期,或邮箱与草稿不一致 → 失效
  if ( ! $stored || ! $pending || strtolower( trim( $pending['email'] ) ) !== $email ) {
    wp_safe_redirect( nickel_ev_redirect_url( $post_id, array( 'email_verify' => 'expired' ) ) );
    exit;
  }

  if ( isset( $stored['hash'] ) && hash_equals( $stored['hash'], wp_hash( $code ) ) ) {
    // 通过:回填字段让 WP 正常入库;记标记供 comment_post 钩子写成功提醒
    $_POST['author'] = $pending['name'];
    $_POST['email']  = $pending['email'];
    $_POST['url']    = $pending['url'];
    $_POST['comment']= $pending['comment'];
    $_POST['comment_parent'] = $pending['parent'];
    delete_transient( 'nickel_vp_' . $token );
    delete_transient( $key );
    nickel_ev_mark_verified( $email );                       // 记入已验证名单
    $GLOBALS['nickel_ev_just_verified'] = $email;            // 供 comment_post 钩子识别
    return;
  }

  // 错误:扣次数
  $attempts = (int) $pending['attempts'] - 1;
  if ( $attempts <= 0 ) {                                    // 5 次耗尽 → 作废
    delete_transient( 'nickel_vp_' . $token );
    delete_transient( $key );
    wp_safe_redirect( nickel_ev_redirect_url( $post_id, array( 'email_verify' => 'expired' ) ) );
    exit;
  }
  $pending['attempts'] = $attempts;
  set_transient( 'nickel_vp_' . $token, $pending, 600 );
  wp_safe_redirect( nickel_ev_redirect_url( $post_id, array( 'email_verify' => $token, 'error' => 'wrong' ) ) );
  exit;
}

(3)提交后统一回跳 + 一次性状态提醒(重要:解决”回跳文章头部 / 提醒丢失”)

// 写/读一次性提醒
function nickel_ev_set_notice( $post_id, $state ) {
  set_transient( 'nickel_comment_notice_' . $post_id, array( 'state' => $state, 't' => time() ), 600 );
}
function nickel_ev_get_notice( $post_id ) {
  $key = 'nickel_comment_notice_' . $post_id;
  $n   = get_transient( $key );
  if ( $n ) delete_transient( $key );   // 读取即删,一次性
  return $n;
}

// 路径 A:首次验证成功(comment_post 钩子,需在 W3TC 清缓存钩子前显式清缓存)
add_action( 'comment_post', 'nickel_ev_after_post', 5 );
function nickel_ev_after_post( $comment_id ) {
  if ( empty( $GLOBALS['nickel_ev_just_verified'] ) ) return;
  $comment = get_comment( $comment_id );
  if ( ! $comment ) return;
  $post_id = (int) $comment->comment_post_ID;
  $state   = ( 'approved' === wp_get_comment_status( $comment_id ) ) ? 'approved' : 'hold';
  nickel_ev_set_notice( $post_id, $state );
  if ( function_exists( 'w3tc_flush_post' ) ) w3tc_flush_post( $post_id ); // W3TC 兼容
  clean_post_cache( $post_id );
  wp_safe_redirect( get_permalink( $post_id ) . '#comment-form-area' );
  exit;
}

// 路径 B:默认流程全部路径(已验证邮箱 / 未开验证 / 登录用户 / 待审核)
add_filter( 'comment_post_redirect', 'nickel_ev_comment_redirect', 10, 2 );
function nickel_ev_comment_redirect( $location, $comment ) {
  $post_id = (int) $comment->comment_post_ID;
  $state   = ( 'approved' === wp_get_comment_status( $comment->comment_ID ) ) ? 'approved' : 'hold';
  nickel_ev_set_notice( $post_id, $state );
  return get_permalink( $post_id ) . '#comment-form-area';
}

为什么必须接管 comment_post_redirect :WP 默认对待审核评论重定向到无锚点文章地址(回文章头部);对直接显示评论重定向 #comment-{id},又可能被主题”隐藏页头/平滑滚动”脚本覆盖。统一接管后所有路径都回评论区,且提醒用 transient 传(URL 无参数,缓存插件不会把 A 的提醒缓存给 B 看)。

(4)验证面板(comments.php 待验证状态时输出)

// comments.php 内:URL 带 email_verify=token 且草稿存在 → 渲染面板替换评论表单
$html  = '<div class="nickel-verify-panel">';
$html .= '<p class="verify-title">邮箱验证</p>';
$html .= '<p class="verify-remind">为防骚扰,首次评论需邮箱验证,验证码发至您填写的邮箱,通过后不再验证。</p>';
$html .= '<p class="verify-desc">验证码已发送至 ' . nickel_ev_mask_email( $email ) . ',10 分钟内有效,请查收并输入。</p>';
$html .= '<form method="post" action="' . esc_url( site_url( '/wp-comments-post.php' ) ) . '">';
$html .= '<input type="hidden" name="comment_post_ID" value="' . $post_id . '">';
$html .= '<input type="hidden" name="nickel_ev_token" value="' . esc_attr( $token ) . '">';
// 以下 hidden 字段回显草稿(昵称/邮箱/网址/评论内容),验证通过后回填入库,内容不丢失
$html .= '<input type="hidden" name="author" value="' . esc_attr( $pending['name'] ) . '">';
$html .= '<input type="hidden" name="email" value="' . esc_attr( $email ) . '">';
$html .= '<input type="hidden" name="url" value="' . esc_attr( $pending['url'] ) . '">';
$html .= '<input type="hidden" name="comment" value="' . esc_attr( $pending['comment'] ) . '">';
$html .= '<input type="text" name="email_code" maxlength="6" pattern="[0-9]*" required placeholder="请输入 6 位数字验证码">';
$html .= '<button type="submit">确认验证</button>';
$html .= ' <a href="' . esc_url( add_query_arg( array( 'email_verify' => $token, 'resend' => '1' ), get_permalink( $post_id ) ) . '#comment-form-area' ) . '">重新发送</a>';
$html .= '<p>若未收到,请检查垃圾邮件,或点击"重新发送"(60 秒后可再次发送)。</p>';
$html .= '</form></div>';

(5)评论区渲染分支(comments.php)

<div id="comment-form-area">
  <?php if ( $nickel_ev_pending ) : ?>
    <?php echo nickel_ev_panel_html( $nickel_ev_pending, $nickel_ev_token ); // 验证面板 ?>
  <?php else : ?>
    <?php if ( $nickel_notice ) : // 提交后状态提醒(transient 一次性) ?>
      <p class="nickel-verify-notice">
        <?php echo ( 'approved' === $nickel_notice['state'] ) ? '评论已发布。' : '评论已提交,待审核后显示。'; ?>
      </p>
    <?php elseif ( $nickel_ev_expand ) : // 发信失败/验证码失效错误回显 ?>
      <p class="nickel-verify-notice error"><?php echo $nickel_ev_msg; ?></p>
    <?php endif; ?>
    <button id="load-comment-form">发表评论</button>  <!-- 或直接 comment_form() -->
    <div id="comment-form-container" style="display:none"></div>
  <?php endif; ?>
</div>

2.6 界面文案(全流程)

环节 文案
验证面板提醒行 为防骚扰,首次评论需邮箱验证,验证码发至您填写的邮箱,通过后不再验证。
发码回显 验证码已发送至 l***@qq.com,10 分钟内有效,请查收并输入。
输入框占位 请输入 6 位数字验证码
验证码错误 验证码错误或已过期,请重新输入(还可尝试 N 次)。
发送频繁 发送过于频繁,请 60 秒后再试。
发信失败 邮件发送失败,请稍后重试。
重发成功 验证码已重新发送,请查收。
验证成功·直接显示 评论已发布。
验证成功·待审核 评论已提交,待审核后显示。
邮箱未收到 若未收到,请检查垃圾邮件,或点击”重新发送”(60 秒后可再次发送)。

三、评论必须包含中文

3.1 设计思路

双层防护

  1. 前端(体验层):表单提交时 JS 检查,无中文用 setCustomValidity 弹原生气泡,内容不丢失
  2. 后端(安全层):pre_comment_on_post(优先级 1,先于邮箱验证发码)+ preprocess_commentwp_new_comment 内部兜底,覆盖 REST/AJAX 路径)。

3.2 流程图

flowchart TD
    A[提交评论] --> B{JS 前端校验
#comment 是否含中文}
    B -- 无中文 --> C[setCustomValidity 弹气泡
preventDefault 阻止提交
内容保留, 输入时清除错误]
    B -- 含中文 --> D[后端 pre_comment_on_post pri 1]
    D -- 无中文 --> E[wp_die 提示
'评论必须包含中文...'
不发验证码]
    D -- 含中文 --> F[继续: 邮箱验证/入库]
    F --> G[wp_new_comment → preprocess_comment]
    G -- 无中文(绕过前端) --> E
    G -- 含中文 --> H[正常入库]

3.3 前端 JS(main.js,事件委托兼容懒加载表单)

/* 事件委托到 document:兼容懒加载 AJAX 注入的评论表单。
   注意:不能在 reportValidity() 后立即 setCustomValidity('')——
   气泡渲染是异步的,立即清空会导致提示不显示(用户"卡在提交界面"却看不到原因)。 */
document.addEventListener('submit', function(e){
  var f = e.target;
  if(!f || f.id !== 'commentform') return;      // 只处理 WP 评论表单
  var ta = f.querySelector('#comment');
  if(!ta) return;
  if(!/[\u4e00-\u9fff]/.test(ta.value)){        // CJK 基本区正则
    e.preventDefault();
    ta.setCustomValidity('评论必须包含中文,请补充中文内容后再提交。');
    ta.reportValidity();
    ta.focus();
    // 用户重新输入时清除错误状态(一次性监听),下次提交正常校验
    ta.addEventListener('input', function(){ this.setCustomValidity(''); }, {once:true});
  }
});

3.4 后端 PHP(inc/comment-chinese.php)

/* 是否包含中文(CJK 统一表意文字基本区) */
function nickel_cc_has_chinese( $content ) {
  return (bool) preg_match( '/[\x{4e00}-\x{9fff}]/u', (string) $content );
}

/* 统一的拦截提示:wp_die 返回文章页,响应码 200 不误报 500 */
function nickel_cc_die() {
  wp_die(
    esc_html__( '评论必须包含中文,请补充中文内容后再提交。', 'nickel' ),
    esc_html__( '评论未提交', 'nickel' ),
    array( 'back_link' => true, 'response' => 200 )
  );
}

/* 校验:空评论放行(交给 WP 自带"请输入评论内容"提示) */
function nickel_cc_check( $content ) {
  if ( ! nickel_cc_enabled() ) return true;
  if ( '' === trim( (string) $content ) ) return true;
  return nickel_cc_has_chinese( $content );
}

/* 挂载点 1:评论表单提交入口——优先级 1,先于邮箱验证(pri 5)发码,
   无中文直接拦截,不浪费一轮验证码邮件 */
add_action( 'pre_comment_on_post', function() {
  if ( isset( $_POST['comment'] ) && ! nickel_cc_check( $_POST['comment'] ) ) {
    nickel_cc_die();
  }
}, 1 );

/* 挂载点 2:wp_new_comment 内部兜底(覆盖 REST / 其他插件路径) */
add_filter( 'preprocess_comment', function( $commentdata ) {
  if ( ! nickel_cc_enabled() ) return $commentdata;
  $content = isset( $commentdata['comment_content'] ) ? (string) $commentdata['comment_content'] : '';
  if ( ! nickel_cc_check( $content ) ) {
    nickel_cc_die();
  }
  return $commentdata;
}, 1 );

3.5 注意事项

场景 处理
邮箱验证面板提交 面板内评论内容是 hidden 字段(草稿回显),首次提交时已通过中文检测,不会误拦
登录用户 同样受检(符合”评论必须含中文”的要求);如需管理员豁免,在 nickel_cc_checkis_user_logged_in() 判断
空评论 放行,交给 WP 自带”请输入评论内容”提示,避免双重报错
正则范围 \x{4e00}-\x{9fff} 覆盖 CJK 基本区;如需扩展 A 区可加 \x{3400}-\x{4dbf}

四、安全与边界说明

  • 验证码哈希存储:数据库不存明文验证码,wp_hash() + hash_equals() 防时序攻击;
  • token 净化:URL 参数一律 preg_replace('/[^a-zA-Z0-9]/', '') 后再查 transient;
  • 隐私:URL 不带邮箱;页面显示掩码邮箱;验证邮件注明”若非您本人操作请忽略”;
  • 防滥用:60s 发信冷却 + 每邮箱草稿独立 + 验证码 10 分钟过期 + 5 次尝试上限;
  • 已知局限:真人手工垃圾评论、接邮件 API 的高配 bot 挡不住(任何验证码皆然),可配合 Akismet / 人工审核兜底;
  • W3TC 提醒显示依赖:默认路径由 W3TC 自带 comment_post 清缓存钩子保证回跳动态渲染;验证路径已显式 w3tc_flush_post()