表单验证

学习目标

  • 掌握客户端和服务器端验证的重要性
  • 学会使用PHP进行数据验证
  • 理解常见的安全威胁和防护方法
  • 能够创建完整的验证系统

验证的重要性

表单验证是Web开发中的关键环节,它确保:

  1. 数据完整性:确保用户输入的数据格式正确
  2. 安全性:防止恶意数据注入和攻击
  3. 用户体验:提供及时、准确的错误反馈
  4. 系统稳定性:防止无效数据导致程序错误

客户端验证 vs 服务器端验证

客户端验证

  • 优点:即时反馈,减轻服务器负担
  • 缺点:可以被绕过,不安全
  • 技术:JavaScript、HTML5验证属性

服务器端验证

  • 优点:安全可靠,无法绕过
  • 缺点:需要网络请求,用户体验稍差
  • 技术:PHP验证逻辑

PHP验证函数和方法

1. 基本验证

<?php
// 检查是否为空
function validateRequired($value) {
    return !empty(trim($value));
}

// 检查字符串长度
function validateLength($value, $min = 0, $max = PHP_INT_MAX) {
    $length = strlen(trim($value));
    return $length >= $min && $length <= $max;
}

// 检查邮箱格式
function validateEmail($email) {
    return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}

// 检查数字
function validateNumber($value, $min = null, $max = null) {
    if (!is_numeric($value)) {
        return false;
    }

    $num = (float)$value;

    if ($min !== null && $num < $min) {
        return false;
    }

    if ($max !== null && $num > $max) {
        return false;
    }

    return true;
}
?>

2. 高级验证类

<?php
class FormValidator {
    private $errors = [];
    private $data;

    public function __construct($data) {
        $this->data = $data;
    }

    public function required($field, $message = null) {
        if (empty(trim($this->data[$field] ?? ''))) {
            $this->errors[$field] = $message ?? ucfirst($field) . '不能为空';
        }
        return $this;
    }

    public function email($field, $message = null) {
        $value = $this->data[$field] ?? '';
        if (!empty($value) && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
            $this->errors[$field] = $message ?? '邮箱格式不正确';
        }
        return $this;
    }

    public function minLength($field, $min, $message = null) {
        $value = $this->data[$field] ?? '';
        if (!empty($value) && strlen(trim($value)) < $min) {
            $this->errors[$field] = $message ?? ucfirst($field) . '至少需要' . $min . '个字符';
        }
        return $this;
    }

    public function maxLength($field, $max, $message = null) {
        $value = $this->data[$field] ?? '';
        if (!empty($value) && strlen(trim($value)) > $max) {
            $this->errors[$field] = $message ?? ucfirst($field) . '不能超过' . $max . '个字符';
        }
        return $this;
    }

    public function pattern($field, $pattern, $message = null) {
        $value = $this->data[$field] ?? '';
        if (!empty($value) && !preg_match($pattern, $value)) {
            $this->errors[$field] = $message ?? ucfirst($field) . '格式不正确';
        }
        return $this;
    }

    public function getErrors() {
        return $this->errors;
    }

    public function isValid() {
        return empty($this->errors);
    }
}
?>

常见验证场景

1. 用户注册验证

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $data = $_POST;

    $validator = new FormValidator($data);

    $validator->required('username', '用户名不能为空')
              ->minLength('username', 3, '用户名至少3个字符')
              ->maxLength('username', 20, '用户名不能超过20个字符')
              ->pattern('username', '/^[a-zA-Z0-9_]+$/', '用户名只能包含字母、数字和下划线')

              ->required('email', '邮箱不能为空')
              ->email('email', '请输入有效的邮箱地址')

              ->required('password', '密码不能为空')
              ->minLength('password', 8, '密码至少8个字符')
              ->pattern('password', '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/',
                       '密码必须包含大小写字母和数字')

              ->required('confirm_password', '请确认密码')
              ->custom('confirm_password', function($value) use ($data) {
                  return $value === ($data['password'] ?? '');
              }, '两次输入的密码不一致');

    if ($validator->isValid()) {
        // 验证通过,处理注册逻辑
        $username = trim($data['username']);
        $email = trim($data['email']);
        $password = password_hash($data['password'], PASSWORD_DEFAULT);

        // 保存到数据库...
        echo "注册成功!";
    } else {
        // 显示错误信息
        $errors = $validator->getErrors();
        foreach ($errors as $error) {
            echo '<p style="color: red;">' . htmlspecialchars($error) . '</p>';
        }
    }
}
?>

2. 文件上传验证

<?php
function validateFileUpload($file, $allowedTypes = [], $maxSize = 5242880) {
    $errors = [];

    // 检查文件是否上传
    if ($file['error'] !== UPLOAD_ERR_OK) {
        switch ($file['error']) {
            case UPLOAD_ERR_INI_SIZE:
                $errors[] = '文件大小超过服务器限制';
                break;
            case UPLOAD_ERR_FORM_SIZE:
                $errors[] = '文件大小超过表单限制';
                break;
            case UPLOAD_ERR_PARTIAL:
                $errors[] = '文件只有部分被上传';
                break;
            case UPLOAD_ERR_NO_FILE:
                $errors[] = '没有文件被上传';
                break;
            default:
                $errors[] = '未知上传错误';
        }
        return $errors;
    }

    // 检查文件大小
    if ($file['size'] > $maxSize) {
        $errors[] = '文件大小不能超过 ' . ($maxSize / 1024 / 1024) . 'MB';
    }

    // 检查文件类型
    if (!empty($allowedTypes)) {
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mimeType = finfo_file($finfo, $file['tmp_name']);
        finfo_close($finfo);

        if (!in_array($mimeType, $allowedTypes)) {
            $errors[] = '不支持的文件类型';
        }
    }

    // 检查文件扩展名
    $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx'];
    $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));

    if (!in_array($extension, $allowedExtensions)) {
        $errors[] = '不支持的文件扩展名';
    }

    return $errors;
}

// 使用示例
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['avatar'])) {
    $allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
    $maxSize = 2 * 1024 * 1024; // 2MB

    $errors = validateFileUpload($_FILES['avatar'], $allowedTypes, $maxSize);

    if (empty($errors)) {
        // 文件验证通过,处理上传
        echo "文件验证通过";
    } else {
        foreach ($errors as $error) {
            echo '<p style="color: red;">' . htmlspecialchars($error) . '</p>';
        }
    }
}
?>

安全性验证

1. XSS防护

<?php
function sanitizeInput($input) {
    if (is_array($input)) {
        return array_map('sanitizeInput', $input);
    }
    return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}

function sanitizeOutput($output) {
    return htmlspecialchars($output, ENT_QUOTES, 'UTF-8');
}

// 使用示例
$userInput = $_POST['comment'] ?? '';
$safeInput = sanitizeInput($userInput);

// 输出时再次转义
echo sanitizeOutput($safeInput);
?>

2. SQL注入防护

<?php
// 使用预处理语句防止SQL注入
function safeQuery($pdo, $sql, $params = []) {
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    return $stmt;
}

// 使用示例
$pdo = new PDO('mysql:host=localhost;dbname=test', 'username', 'password');

$username = $_POST['username'] ?? '';
$email = $_POST['email'] ?? '';

$sql = "INSERT INTO users (username, email) VALUES (?, ?)";
$params = [$username, $email];

$stmt = safeQuery($pdo, $sql, $params);
?>

3. CSRF防护

<?php
session_start();

function generateCsrfToken() {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        $_SESSION['csrf_token_time'] = time();
    }
    return $_SESSION['csrf_token'];
}

function validateCsrfToken($token, $maxAge = 3600) {
    if (!isset($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $token)) {
        return false;
    }

    // 检查令牌是否过期
    if (isset($_SESSION['csrf_token_time']) && (time() - $_SESSION['csrf_token_time']) > $maxAge) {
        unset($_SESSION['csrf_token']);
        unset($_SESSION['csrf_token_time']);
        return false;
    }

    return true;
}

// 在表单中包含CSRF令牌
$csrfToken = generateCsrfToken();
echo '<input type="hidden" name="csrf_token" value="' . $csrfToken . '">';

// 验证CSRF令牌
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['csrf_token'] ?? '';
    if (!validateCsrfToken($token)) {
        die('CSRF验证失败');
    }
}
?>

AJAX表单验证

JavaScript前端验证

class FormValidator {
    constructor(formId) {
        this.form = document.getElementById(formId);
        this.errors = {};
        this.setupEventListeners();
    }

    setupEventListeners() {
        this.form.addEventListener('submit', (e) => {
            if (!this.validate()) {
                e.preventDefault();
                this.displayErrors();
            }
        });

        // 实时验证
        const inputs = this.form.querySelectorAll('input, textarea, select');
        inputs.forEach(input => {
            input.addEventListener('blur', () => {
                this.validateField(input);
            });
        });
    }

    validate() {
        const inputs = this.form.querySelectorAll('input, textarea, select');
        this.errors = {};

        inputs.forEach(input => {
            this.validateField(input);
        });

        return Object.keys(this.errors).length === 0;
    }

    validateField(input) {
        const name = input.name;
        const value = input.value.trim();
        const type = input.type;

        // 必填验证
        if (input.hasAttribute('required') && !value) {
            this.errors[name] = `${this.getFieldLabel(name)}不能为空`;
            return;
        }

        // 长度验证
        if (input.hasAttribute('minlength') && value.length < parseInt(input.minlength)) {
            this.errors[name] = `${this.getFieldLabel(name)}至少需要${input.minlength}个字符`;
            return;
        }

        // 类型验证
        switch (type) {
            case 'email':
                if (value && !this.isValidEmail(value)) {
                    this.errors[name] = '邮箱格式不正确';
                }
                break;
            case 'tel':
                if (value && !this.isValidPhone(value)) {
                    this.errors[name] = '电话号码格式不正确';
                }
                break;
        }

        // 清除该字段的错误
        if (!this.errors[name]) {
            this.clearFieldError(input);
        }
    }

    isValidEmail(email) {
        const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        return pattern.test(email);
    }

    isValidPhone(phone) {
        const pattern = /^1[3-9]\d{9}$/;
        return pattern.test(phone);
    }

    getFieldLabel(name) {
        const label = this.form.querySelector(`label[for="${name}"]`);
        return label ? label.textContent.replace(':', '') : name;
    }

    displayErrors() {
        Object.keys(this.errors).forEach(fieldName => {
            const field = this.form.querySelector(`[name="${fieldName}"]`);
            if (field) {
                this.showFieldError(field, this.errors[fieldName]);
            }
        });
    }

    showFieldError(field, message) {
        this.clearFieldError(field);

        field.classList.add('error');

        const errorElement = document.createElement('div');
        errorElement.className = 'error-message';
        errorElement.textContent = message;

        field.parentNode.appendChild(errorElement);
    }

    clearFieldError(field) {
        field.classList.remove('error');

        const errorElement = field.parentNode.querySelector('.error-message');
        if (errorElement) {
            errorElement.remove();
        }
    }
}

// 使用示例
document.addEventListener('DOMContentLoaded', () => {
    new FormValidator('registrationForm');
});

总结

表单验证是Web开发中不可或缺的重要环节。通过结合客户端和服务器端验证,使用适当的安全措施,可以创建既安全又用户友好的表单系统。记住:永远不要信任用户的输入,始终在服务器端进行验证。