PHP 使用 Session 和 PDO Prepare 实现安全登录验证
一、核心组件与流程
数据库连接 - 使用 PDO 建立安全连接
用户认证 - 预处理语句验证用户凭证
会话管理 - 设置安全会话变量
状态验证 - 检查登录状态
安全登出 - 销毁会话数据
二、完整实现代码与解析
1. 数据库连接 (db_connect.php)
<?php
// PDO连接参数配置
$dbConfig = [
'host' => 'localhost',
'dbname' => 'user_auth_system',
'charset' => 'utf8mb4',
'username' => 'auth_user',
'password' => 'Secur3P@ssw0rd!'
];
try {
// 创建PDO实例
$pdo = new PDO(
"mysql:host={$dbConfig['host']};dbname={$dbConfig['dbname']};charset={$dbConfig['charset']}",
$dbConfig['username'],
$dbConfig['password'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 错误处理模式
PDO::ATTR_EMULATE_PREPARES => false, // 禁用模拟预处理
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 默认获取模式
PDO::ATTR_PERSISTENT => false // 非持久连接
]
);
} catch (PDOException $e) {
error_log("Database connection failed: " . $e->getMessage());
die("系统维护中,请稍后再试");
}
?>PDO连接参数详解:
PDO连接选项:
2. 登录处理 (login.php)
<?php
require 'db_connect.php';
session_start();
// 1. 输入过滤与验证
$username = filter_input(INPUT_POST, 'username', FILTER_SANITIZE_STRING);
$password = $_POST['password'] ?? '';
if (empty($username) || empty($password)) {
$_SESSION['error'] = '用户名和密码不能为空';
header('Location: login_form.php');
exit;
}
// 2. 预处理查询验证用户
try {
// 准备SQL语句
$stmt = $pdo->prepare("
SELECT
id,
username,
password_hash,
is_active,
login_attempts,
last_login_ip
FROM users
WHERE username = :username
LIMIT 1
");
// 绑定参数
$stmt->bindParam(':username', $username, PDO::PARAM_STR);
// 执行查询
$stmt->execute();
// 获取用户数据
$user = $stmt->fetch();
// 3. 验证用户状态和密码
if ($user) {
// 检查账户是否被锁定
if ($user['login_attempts'] >= 5) {
$_SESSION['error'] = '账户已被锁定,请联系管理员';
header('Location: login_form.php');
exit;
}
// 验证密码
if (password_verify($password, $user['password_hash'])) {
// 密码正确,重置尝试次数
$resetStmt = $pdo->prepare("
UPDATE users
SET login_attempts = 0
WHERE id = :user_id
");
$resetStmt->bindParam(':user_id', $user['id'], PDO::PARAM_INT);
$resetStmt->execute();
// 4. 设置安全会话
session_regenerate_id(true); // 防止会话固定
$_SESSION['auth'] = [
'user_id' => $user['id'],
'username' => $user['username'],
'ip' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
'logged_in_at' => time(),
'last_activity' => time()
];
// 5. 更新最后登录信息
$updateStmt = $pdo->prepare("
UPDATE users
SET
last_login = NOW(),
last_login_ip = :ip
WHERE id = :user_id
");
$updateStmt->bindParam(':ip', $_SERVER['REMOTE_ADDR'], PDO::PARAM_STR);
$updateStmt->bindParam(':user_id', $user['id'], PDO::PARAM_INT);
$updateStmt->execute();
// 重定向到受保护页面
header('Location: dashboard.php');
exit;
} else {
// 密码错误,增加尝试次数
$attemptStmt = $pdo->prepare("
UPDATE users
SET login_attempts = login_attempts + 1
WHERE id = :user_id
");
$attemptStmt->bindParam(':user_id', $user['id'], PDO::PARAM_INT);
$attemptStmt->execute();
$_SESSION['error'] = '用户名或密码错误';
header('Location: login_form.php');
exit;
}
} else {
$_SESSION['error'] = '用户名或密码错误';
header('Location: login_form.php');
exit;
}
} catch (PDOException $e) {
error_log("Login error: " . $e->getMessage());
$_SESSION['error'] = '登录过程中发生错误';
header('Location: login_form.php');
exit;
}
?>PDO预处理方法详解:
prepare()- 准备SQL语句参数: SQL字符串(包含命名占位符)
返回: PDOStatement对象
bindParam()- 绑定参数参数1: 占位符名称(带冒号)
参数2: 绑定的变量
参数3: 数据类型常量(PDO::PARAM_*)
可选参数: 长度和数据驱动选项
execute()- 执行预处理语句可选参数: 输入参数数组(替代bindParam)
fetch()- 获取一行结果可选参数: 获取模式(默认PDO::FETCH_ASSOC)
3. 登录状态检查 (auth_check.php)
<?php
session_start();
// 1. 检查基本会话存在
if (!isset($_SESSION['auth'])) {
header('Location: login.php?redirect=' . urlencode($_SERVER['REQUEST_URI']));
exit;
}
// 2. 验证会话数据完整性
$requiredKeys = ['user_id', 'username', 'ip', 'user_agent', 'logged_in_at', 'last_activity'];
foreach ($requiredKeys as $key) {
if (!array_key_exists($key, $_SESSION['auth'])) {
session_unset();
session_destroy();
header('Location: login.php?reason=invalid_session');
exit;
}
}
// 3. 安全验证
$currentIp = $_SERVER['REMOTE_ADDR'];
$currentUserAgent = $_SERVER['HTTP_USER_AGENT'];
$session = $_SESSION['auth'];
// 3.1 IP验证(可选,对动态IP用户不友好)
if ($session['ip'] !== $currentIp) {
session_unset();
session_destroy();
header('Location: login.php?reason=ip_mismatch');
exit;
}
// 3.2 用户代理验证
if ($session['user_agent'] !== $currentUserAgent) {
session_unset();
session_destroy();
header('Location: login.php?reason=agent_mismatch');
exit;
}
// 3.3 会话超时(30分钟)
if (time() - $session['last_activity'] > 1800) {
session_unset();
session_destroy();
header('Location: login.php?reason=session_timeout');
exit;
}
// 3.4 绝对超时(8小时)
if (time() - $session['logged_in_at'] > 28800) {
session_unset();
session_destroy();
header('Location: login.php?reason=absolute_timeout');
exit;
}
// 4. 更新最后活动时间
$_SESSION['auth']['last_activity'] = time();
// 5. 可选: 从数据库验证用户状态
require 'db_connect.php';
try {
$stmt = $pdo->prepare("
SELECT is_active
FROM users
WHERE id = :user_id
LIMIT 1
");
$stmt->bindParam(':user_id', $_SESSION['auth']['user_id'], PDO::PARAM_INT);
$stmt->execute();
$user = $stmt->fetch();
if (!$user || !$user['is_active']) {
session_unset();
session_destroy();
header('Location: login.php?reason=account_inactive');
exit;
}
} catch (PDOException $e) {
error_log("Auth check error: " . $e->getMessage());
// 可以选择继续或终止会话
}
?>4. 登出处理 (logout.php)
<?php
session_start();
// 1. 清空所有会话数据
$_SESSION = [];
// 2. 删除会话cookie
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(
session_name(), // 会话名称(默认PHPSESSID)
'', // 空值
time() - 42000, // 过期时间(过去)
$params["path"], // 原始路径
$params["domain"], // 原始域名
$params["secure"], // 是否仅HTTPS
$params["httponly"] // 是否仅HTTP访问
);
}
// 3. 销毁会话
session_destroy();
// 4. 重定向到登录页
header("Location: login.php");
exit;
?>三、安全最佳实践
密码存储
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
使用
password_hash()生成哈希推荐使用PASSWORD_BCRYPT算法
成本因子至少为12
会话安全配置
session_set_cookie_params([ 'lifetime' => 0, // 浏览器关闭时过期 'path' => '/', 'domain' => $_SERVER['HTTP_HOST'], 'secure' => true, // 仅HTTPS 'httponly' => true, // 防止XSS 'samesite' => 'Strict' // CSRF防护 ]);
PDO安全设置
始终禁用模拟预处理
使用异常模式处理错误
设置合适的字符集
输入验证
过滤所有用户输入
验证数据格式和长度
使用白名单而非黑名单
防御措施
实现登录尝试限制
记录安全相关事件
定期审计会话活动
四、常见问题解决方案
"Headers already sent"错误
确保
session_start()前无输出检查文件编码(UTF-8无BOM)
会话不持久
验证
session.save_path权限检查磁盘空间
性能优化
考虑会话存数据库
实现会话垃圾回收
跨子域共享会话
ini_set('session.cookie_domain', '.example.com');通过以上实现,我们建立了一个完整的、安全的PHP登录验证系统,有效防止了SQL注入、会话劫持和其他常见Web攻击。