本文目录导读:

在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项目就具备了基本的登录失败锁定机制,能有效防止暴力破解攻击。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播