ThinkPHP项目登录失败锁定策略

wen PHP项目 3

本文目录导读:

ThinkPHP项目登录失败锁定策略

  1. 数据库设计
  2. 配置项
  3. 核心逻辑类
  4. 在登录控制器中使用
  5. 前端提示优化(可选)
  6. 高级特性扩展
  7. 注意事项

在ThinkPHP项目中实现登录失败锁定策略,主要是为了防止暴力破解密码,以下是一个完整的实现方案,包含数据库设计、核心逻辑代码和配置说明。

数据库设计

我们需要一张表来记录登录失败的情况:

CREATE TABLE `login_attempts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '登录用户名',
  `ip_address` varchar(45) NOT NULL COMMENT '登录IP地址',
  `attempt_time` int(11) NOT NULL COMMENT '尝试时间戳',
  `is_locked` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否锁定:0否,1是',
  `lock_until` int(11) DEFAULT NULL COMMENT '锁定截止时间戳',
  PRIMARY KEY (`id`),
  KEY `username` (`username`),
  KEY `ip_address` (`ip_address`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='登录失败记录表';

配置项

config/login.php 中添加以下配置:

<?php
return [
    // 登录失败次数限制
    'max_attempts' => 5,           // 允许的最大失败次数
    'lock_time' => 600,             // 锁定时间(秒),默认10分钟
    'lock_by_ip' => true,          // 是否启用IP锁定
    'lock_by_username' => true,    // 是否按用户名锁定
    'check_interval' => 3600,      // 检查时间范围(秒),1小时内的记录
];

核心逻辑类

创建 app/common/service/LoginSecurity.php

<?php
namespace app\common\service;
use think\facade\Db;
use think\facade\Config;
class LoginSecurity
{
    /**
     * 检查用户是否被锁定
     * @param string $username 用户名
     * @param string $ip IP地址
     * @return array [是否锁定, 剩余时间]
     */
    public static function isLocked($username = '', $ip = '')
    {
        $config = Config::get('login');
        $now = time();
        // 按用户名查锁
        if ($config['lock_by_username'] && !empty($username)) {
            $userLocked = Db::name('login_attempts')
                ->where('username', $username)
                ->where('is_locked', 1)
                ->where('lock_until', '>', $now)
                ->find();
            if ($userLocked) {
                $remaining = $userLocked['lock_until'] - $now;
                return [true, $remaining];
            }
        }
        // 按IP查锁
        if ($config['lock_by_ip'] && !empty($ip)) {
            $ipLocked = Db::name('login_attempts')
                ->where('ip_address', $ip)
                ->where('is_locked', 1)
                ->where('lock_until', '>', $now)
                ->find();
            if ($ipLocked) {
                $remaining = $ipLocked['lock_until'] - $now;
                return [true, $remaining];
            }
        }
        return [false, 0];
    }
    /**
     * 记录登录失败
     * @param string $username 用户名
     * @param string $ip IP地址
     * @return bool 是否达到锁定阈值
     */
    public static function recordFailed($username = '', $ip = '')
    {
        $config = Config::get('login');
        $now = time();
        $isLocked = false;
        // 记录本次失败
        Db::name('login_attempts')->insert([
            'username' => $username,
            'ip_address' => $ip,
            'attempt_time' => $now,
            'is_locked' => 0,
            'lock_until' => null
        ]);
        // 清理过期记录
        self::cleanExpiredRecords();
        // 按用户名统计失败次数
        if ($config['lock_by_username'] && !empty($username)) {
            $usernameAttempts = Db::name('login_attempts')
                ->where('username', $username)
                ->where('attempt_time', '>=', $now - $config['check_interval'])
                ->count();
            if ($usernameAttempts >= $config['max_attempts']) {
                self::lockAccount($username, '', $now + $config['lock_time']);
                $isLocked = true;
            }
        }
        // 按IP统计失败次数
        if ($config['lock_by_ip'] && !empty($ip)) {
            $ipAttempts = Db::name('login_attempts')
                ->where('ip_address', $ip)
                ->where('attempt_time', '>=', $now - $config['check_interval'])
                ->count();
            if ($ipAttempts >= $config['max_attempts']) {
                self::lockAccount('', $ip, $now + $config['lock_time']);
                $isLocked = true;
            }
        }
        return $isLocked;
    }
    /**
     * 锁定账户或IP
     * @param string $username 用户名
     * @param string $ip IP地址
     * @param int $lockUntil 锁定截止时间戳
     */
    private static function lockAccount($username, $ip, $lockUntil)
    {
        $condition = [];
        if (!empty($username)) {
            $condition['username'] = $username;
            $condition['is_locked'] = 0;
        }
        if (!empty($ip)) {
            $condition['ip_address'] = $ip;
            $condition['is_locked'] = 0;
        }
        Db::name('login_attempts')
            ->where($condition)
            ->update([
                'is_locked' => 1,
                'lock_until' => $lockUntil
            ]);
    }
    /**
     * 登录成功后清除失败记录
     * @param string $username 用户名
     * @param string $ip IP地址
     */
    public static function clearAttempts($username = '', $ip = '')
    {
        $condition = [];
        if (!empty($username)) {
            $condition['username'] = $username;
        }
        if (!empty($ip)) {
            $condition['ip_address'] = $ip;
        }
        if (!empty($condition)) {
            Db::name('login_attempts')->where($condition)->delete();
        }
    }
    /**
     * 清理过期的锁定记录
     */
    private static function cleanExpiredRecords()
    {
        $now = time();
        // 清理超过锁定时间的记录
        Db::name('login_attempts')
            ->where('lock_until', '<', $now)
            ->where('is_locked', 1)
            ->delete();
        // 清理超过检查时间范围的失败记录
        $config = Config::get('login');
        Db::name('login_attempts')
            ->where('attempt_time', '<', $now - ($config['check_interval'] * 2))
            ->delete();
    }
    /**
     * 获取剩余锁定时间(人类可读格式)
     * @param int $seconds 剩余秒数
     * @return string
     */
    public static function formatLockTime($seconds)
    {
        $minutes = ceil($seconds / 60);
        if ($minutes < 60) {
            return $minutes . '分钟';
        }
        $hours = floor($minutes / 60);
        $remMinutes = $minutes % 60;
        return $hours . '小时' . $remMinutes . '分钟';
    }
}

在登录控制器中使用

<?php
namespace app\index\controller;
use think\facade\View;
use think\facade\Session;
use think\facade\Config;
use app\common\service\LoginSecurity;
use app\common\model\User;
class Login extends Base
{
    /**
     * 登录处理
     */
    public function doLogin()
    {
        if (request()->isPost()) {
            $username = input('post.username', '', 'trim');
            $password = input('post.password', '', 'trim');
            $ip = request()->ip();
            // 获取客户端真实IP(如果使用了代理)
            $ip = request()->header('X-Real-IP', $ip);
            // 验证数据完整性
            if (empty($username) || empty($password)) {
                return json(['code' => 0, 'msg' => '用户名和密码不能为空']);
            }
            // 检查是否被锁定
            list($isLocked, $remaining) = LoginSecurity::isLocked($username, $ip);
            if ($isLocked) {
                $lockTime = LoginSecurity::formatLockTime($remaining);
                return json(['code' => 0, 'msg' => '账户已被锁定,请 ' . $lockTime . ' 后再试']);
            }
            // 验证用户
            $user = User::where('username', $username)->find();
            if (!$user || !$this->verifyPassword($password, $user->password)) {
                // 记录失败
                $isLocked = LoginSecurity::recordFailed($username, $ip);
                if ($isLocked) {
                    $lockTime = LoginSecurity::formatLockTime(Config::get('login.lock_time'));
                    return json(['code' => 0, 'msg' => '登录失败次数过多,账户已被锁定 ' . $lockTime]);
                }
                return json(['code' => 0, 'msg' => '用户名或密码错误']);
            }
            // 登录成功,清除记录
            LoginSecurity::clearAttempts($username, $ip);
            // 设置session等...
            Session::set('user_id', $user->id);
            Session::set('username', $user->username);
            return json(['code' => 1, 'msg' => '登录成功', 'url' => '/index/index']);
        }
        return View::fetch();
    }
    /**
     * 密码验证
     */
    private function verifyPassword($inputPassword, $storedPassword)
    {
        // 根据你的加密方式调整
        return password_verify($inputPassword, $storedPassword);
    }
}

前端提示优化(可选)

在登录页面添加JavaScript实时检查:

// 登录表单提交前检查
function checkLoginLock() {
    const username = $('#username').val();
    const ip = ''; // 后台会处理IP
    $.ajax({
        url: '/login/checkLock',
        type: 'POST',
        data: {username: username},
        success: function(response) {
            if (response.code === 1) {
                // 显示锁定提示
                $('#login-error').html(response.msg);
            } else {
                $('#login-form').submit();
            }
        }
    });
}

高级特性扩展

1 支持多种锁定策略组合

// 增加配置
'lock_by' => ['username', 'ip'],  // 可以组合使用
// 更新最多允许失败的次数(可按用户名、IP分别配置)
'max_attempts' => [
    'username' => 5,   // 用户名最多5次
    'ip' => 10,        // IP最多10次
],

2 增加人机验证

// 超过一定失败次数后要求验证码
'captcha_threshold' => 3,  // 失败3次后需要验证码

3 增加通知功能

在账户被锁定时,可以通过邮件或在管理后台通知管理员:

// 在lockAccount方法中添加通知
if ($isLocked) {
    // 发送通知邮件
    NotificationService::send([
        'subject' => '账户锁定通知',
        'message' => "用户 {$username} 因多次登录失败已被锁定"
    ]);
}

注意事项

  1. 安全性考虑:建议将失败记录存储在数据库中,并定期清理过期数据
  2. 性能优化:对表添加索引以提高查询效率
  3. 用户体验:提供清晰友好的提示信息,说明锁定原因和剩余时间
  4. 测试建议:进行充分的边界测试,确保不会出现误锁定情况

这样实现后,你的ThinkPHP项目就具备了基本的登录失败锁定机制,能有效防止暴力破解攻击。

抱歉,评论功能暂时关闭!