本文目录导读:

在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} 因多次登录失败已被锁定"
]);
}
注意事项
- 安全性考虑:建议将失败记录存储在数据库中,并定期清理过期数据
- 性能优化:对表添加索引以提高查询效率
- 用户体验:提供清晰友好的提示信息,说明锁定原因和剩余时间
- 测试建议:进行充分的边界测试,确保不会出现误锁定情况
这样实现后,你的ThinkPHP项目就具备了基本的登录失败锁定机制,能有效防止暴力破解攻击。