DedeCMS 的"自定义表单"功能让站长能在前台收集用户提交(咨询、报名、留言等),后台 → 核心 → 自定义表单管理可以可视化建表。但默认表单没有必填项校验——用户提交空白表单也能成功,结果后台收一堆空数据。社区流传的修补方法是改 /plus/diy.php 加服务端校验,或前端 jQuery 校验。两种方法 2026 年都有需要重新审视的地方:服务端的写法有 变量变量($$field)安全风险、前端的 jQuery 在现代浏览器已是冗余依赖。
这一篇把 DedeCMS 自定义表单必填项校验讲透:原社区代码的安全隐患、加固版的服务端校验、纯前端的现代浏览器原生校验(HTML5 required + JS)、二者协同设计、与 reCAPTCHA / 蜂蜜罐反爬虫的协同、提交后跳转优化、跨版本(V5.7 SP1/SP2 + DedeBIZ)兼容、迁移现代 CMS 的等价做法。
一、社区代码的安全隐患:变量变量 $$field
原社区流传的服务端校验代码核心:
$requireds = explode(',', $required);
foreach($requireds as $field) {
if($$field == '') { // ← 变量变量
showMsg('带*号的为必填内容', '-1');
exit();
}
}$$field 是 PHP 的变量变量语法——如果 $field = "name",那么 $$field 等价于 $name。这个特性配合 DedeCMS 的全局变量注入机制(register_globals=on 或 DedeCMS 自家的全局注入),允许通过 $_POST['name'] 自动变成 $name。但这同时是个安全坑:
- 开 register_globals 的 PHP 5.x 老站:攻击者可以伪造任意变量名,覆盖应用内部变量。但 PHP 5.4 起 register_globals 已被移除,新站点无此风险。
- DedeCMS 全局注入:DedeCMS 自带
foreach($_POST as $k => $v) ${$k} = $v;这种危险代码(在common.inc.php里)。攻击者构造POST: required=cfg_db_pwd&cfg_db_pwd=hacked能覆盖数据库密码全局变量。 - 变量名校验缺失:原代码没验证
$field是不是合法的字段名——攻击者可以传required=xxx;DROP TABLE这种值,foreach 进去虽不直接 SQL 注入,但破坏程序流。
正确的写法不要用变量变量,直接读 $_POST:
if (!empty($required)) {
$requireds = is_array($required) ? $required : explode(',', $required);
foreach ($requireds as $field) {
// 严格校验字段名(仅字母数字下划线)
if (!preg_match('/^[a-zA-Z0-9_]+$/', $field)) continue;
$val = isset($_POST[$field]) ? trim($_POST[$field]) : '';
if ($val === '') {
showMsg('带*号的为必填内容,请正确填写:' . htmlspecialchars($field), '-1');
exit();
}
}
}这个写法相比社区版本:① 不用变量变量,避免依赖 DedeCMS 的全局注入;② 字段名加白名单正则,防止注入;③ 错误提示告知具体哪个字段没填,对用户更友好。
二、加固服务端校验:完整代码
放在 /plus/diy.php 找到 $dede_fields = empty($dede_fields) ? '' : trim($dede_fields); 行,下面加:
// === 自定义表单必填项与基础校验 ===
// 1. 必填项检查
if (!empty($required)) {
$requireds = is_array($required) ? $required : explode(',', $required);
foreach ($requireds as $field) {
if (!preg_match('/^[a-zA-Z0-9_]+$/', $field)) continue;
$val = isset($_POST[$field]) ? trim($_POST[$field]) : '';
if ($val === '') {
showMsg('带 * 号的为必填内容,请正确填写', '-1');
exit();
}
}
}
// 2. 邮箱字段格式校验
if (isset($_POST['email']) && $_POST['email'] !== '') {
if (!filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
showMsg('邮箱格式不正确', '-1');
exit();
}
}
// 3. 手机号简单校验(中国大陆)
if (isset($_POST['mobile']) && $_POST['mobile'] !== '') {
if (!preg_match('/^1[3-9]\d{9}$/', $_POST['mobile'])) {
showMsg('手机号格式不正确', '-1');
exit();
}
}
// 4. 蜂蜜罐反爬虫(hidden 字段,正常用户不填,爬虫填了就拦)
if (isset($_POST['url']) && $_POST['url'] !== '') {
// 静默拦截,不告诉爬虫具体原因
showMsg('提交失败', '/');
exit();
}
// 5. 提交频率限制(同 IP 30 秒内只能提交 1 次)
$cacheKey = 'diy_submit_' . md5($_SERVER['REMOTE_ADDR']);
$cacheFile = DEDEDATA . '/cache/' . $cacheKey;
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < 30) {
showMsg('提交太频繁,请 30 秒后重试', '-1');
exit();
}
file_put_contents($cacheFile, time());这套加固版做了 5 件事:① 必填校验;② 邮箱格式校验;③ 手机号校验;④ 蜂蜜罐反爬;⑤ 提交频率限制。比社区版本只做必填的安全防御提升一个量级。
三、纯前端校验:现代浏览器 + HTML5
2026 年的浏览器已经原生支持表单校验,不需要 jQuery。直接用 HTML5 required 属性:
<form action="/plus/diy.php" method="post" enctype="multipart/form-data">
<label>姓名 *
<input type="text" name="name" required minlength="2" maxlength="20">
</label>
<label>邮箱 *
<input type="email" name="email" required>
</label>
<label>手机号 *
<input type="tel" name="mobile" required pattern="^1[3-9]\d{9}$">
</label>
<label>留言内容 *
<textarea name="message" required minlength="10" maxlength="500"></textarea>
</label>
<!-- 蜂蜜罐:CSS 隐藏,正常用户看不到,爬虫填了就被服务端拦 -->
<label style="position:absolute;left:-9999px">Website
<input type="text" name="url" tabindex="-1" autocomplete="off">
</label>
<input type="hidden" name="required" value="name,email,mobile,message">
<button type="submit">提交</button>
</form>HTML5 表单校验的优势:
- 无 JS 依赖——浏览器原生支持,不用引 jQuery;
- 多语言友好——浏览器按用户语言显示错误提示(中文用户看到中文);
- 可访问性好——屏幕阅读器能读 required 标签,无障碍体验更佳;
- 禁用 JS 也能提交——服务端兜底,防 JS 关闭场景。
3.1 自定义错误提示
HTML5 默认错误提示是浏览器自带的("请填写此字段" 等),可以自定义:
<input type="email" name="email" required
oninvalid="this.setCustomValidity('请填写有效的邮箱地址')"
oninput="this.setCustomValidity('')">oninvalid 设错误文案,oninput 在用户重新输入时清空错误。这两个事件让自定义提示与原生校验机制结合。
四、服务端 + 前端协同设计
正确的做法是两端都校验:
| 场景 | 前端 | 服务端 |
|---|---|---|
| 用户体验 | 实时反馈,不需要等服务器 | 提交后才知道错 |
| 禁用 JS / 直接 POST | 失效 | 仍能挡住 |
| 性能 | 减少无效请求到服务器 | 无差别 |
| 安全 | 不可信(用户能改 DOM 绕过) | 可信,最后防线 |
结论:前端用 HTML5 增强用户体验,服务端校验是不能省的最终防线。任何只做前端不做服务端校验的表单都不安全。
五、与 reCAPTCHA / 蜂蜜罐的协同
5.1 蜂蜜罐(Honeypot)
原理:在表单里加一个对正常用户隐藏的字段(CSS position: absolute; left: -9999px),机器人爬虫填表时会无脑填所有字段,正常用户因为看不到所以不填。服务端检查这个字段:
- 空 = 正常用户,放行;
- 非空 = 爬虫,静默拦截(不告诉它具体哪个字段有问题,让它继续浪费配额)。
蜂蜜罐对 80%+ 的低端爬虫有效,且对正常用户零打扰——不需要点验证码。
5.2 Google reCAPTCHA v3
对剩余的高端爬虫(能识别蜂蜜罐的),加 reCAPTCHA v3。它在背后给每个用户打分(0-1,1 是真人,0 是爬虫),不需要用户点击:
<!-- 前端 -->
<script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
<script>
grecaptcha.ready(function() {
grecaptcha.execute('YOUR_SITE_KEY', {action: 'submit'}).then(function(token) {
document.querySelector('input[name="recaptcha_token"]').value = token;
});
});
</script>
<input type="hidden" name="recaptcha_token">// 服务端校验
$token = $_POST['recaptcha_token'] ?? '';
$secret = 'YOUR_SECRET_KEY';
$resp = file_get_contents("https://www.google.com/recaptcha/api/siteverify?secret={$secret}&response={$token}");
$data = json_decode($resp, true);
if (!$data['success'] || $data['score'] < 0.5) {
showMsg('请稍后重试', '-1');
exit();
}注意:reCAPTCHA 需要 Google 服务可访问——国内用户可能因网络问题加载失败,建议用国产替代(极验、网易易盾、阿里云人机验证)。
六、提交成功后的跳转优化
原代码用 showMsg() 显示提示后跳转,体验不佳。优化:
// 提交成功后跳转到独立感谢页(更专业)
header('Location: /thank-you.html?from=' . urlencode($_POST['name'] ?? ''));
exit();感谢页可以放:① 提交成功提示;② 客户经理联系方式;③ 引导用户去看其他相关产品/文章;④ 触发 Google Analytics 转化事件(电商漏斗追踪)。
6.1 GA / 百度统计的转化追踪
在感谢页 thank-you.html 里加:
<script>
gtag('event', 'form_submit', {
'event_category': 'Lead',
'event_label': '咨询表单',
'value': 100
});
</script>这样 Google Analytics 能跟踪每次表单提交的转化漏斗,结合广告投放的 CPA / ROAS 计算。
七、跨版本兼容(V5.7 / SP1 / SP2 / DedeBIZ)
/plus/diy.php 在 DedeCMS 各版本里逻辑大致一致,但行号略有差异:
| 版本 | $dede_fields trim 行 | 建议插入位置 |
|---|---|---|
| V5.7 | ~ 第 38 行 | 第 39 行后 |
| V5.7 SP1 | ~ 第 40 行 | 第 41 行后 |
| V5.7 SP2 | ~ 第 42 行 | 第 43 行后 |
| DedeBIZ V6 | 已合并必填校验 | 无需手改 |
升级 DedeCMS 时这个改动会被覆盖——记得升级前备份 /plus/diy.php。
八、与防垃圾留言的纵深防御
必填校验只挡空表单,挡不了"机器人填假数据"。完整的防垃圾留言要做:
- 必填项 + 格式校验(本文)
- 蜂蜜罐(隐藏字段)
- 提交频率限制(同 IP 30 秒)
- reCAPTCHA / 极验(人机判断)
- 关键词黑名单(拦"代开发票"、"刷流量"等垃圾文本)
- fail2ban(动态拉黑高频提交 IP)
- WAF(Cloudflare / 阿里云)
每多一层就少一个数量级的垃圾留言。中小站点至少做前 4 层。
九、迁移到现代 CMS 的等价做法
| DedeCMS 自定义表单 | WordPress | Hexo / 静态站 |
|---|---|---|
| plus/diy.php | Contact Form 7 / Gravity Forms / WPForms | Formspree / Netlify Forms / Vercel Edge |
| 必填校验 | 插件原生支持 | HTML5 + 服务端 webhook |
| 反垃圾 | Akismet | Formspree honeypot 内置 |
| 邮件通知 | 插件原生 | Formspree 邮件转发 |
WordPress 上这些功能都是装个插件即可,不用改源码。Hexo 等静态站点用第三方 SaaS 服务(Formspree 免费 50 次/月)。
十、表单提交日志的留存
除了写入 dede_diyforms 数据库表,建议同时记录到日志:
// 记录提交日志(追加到 logs/form_submit.log)
$logEntry = sprintf(
"[%s] [%s] %s\n",
date('Y-m-d H:i:s'),
$_SERVER['REMOTE_ADDR'],
json_encode([
'name' => $_POST['name'] ?? '',
'email' => $_POST['email'] ?? '',
'mobile' => $_POST['mobile'] ?? '',
'message' => mb_substr($_POST['message'] ?? '', 0, 200, 'UTF-8'),
'ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 100),
], JSON_UNESCAPED_UNICODE)
);
file_put_contents(DEDEDATA . '/logs/form_submit.log', $logEntry, FILE_APPEND | LOCK_EX);日志的好处:① 数据库被攻击时还有副本;② 配 fail2ban 直接读日志识别垃圾 IP;③ 后期数据分析(哪些时段提交多、什么 UA 提交多)。
常见问题解答
改完 plus/diy.php 没生效?
① 确认文件保存了;② DedeCMS 后台 → 系统 → 系统设置 → 更新缓存(虽然 plus 不走模板缓存,但保险起见全清);③ 浏览器开 F12 看提交时是否真请求到 plus/diy.php(路径对不对);④ 看 PHP error_log 有没有报错。
required 隐藏字段的字段名要按什么命名?
按你后台自定义表单建立时的"数据字段名"。比如建表单时"姓名"对应数据字段名 name,"邮箱"对应 email,那 required 就写 name,email。字段名严格区分大小写。
表单提交后页面跳转到 plus/diy.php 但页面空白?
多半是 PHP 报错且 display_errors=Off。先打开 display_errors 看报错——大概率是某个 require 路径错或 include 失败。生产环境调试完务必关回 display_errors,避免暴露路径信息给攻击者。
能不能给不同表单设不同的必填项规则?
能。在 /plus/diy.php 里读 $_POST['diyid'](自定义表单 ID),按 ID 走不同规则:if ($diyid == 1) { ... } elseif ($diyid == 2) { ... }。或者把规则配置写到数据库的扩展字段,让管理员后台编辑。
用 reCAPTCHA 后 SEO 受影响吗?
不直接影响。Google 不会因为页面用了自家 reCAPTCHA 给 SEO 加分(也不扣分)。但 reCAPTCHA 加载约 200KB JS,对 LCP 有约 50-100ms 影响。生产建议仅在表单页加 reCAPTCHA,不要全站加。
蜂蜜罐字段叫什么名字最有效?
选爬虫"看到一定会填"的字段名:url、website、company、fax 等。爬虫的填表逻辑是按字段名匹配自带字典,常见字段名几乎必填。但要避免和真实表单里的同名字段冲突。
提交频率限制用 IP 还是 cookie?
都用更稳。IP 限制对 NAT 后的多用户共享 IP 不友好(误伤);cookie 限制可被清除绕过。生产建议组合:① IP 限频较宽(30 秒 1 次);② cookie 配合标识"已提交过"(24 小时内不能重复);③ 用 Redis 存计数器,性能更好。
能拦截使用 cURL 等工具直接提交吗?
用 cURL 等工具能绕过前端校验直接 POST。要拦:① 服务端必填校验(绕不过);② 蜂蜜罐 + reCAPTCHA + IP 限频组合;③ 检查 User-Agent 是否包含 cURL/Wget/Python 等爬虫特征字符串。但攻击者改 UA 容易,最终防御还是看服务端的逻辑校验。
表单提交日志要不要清理?
要。logs/form_submit.log 长期不清会越来越大,建议 logrotate:每天滚动 + 保留 30 天 + 超期归档到云存储(OSS / S3)。日志体量大时(每天几万次提交)走结构化日志(JSON Lines)+ 上 ELK 或 Loki,比 grep 文本快得多。
能在表单页加防止 F12 调试的代码吗?
不建议。这种"反调试"对正常开发者是骚扰,对攻击者完全无效(任何反调试都能在浏览器层面绕过)。要保护表单不被滥用,靠服务端校验 + 速率限制 + WAF + Honeypot 组合,不要靠"防 F12"这种心理防御。