DedeCMS自定义表单必填项校验完整方案:服务端+JS+HTML5三层+reCAPTCHA+CSRF防护

DedeCMS 自定义表单默认所有字段都可空,运营拿到的留言半数没联系方式。本文给出 plus/diy.php 服务端校验、原生 JS 即时反馈、HTML5 兜底属性的三层方案,并扩展到邮箱手机号正则、reCAPTCHA v3 评分阈值、双 token CSRF 防护、SameSite cookie 等加固,与 DedeCMS 表单 UI 美化(cid 822)配套使用。

张文保 更新 28 分钟阅读 2,894 阅读
本文目录
  1. 方案设计:三层校验缺一不可
  2. 为什么要三层
  3. 服务端校验:plus/diy.php 改造
  4. 原文方案的强化版
  5. 相比原方案的改进
  6. 表单端的传参
  7. 前端 JS 即时校验
  8. JS 实现
  9. 关键设计点
  10. HTML5 原生校验兜底
  11. 验证码防机器人
  12. 为什么需要验证码
  13. 方案 A:DedeCMS 自带验证码
  14. 方案 B:Google reCAPTCHA
  15. CSRF 防护
  16. 问题描述
  17. 双 token 防护
  18. SameSite Cookie
  19. 常见故障
  20. 故障 1:必填校验不生效
  21. 故障 2:错误提示中文乱码
  22. 故障 3:必填字段填了仍提示空
  23. 故障 4:reCAPTCHA 验证失败
  24. 故障 5:CSRF token 校验通不过
  25. 故障 6:HTML5 required 在 iOS Safari 上不弹提示
  26. 常见问题解答
  27. 必填项配置在表单 HTML 里好还是在数据库里好?
  28. 能否给文件上传字段做必填?
  29. 多选 checkbox 怎么校验必填?
  30. 必填校验影响搜索引擎抓取吗?
  31. 能否做条件必填(A 字段填了 B 字段才必填)?
  32. 表单提交后页面跳转白屏?
  33. 校验信息能否多语言?
  34. 能否限制同一 IP 频繁提交?
  35. 验证码刷一下还是错?
  36. 表单提交后能否邮件通知运营?

DedeCMS 自定义表单(diy)模块默认所有字段都是可选的,留言、报名、咨询等场景里用户可以提交一份空表单。运营拿到一堆没有联系方式的留言完全没用。本文给出 DedeCMS 自定义表单必填项校验的三层完整方案:服务端 plus/diy.php 强制校验、前端 JS 即时反馈、HTML5 原生校验,并扩展到字段格式校验(手机号、邮箱、URL 正则)、图形验证码防机器人、CSRF token 双 token 防护、必填规则配置化、错误信息友好显示。本文与本站“DedeCMS 自定义表单 CSS 美化”(cid 822)配套使用,是同一组 diy 模块的功能加固而不是 UI 美化。

方案设计:三层校验缺一不可

为什么要三层

  • 服务端校验是底线,永远不能省。攻击者构造 POST 请求绕过前端校验是基本操作。
  • 前端 JS 校验是体验。用户填错某个字段时不应该等提交后才告知,要在 blur(失去焦点)时立刻红框提示。
  • HTML5 校验是兜底。input type=email、required 这种属性让浏览器原生支持基础校验,无需 JS。

三层任一缺失都有问题。只做服务端:用户体验差。只做前端:黑产秒绕过。只做 HTML5:不灵活、错误提示不可定制。

服务端校验:plus/diy.php 改造

原文方案的强化版

编辑 plus/diy.php,在第 40 行附近找到:

$dede_fields = empty($dede_fields) ? '' : trim($dede_fields);

下面插入完整的必填校验逻辑:

/* ===== DIY 表单必填项校验 START ===== */
if (!empty($required)) {
    $required_fields = array_filter(array_map('trim', explode(',', $required)));
    $missing_fields = [];
    $field_labels = [];

    /* 取字段中文标签(如果传了 required_labels 参数) */
    if (!empty($required_labels)) {
        $labels_arr = array_filter(array_map('trim', explode(',', $required_labels)));
        $field_labels = array_combine($required_fields, array_pad($labels_arr, count($required_fields), ''));
    }

    foreach ($required_fields as $field) {
        /* 防止变量名注入:限定字符集 */
        if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]{0,30}$/', $field)) {
            continue;
        }
        $value = isset($GLOBALS[$field]) ? $GLOBALS[$field] : '';
        if (is_string($value)) $value = trim($value);

        if ($value === '' || $value === null || (is_array($value) && empty($value))) {
            $label = !empty($field_labels[$field]) ? $field_labels[$field] : $field;
            $missing_fields[] = $label;
        }
    }

    if (!empty($missing_fields)) {
        $msg = '以下字段为必填,请补充:' . implode('、', $missing_fields);
        ShowMsg($msg, '-1');
        exit();
    }
}
/* ===== DIY 表单必填项校验 END ===== */

相比原方案的改进

  • 统一处理多字段:原方案在 explode 之后用嵌套 if-else 处理“单字段 vs 多字段”,本版本用 array_filter 统一。
  • 字段名格式校验:限定 required 参数只接受字母开头 + 字母数字下划线的合法变量名,防止攻击者传 required=";rm -rf /;" 这种注入。
  • 友好错误提示:通过 required_labels 参数传入字段中文名(如“姓名,邮箱”),提示信息从“字段 name 不能为空”变成“以下字段为必填:姓名、邮箱”。
  • 批量错误提示:用户一次填错多个字段时一次性显示所有缺失项,不是改一个再发现下一个错。
  • 处理多种空值:trim 之后的空字符串、null、空数组都判定为“未填”。

表单端的传参

表单 HTML 模板里,在 form 内加两个隐藏字段:

<form action="/plus/diy.php" enctype="multipart/form-data" method="post">
    <input type="hidden" name="dopost" value="save" />
    <input type="hidden" name="diyid" value="1" />
    <input type="hidden" name="required" value="username,phone,content" />
    <input type="hidden" name="required_labels" value="姓名,联系电话,留言内容" />
    ...其它字段...
</form>

required 字段名与 required_labels 中文名按相同顺序对齐。

前端 JS 即时校验

JS 实现

把以下 JS 加到模板末尾或单独 .js 文件:

<script>
(function() {
    var form = document.querySelector('form[action*="/plus/diy.php"]');
    if (!form) return;

    var requiredFields = (form.querySelector('input[name="required"]') || {}).value || '';
    var requiredLabels = (form.querySelector('input[name="required_labels"]') || {}).value || '';
    var fields = requiredFields.split(',').map(function(s) { return s.trim(); }).filter(Boolean);
    var labels = requiredLabels.split(',').map(function(s) { return s.trim(); });

    function getFieldLabel(name, idx) {
        return labels[idx] || name;
    }

    function showError(input, msg) {
        var hint = input.nextElementSibling;
        if (!hint || !hint.classList.contains('field-error')) {
            hint = document.createElement('span');
            hint.className = 'field-error';
            hint.style.cssText = 'color:#e74c3c;font-size:12px;margin-left:8px;';
            input.parentNode.insertBefore(hint, input.nextSibling);
        }
        hint.textContent = msg;
        input.style.borderColor = '#e74c3c';
    }

    function clearError(input) {
        var hint = input.nextElementSibling;
        if (hint && hint.classList.contains('field-error')) {
            hint.remove();
        }
        input.style.borderColor = '';
    }

    function validateField(input, idx) {
        var value = (input.value || '').trim();
        var label = getFieldLabel(input.name, idx);
        if (!value) {
            showError(input, label + '不能为空');
            return false;
        }

        /* 邮箱格式 */
        if (input.type === 'email' || input.name.toLowerCase().indexOf('email') !== -1) {
            if (!/^[\w.+-]+@[\w-]+\.[\w.-]+$/.test(value)) {
                showError(input, '邮箱格式不正确');
                return false;
            }
        }
        /* 手机号 */
        if (input.name.toLowerCase().indexOf('phone') !== -1 || input.name.toLowerCase().indexOf('mobile') !== -1) {
            if (!/^1[3-9]\d{9}$/.test(value)) {
                showError(input, '请输入 11 位手机号');
                return false;
            }
        }
        /* URL */
        if (input.type === 'url') {
            try { new URL(value); }
            catch (e) { showError(input, 'URL 格式不正确'); return false; }
        }
        clearError(input);
        return true;
    }

    /* 给每个必填字段挂 blur 监听 */
    fields.forEach(function(name, idx) {
        var input = form.querySelector('[name="' + name + '"]');
        if (!input) return;
        input.addEventListener('blur', function() { validateField(input, idx); });
        input.addEventListener('input', function() { clearError(input); });
    });

    /* 提交时统一校验 */
    form.addEventListener('submit', function(ev) {
        var allValid = true;
        fields.forEach(function(name, idx) {
            var input = form.querySelector('[name="' + name + '"]');
            if (input && !validateField(input, idx)) {
                allValid = false;
            }
        });
        if (!allValid) {
            ev.preventDefault();
            alert('请检查表单填写');
        }
    });
})();
</script>

关键设计点

  • 不依赖 jQuery:原文方案用了 jQuery,但 DedeCMS 默认不引入。纯原生 JS 减少依赖。
  • 渐进式提示:blur 时校验该字段,input 时清除错误。用户改错过程中体验流畅。
  • 提交时全量校验:防止用户从未 focus 过某些字段导致 blur 没触发的情况。
  • 正则按字段名启发:name 含 email 的自动套邮箱正则,含 phone/mobile 的套手机号正则。无需手动配置每个字段的格式。

HTML5 原生校验兜底

给关键字段加 HTML5 属性:

<input name="username" type="text" required minlength="2" maxlength="20" />
<input name="email" type="email" required />
<input name="phone" type="tel" required pattern="^1[3-9]\d{9}$" />
<input name="age" type="number" min="18" max="120" />
<textarea name="content" required minlength="10" maxlength="500"></textarea>

浏览器看到 required 属性,提交时如果对应字段为空会自动弹出原生提示阻止表单发送。pattern 正则做格式校验。这一层不需要 JS。

缺点:原生提示样式不可定制(不同浏览器风格不同),错误信息也是浏览器自己的话术。生产环境一般用 JS 接管校验提示,但保留 HTML5 属性作为兜底。

验证码防机器人

为什么需要验证码

必填项校验只防“空表单”,挡不住“填了垃圾内容的表单”。机器人能填上 abc@xxx.com、13800000000 这种格式合法但虚假的数据,绕过所有上面的校验。验证码是防自动化提交的最后一道防线。

方案 A:DedeCMS 自带验证码

diy.php 在表单提交时自动校验 vdcode 字段(如果模板里有的话)。模板里加:

<label>验证码:
    <input name="vdcode" type="text" required size="6" />
    <img src="/include/vdimgck.php" alt="点击换一张" onclick="this.src='/include/vdimgck.php?'+Math.random()" />
</label>

plus/diy.php 默认会校验 vdcode,无需额外代码。但 DedeCMS 自带验证码只是 4 位简单字符,OCR 识别率高(自动机器人能识别 90%+)。

方案 B:Google reCAPTCHA

更可靠的选择是接入 reCAPTCHA v3(无感验证):

<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_form'}).then(function(token) {
        document.getElementById('g-recaptcha-token').value = token;
    });
});
</script>
<input type="hidden" id="g-recaptcha-token" name="g-recaptcha-token" />

plus/diy.php 校验时调 Google API:

$token = $_POST['g-recaptcha-token'];
$secret = 'YOUR_SECRET_KEY';
$verify = file_get_contents('https://www.google.com/recaptcha/api/siteverify?secret=' . $secret . '&response=' . $token);
$data = json_decode($verify, true);
if (empty($data['success']) || $data['score'] < 0.5) {
    ShowMsg('系统检测到异常请求,请稍后重试', '-1');
    exit();
}

v3 评分越高越像真人,0.5 是常见阈值。

但 Google reCAPTCHA 在中国大陆访问受限,国内用户体验差。备选方案:geetest(极验)、hCaptcha、aliyun 行为验证。

CSRF 防护

问题描述

攻击者在另一个网站放一段代码:

<form action="https://your-site.com/plus/diy.php" method="post">
    <input name="dopost" value="save" />
    <input name="diyid" value="1" />
    <input name="content" value="垃圾广告" />
</form>
<script>document.forms[0].submit();</script>

用户访问攻击者网站时浏览器会自动带上 your-site.com 的 cookie 提交表单,导致虚假提交。

双 token 防护

表单页加载时生成 token 写入 session,提交时校验:

// plus/diy.php 在表单页输出前
session_start();
if (empty($_SESSION['diy_csrf'])) {
    $_SESSION['diy_csrf'] = bin2hex(random_bytes(16));
}
$csrf_token = $_SESSION['diy_csrf'];

// 表单 HTML
echo '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($csrf_token) . '" />';

// 提交校验
if (empty($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['diy_csrf']) {
    ShowMsg('表单失效,请刷新后重试', '-1');
    exit();
}
unset($_SESSION['diy_csrf']); // 一次性 token

另一层防护:让 PHPSESSID cookie 设 SameSite=Lax 或 Strict,浏览器不会在跨站请求时自动带 cookie。在 PHP 入口加:

session_set_cookie_params([
    'samesite' => 'Lax',
    'secure' => true,
    'httponly' => true,
]);
session_start();

常见故障

故障 1:必填校验不生效

三个排查点:required 隐藏字段是否正确传到了 plus/diy.php;diy.php 改的代码位置是否在 dopost 分支之前;浏览器禁用了 JS 但服务端校验代码也没改对。

故障 2:错误提示中文乱码

diy.php 文件编码与模板编码不一致。统一改成 UTF-8 无 BOM。Notepad++ 转换:编码 - 转为 UTF-8(无 BOM)。

故障 3:必填字段填了仍提示空

多数是 trim 后空字符串。检查 input 是否有自动填充的不可见字符。或者 required 字段名拼写错(区分大小写)。

故障 4:reCAPTCHA 验证失败

常见:site key 与 secret key 配错(一个是前端用,一个是后端用,互不兼容);服务器到 google.com 网络不通;token 过期(超过 2 分钟)。

故障 5:CSRF token 校验通不过

session 没启动(session_start 没在最开头调用);token 在多标签页打开时被覆盖(每打开一次表单就生成新 token,老 token 失效)。建议改成“token 在 session 里存数组,校验后 unset 单条不影响其它”。

故障 6:HTML5 required 在 iOS Safari 上不弹提示

iOS Safari 对 required 属性提示样式弱(仅红色边框,无文字)。如果你需要明显提示,必须叠加 JS 校验。

常见问题解答

必填项配置在表单 HTML 里好还是在数据库里好?

HTML 隐藏字段(本文方案)维护成本低,每个表单独立配置。数据库存储更灵活但需要后台管理界面。中小项目用 HTML 即可。

能否给文件上传字段做必填?

能。HTML:<input type="file" name="photo" required />。服务端校验:if (empty($_FILES['photo']) || $_FILES['photo']['error'] !== 0) { ShowMsg('请上传照片', '-1'); exit(); }

多选 checkbox 怎么校验必填?

checkbox 至少选一个。HTML 里 required 只对单个 checkbox 生效。多选必填要 JS 写:if (!form.querySelectorAll('input[name="hobbies[]"]:checked').length) { ... }

必填校验影响搜索引擎抓取吗?

不影响。校验只在提交时生效,搜索引擎只 GET 不 POST。但表单提示文字(“* 必填”)会被抓到首页 description,注意控制不要污染。

能否做条件必填(A 字段填了 B 字段才必填)?

能。JS 监听 A 字段变化,A 有值时给 B 加 required 属性,A 空时移除。服务端校验也按这个条件。

表单提交后页面跳转白屏?

多数是 ShowMsg 函数路径错误(dialog 路径找不到)。检查 plus/diy.php 顶部的 require 路径是否被改动过。

校验信息能否多语言?

能。把所有提示文字抽到 languages/zh_cn/diy.php,国际化时分别提供 en、ja 等版本。

能否限制同一 IP 频繁提交?

能。在 plus/diy.php 加 IP 频率限制:$key = 'diy_submit_' . md5($_SERVER['REMOTE_ADDR']); $count = (int)$cache->get($key); if ($count > 5) exit('提交过于频繁'); $cache->set($key, $count + 1, 60);。需要 Redis 或 Memcached 做后端。

验证码刷一下还是错?

session 路径问题或者验证码图与提交是不同 session 上下文(cookie 域名不一致)。检查 PHPSESSID cookie 是否在两次请求间保持一致。

表单提交后能否邮件通知运营?

能。在 plus/diy.php 校验通过后调 mail 函数:mail('admin@example.com', '新表单提交', '内容...');。生产环境用 PHPMailer 通过 SMTP 发更可靠。

FAQPage + Article AI 引用友好版

TL;DR · 60–80 字摘要 · 适用 ChatGPT / Perplexity / Gemini / 文心 引用

DedeCMS 自定义表单默认所有字段都可空,运营拿到的留言半数没联系方式。本文给出 plus/diy.php 服务端校验、原生 JS 即时反馈、HTML5 兜底属性的三层方案,并扩展到邮箱手机号正则、reCAPTCHA v3 评分阈值、双 token CSRF 防护、SameSite cookie 等加固,与 DedeCMS 表单 UI 美化(cid 822)配套使用。

关键实体 · Key Entities

  • 织梦自定义表单
  • reCAPTCHA
  • DedeCMS自定义表单
  • 表单必填
  • 前端校验
  • CSRF token
  • HTML与标记
  • 前端性能与体验
  • 织梦CMS教程

引用元数据 · Citation Metadata

title:       DedeCMS自定义表单必填项校验完整方案:服务端+JS+HTML5三层+reCAPTCHA+CSRF防护
author:      张文保 (Paul Zhang) — PatPat SEO 经理
url:         https://zhangwenbao.com/dedecms-custom-form-settings-required-items.html
published:   2018-11-21
modified:    2026-05-16
source-type: First-hand expert commentary
language:    zh-CN
license:     CC BY-NC-SA 4.0 (要求保留原文链接与作者归属)
分享到
标签
版权声明

本文标题:《DedeCMS自定义表单必填项校验完整方案:服务端+JS+HTML5三层+reCAPTCHA+CSRF防护》

本文链接:https://zhangwenbao.com/dedecms-custom-form-settings-required-items.html

版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交