表单验证
学习目标
- 掌握客户端和服务器端验证的重要性
- 学会使用PHP进行数据验证
- 理解常见的安全威胁和防护方法
- 能够创建完整的验证系统
验证的重要性
表单验证是Web开发中的关键环节,它确保:
- 数据完整性:确保用户输入的数据格式正确
- 安全性:防止恶意数据注入和攻击
- 用户体验:提供及时、准确的错误反馈
- 系统稳定性:防止无效数据导致程序错误
客户端验证 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开发中不可或缺的重要环节。通过结合客户端和服务器端验证,使用适当的安全措施,可以创建既安全又用户友好的表单系统。记住:永远不要信任用户的输入,始终在服务器端进行验证。