作为个人站长,最烦的就是闲的蛋疼刷流量,或刷垃圾评论行为。
博友们用尽各种验证方式想挡住机器人刷评论,但都效果一直不令人满意,很难杜绝。我跟着大佬们的脚步,亦步亦趋,填字母、算数验证码、图片验证码都用过,还是挡不住这些闲人。所有验证方法只是给垃圾评论者增加了麻烦,并不能给他们装上锁,如果他们不怕麻烦,或者有高超技术绕过这些麻烦,那垃圾评论就又来了。
前短时间忘了在那位大佬的博客上看到了首次评论时邮箱需要验证的功能,觉得很好,于是今天让AI给加上了。这种验证方法虽然麻烦点,但好处是只需要验证一次,成功后永远不用再验证,这样其实比填验证码或拖动图片更友好。
增加首次邮箱验证的同时,也增加了评论中文检测功能,这是纯防国外机器人的。
当然,以上两种障碍也不一定能挡住垃圾评论,只要让它们减少点我就心满意足了。
在这里向各位道个歉,给大家添麻烦了,克服下填完验证码,以后将享受没有垃圾的评论。
下面是AI整理的开发经验,对于懂代码的大佬可能价值很低,但对于像我这样完全不懂代码的人来说,或许可以不用重复踩坑,也能省点token。
一、功能总览
| 功能 | 作用 | 拦防对象 | 对真实用户打扰 | 默认状态 |
|---|---|---|---|---|
| 邮箱首次验证 | 新邮箱首次评论须输入邮箱验证码,通过后该邮箱永久免验 | 批量灌水脚本、SEO 群发、随机邮箱 bot | 仅第一次,约 30 秒 | 后台开关,默认关 |
| 评论必须含中文 | 评论内容无汉字则无法提交 | 纯外文/乱码灌水 | 几乎为零(正常中文评论无感) | 后台开关,默认开 |
为什么邮箱验证比算术验证码强:算术码 bot 用正则/OCR 几行代码就解;邮箱验证要求”真实邮箱 + 收信 + 提取码 + 回填”,灌水脚本不会为个人博客做这套。加上”首次验证、终身免验”,真人成本一次、bot 每次都要新邮箱走全流程。
二、邮箱首次评论验证
2.1 核心设计思路
- 不新建自定义表,全部用 WP 原生
option+transient(自动过期,免清理); - 发码在提交拦截钩子里做(
pre_comment_on_post),发码前先检查中文(另一个钩子优先级更高); - 评论内容不丢失:首次提交时把昵称/邮箱/网址/评论内容存成”草稿”(transient),验证面板用 hidden 字段回显,验证通过后回填
$_POST让 WP 正常入库; - 邮箱不暴露:回跳 URL 只带随机 token,不出现邮箱;页面显示掩码邮箱
l***@qq.com; - 验证码存哈希:
wp_hash()存储,校验用hash_equals(),防数据库侧读明文; - 回跳统一带锚点 + 一次性状态提醒:所有评论提交路径(验证/默认流程)回跳
文章地址#comment-form-area,提醒用 transient 传递,URL 干净、不被页面缓存污染。
2.2 整体流程图
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 设计思路
双层防护:
- 前端(体验层):表单提交时 JS 检查,无中文用
setCustomValidity弹原生气泡,内容不丢失; - 后端(安全层):
pre_comment_on_post(优先级 1,先于邮箱验证发码)+preprocess_comment(wp_new_comment内部兜底,覆盖 REST/AJAX 路径)。
3.2 流程图
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_check 加 is_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()。


目前用了蜜罐型评论拦截感觉没啥评论了。哈哈。
我也上蜜罐了,但还是有很多。
半年前,我的WP站点天天收到几条垃圾评论,我记得WP压缩包里面拦截垃圾评论的插件,但是我没用过,当时搭建WP站点后把插件删掉了,后来想着试试在模板函数文件(functions.php)添加代码只允许提交包含中文的评论,实测效果不错,这半年没收到垃圾评论了。
代码:wp.bufanz.com/wordpress/only-comments-containing-chinese-are-permitted.html
检测中文是投资小,收益大的事情,很值得。
其他的垃圾邮件就要慢慢和他们战斗了。
我提交评论时需要填写邮箱验证码,可以防止被别有用心之人在别人博客提交不当言论。
我发现你博客有个问题,我刚才提交评论时开了“在此浏览器中保存我的显示名称、邮箱地址和网站地址,以便下次评论时使用。”,你回复评论后,我打开这个链接,评论表单是空的,如果我再提交评论,我还需要手动填写昵称、邮箱和网址,你是用了什么缓存插件吗?
这个也是写到主题里的,现在博客的功能都是通过主题实现的,插件只有W3缓存、一个阿里ESA同步刷新插件、和一个修订版本等清理插件。
我看下这个评论表单问题,看能不能修好。
你没发评论都要填验证码的方法很严谨,但我觉得每次都填就太麻烦了,所以我选了只填一次。但就像你说的,这样可能被其他人利用。
你是把文章页或.html文件类型缓存到阿里云ESA了吗?(这次提交评论仍然还是手动填写表单)
是的,W3缓存,然后直接给ESA,这是速度块的关键。评论还没有修好,正在修。
评论表单记忆现在好了,谢谢你的提醒。
现在表单是自动填写了,你怎么解决的?
AI解决的,原来记录表单在验证码逻辑之前,被验证环节给清掉了,改了个位置就好了。
因为我用的是ajax提交评论,于是删掉根目录的wp-comments-post.php,也用了评论必须包含中文,同时使用WordPress后台的过滤词,过滤:算命、seo之类的。
感觉我一年不超过10条垃圾评论,而且这些也是那种看着像人工发表垃圾评论。
我怎么忘了可以这么用,我的过滤都是填的垃圾评论的邮箱和域名,浪费了。
我差不多也是这么弄的
想不通现在都移动App+AI时代了,这些刷垃圾评论的挂外链还有啥意义~
搞不懂,也不知道有什么利益,能让他们这么拼。
好专业,我这边谷歌浏览器无法保存评论信息,建议修复下,每次评论都要填名称邮箱。
好的,我看下那出问题了。
不明觉厉。
Akismet挡不住吗?
没有装这个插件,据说装了更会被骚扰评论盯上?
用了几年看还行。
我先自己折腾着看下。
大佬,你那边能收到评论回复的有见吧,我从昨晚到今天一直没有收到邮件,还以为增加了这个验证大家都不愿留言了。我改了主题,还升级了php,改动比较大,可能会有冲突。
我昨天收到了一封密码函。我觉得这么搞有点不丝滑哈哈,你要不要参考下微言微语的方法。
一生只认证一次,我觉得可以忍忍。这样应该能挡住机器了,如果他们还愿意自动接收验证码,我就真服了。
wp垃圾评论,的确猖獗
都是机器扫描的,人工的其实并不是很多。
不用 wordpress,换了程序之后,垃圾评论少了很多,偶尔一两条都能拦截
那是,机器扫描的话wordpress是热门。
这个厉害,其实我用 Typecho 感觉还好吧,搭配插件,设置首次评论审核,一般问题不大
有一些广告,还有不合规的评论,估计是他们赌没有人工审核。
上一条评论不知道失效没有~
屏蔽外网访问能省事一些,我看有些用cf的机器人能拦截认证。
两条评论都在,CF我感觉在国内太慢了。直接屏蔽国外IP太粗暴了,得想温和的方式。