PHP 联系表单处理

联系表单几乎是每个网站的标配,但很多初版实现只用 mail() 直接转发内容,既不校验输入,也不防垃圾评论和伪造发件人。等被灌满垃圾邮件再回头补,成本就高了。下面是一个更稳妥的 PHP 处理方案,输入校验、CSRF 防护、限流和蜜罐字段都覆盖到。

一个基础可用的版本

<?php
// 联系表单处理
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = htmlspecialchars($_POST['name'] ?? '');
    $email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
    $message = htmlspecialchars($_POST['message'] ?? '');
    
    if (!$email) {
        die('请输入有效的邮箱地址');
    }
    
    $to = '[email protected]';
    $subject = "来自 $name 的咨询";
    $headers = "From: $email\nReply-To: $email";
    
    if (mail($to, $subject, $message, $headers)) {
        echo '感谢您的留言,我们会尽快回复!';
    } else {
        echo '发送失败,请稍后重试。';
    }
}
?>

这个版本能跑,但有三处需要补:没有验证码或限流、发件人直接用访客邮箱容易被标记为垃圾邮件、缺少 CSRF 防护。

加一层防垃圾与安全防护

防护手段 作用
CSRF Token 防止跨站伪造提交
会话限流 同一会话每小时最多提交 3 次
蜜罐字段 隐藏的输入框,机器人填了就当垃圾丢弃
邮件头净化 使用固定 From 地址,防止邮件头注入

CSRF 的做法是:开会话后生成一次性 token 放进隐藏字段,提交时比对;限流则在会话里记录提交时间和次数。蜜罐字段对真人不可见,却很容易被自动脚本填满。把这三者叠到一起,绝大多数自动刷单就进不来了。

在真实环境里,垃圾表单比很多人想象的更凶猛。我见过一个小企业官网,上线第二周后三个月内被灌了四千多封垃圾留言,基本都是"加微信""贷款"之类的推广,人工后台根本清理不过来。后来补上蜜罐字段和会话限流,垃圾量立刻降到接近零。这说明防护不是可选项,而是上线前就该做好的基础工作。

蜜罐的做法很简单:在表单里放一个用 CSS 隐藏的输入框,命名随意(比如 website),真人看不到也不会填,自动脚本却常常照单全收。提交时如果这个字段有值,直接按机器人处理——可以静默返回成功,也可以返回 403,但不要给机器人任何"提交失败"的信号,否则它会换着脚本不断重试。

更完整的处理流程

生产环境建议把逻辑拆开:先解析输入(支持普通 POST 和 AJAX JSON),再校验 CSRF 和字段长度,最后通过邮件服务发送。

<?php
session_start();
if (!isset($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

$input = ($_SERVER['CONTENT_TYPE'] === 'application/json')
    ? json_decode(file_get_contents('php://input'), true)
    : $_POST;

// CSRF 校验
if (!isset($input['csrf_token']) || $input['csrf_token'] !== $_SESSION['csrf_token']) {
    http_response_code(403);
    exit('请求已过期,请刷新页面重试');
}

// 字段校验
$email = filter_var($input['email'] ?? '', FILTER_VALIDATE_EMAIL);
if (!$email) {
    http_response_code(422);
    exit('请输入有效的邮箱地址');
}

// 蜜罐字段:隐藏输入框被填充说明是机器人
if (!empty($input['website'])) {
    http_response_code(200);
    exit('感谢您的留言!');
}

// 使用固定 From 地址发送,避免伪造发件人
$headers = "From: 网站联系表单 <[email protected]>\r\nReply-To: $email\r\n";
if (mail('[email protected]', '网站新留言', $input['message'] ?? '', $headers)) {
    echo '感谢您的留言,我们会尽快回复!';
} else {
    http_response_code(500);
    echo '发送失败,请稍后重试。';
}
?>

三种发送方式的对比

方式 送达率 维护成本 适用场景
mail() 低,易进垃圾箱 最低 本地开发、临时测试
SMTP(企业邮箱/邮件服务商) 高,配合 SPF/DKIM 正式生产环境
第三方表单服务 / 邮件 API 高,自带防滥用 低(按量付费) 快速上线、不想维护服务器

选择哪种方式,取决于你对送达率与维护成本的权衡。mail() 依赖服务器自带的 MTA,很多云主机的默认配置早已被各大邮箱服务商拉黑,发出去大概率进垃圾箱。SMTP 则需要在邮箱服务商后台配好 SPF、DKIM 记录,首次配置有一点学习成本,但一劳永逸。如果不想碰这些细节,第三方表单服务(如 Formspree、Basin)或邮件 API(如 Resend)更省心,缺点是按量计费、数据经过第三方。

字段校验与长度限制

除了邮箱格式,字段长度和内容本身也要约束。一组常用的边界值:

字段 建议限制 原因
姓名 2-50 字 过短可能是随机字符,过长没意义
邮箱 3-254 字符 遵循邮箱地址长度规范
留言 10-2000 字 太短没内容,太长可能是灌水或攻击
网址(蜜罐) 必须为空 非空即机器人

长度校验在 filter_var 之外再补一层 mb_strlen() 判断即可,中文按字符而不是按字节计算。返回给前端的错误信息也要区分场景:字段错误返回 422,会话过期返回 403,服务器异常返回 500,前端据此给用户不同的提示文案。

mail() 迁移到 SMTP

如果你的站点还在用 mail() 且经常丢信,迁移到 SMTP 通常一个下午就能完成:先在邮箱服务商或你的企业邮箱拿到 SMTP 主机、端口与账号;在 DNS 添加 SPF 和 DKIM 记录;再把发送代码从 mail() 换成 PHPMailer 或 Symfony Mailer 的 SmtpTransport。迁移后建议各发一封测试邮件到 QQ 邮箱、163 邮箱和 Gmail,确认都能进收件箱而不是垃圾箱。具体配置可以对照SMTP 配置指南。

场景建议

如果只是个人博客的简单留言,上面这版就够用;如果是企业官网或需要接客服系统的站点,建议把提交记录落到数据库、接入后台通知,并考虑使用第三方表单服务或邮件 API。字段校验、CSRF 和限流的细节可以对照联系表单 AJAX 交互,想避免发信进垃圾箱,建议改用 SMTP 发送,相关配置可参考SMTP 配置指南,送达率优化可以看邮件送达率优化,自动化营销场景可参考邮件自动化工作流。

常见问题

  • mail() 发送失败或进垃圾箱怎么办? 服务器自带的 mail 功能不稳定,优先切换 SMTP,并做好 SPF/DKIM 配置。
  • 为什么不能直接信任用户输入? 邮件头注入、XSS 和伪造发件人都是真实风险,输入一律过滤、输出一律转义。
  • 返回 JSON 而不是 HTML 给 AJAX 用可以吗? 可以,把成功和失败统一成 {success: true, message: "..."} 的结构,前端解析更省事,交互细节见联系表单 AJAX 交互。
  • 什么时候需要上验证码? 蜜罐 + 限流已能挡掉大部分机器人;如果垃圾量仍然大,再叠加图形验证码或 Cloudflare Turnstile,别一上来就用验证码拖慢真实用户。

参考

参考:PHP filter_var 文档 https://www.php.net/manual/zh/function.filter-var.php
参考:PHP mail 函数 https://www.php.net/manual/zh/function.mail.php
参考:PHP session 文档 https://www.php.net/manual/zh/book.session.php
参考:PHPMailer https://github.com/PHPMailer/PHPMailer