您好!欢迎访问家园网-www.jy.wang!

家园网

PHP 使用 Session 和 PDO Prepare 实现安全登录验证

网络 作者:本站 点击:

PHP 使用 Session 和 PDO Prepare 实现安全登录验证


一、核心组件与流程

  1. 数据库连接‌ - 使用 PDO 建立安全连接

  2. 用户认证‌ - 预处理语句验证用户凭证

  3. 会话管理‌ - 设置安全会话变量

  4. 状态验证‌ - 检查登录状态

  5. 安全登出‌ - 销毁会话数据

二、完整实现代码与解析

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连接参数详解:

参数类型说明
DSNstring数据源名称(host;dbname;charset)
usernamestring数据库用户名
passwordstring数据库密码
optionsarrayPDO连接选项

PDO连接选项:

选项值类型说明
ATTR_ERRMODEint错误报告模式(推荐EXCEPTION)
ATTR_EMULATE_PREPARESbool是否模拟预处理语句
ATTR_DEFAULT_FETCH_MODEint默认结果集获取模式
ATTR_PERSISTENTbool是否使用持久连接

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预处理方法详解:

  1. prepare() - 准备SQL语句

    • 参数: SQL字符串(包含命名占位符)

    • 返回: PDOStatement对象

  2. bindParam() - 绑定参数

    • 参数1: 占位符名称(带冒号)

    • 参数2: 绑定的变量

    • 参数3: 数据类型常量(PDO::PARAM_*)

    • 可选参数: 长度和数据驱动选项

  3. execute() - 执行预处理语句

    • 可选参数: 输入参数数组(替代bindParam)

  4. 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;
?>

三、安全最佳实践

  1. 密码存储

$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
    • 使用password_hash()生成哈希

    • 推荐使用PASSWORD_BCRYPT算法

    • 成本因子至少为12

  1. 会话安全配置

session_set_cookie_params([
    'lifetime' => 0, // 浏览器关闭时过期
    'path' => '/',
    'domain' => $_SERVER['HTTP_HOST'],
    'secure' => true, // 仅HTTPS
    'httponly' => true, // 防止XSS
    'samesite' => 'Strict' // CSRF防护
]);
  1. PDO安全设置

    • 始终禁用模拟预处理

    • 使用异常模式处理错误

    • 设置合适的字符集

  2. 输入验证

    • 过滤所有用户输入

    • 验证数据格式和长度

    • 使用白名单而非黑名单

  3. 防御措施

    • 实现登录尝试限制

    • 记录安全相关事件

    • 定期审计会话活动

四、常见问题解决方案

  1. "Headers already sent"错误

    • 确保session_start()前无输出

    • 检查文件编码(UTF-8无BOM)

  2. 会话不持久

    • 验证session.save_path权限

    • 检查磁盘空间

  3. 性能优化

    • 考虑会话存数据库

    • 实现会话垃圾回收

  4. 跨子域共享会话

ini_set('session.cookie_domain', '.example.com');

通过以上实现,我们建立了一个完整的、安全的PHP登录验证系统,有效防止了SQL注入、会话劫持和其他常见Web攻击。

标签: