# 5.15 登录日志 :::tip 概述 登录日志记录用户的登录/登出行为,包括成功和失败记录。每次登录尝试都会记录,便于安全审计和异常行为分析。与操作日志不同,登录日志由 LoginLogic 手动记录,而非中间件自动记录。 ::: ## 记录流程 ```text 用户提交登录 │ ▼ LoginLogic::login() │ ├─ 创建 RequestInfoService(解析 IP、UA、归属地) │ ├─ 验证验证码 CaptchaService::check() │ └─ 失败 → 记录登录日志(status=0, error="验证码错误") │ ├─ 调用 JwtService::login() │ ├─ 查询用户 → 不存在 → 记录登录日志(status=0, error="用户名或密码错误") │ ├─ 校验状态 → 禁用 → 记录登录日志(status=0, error="账号已被禁用") │ ├─ 校验密码 → 错误 → 记录登录日志(status=0, error="用户名或密码错误") │ └─ 签发 Token → 成功 │ ├─ 记录登录日志(status=1, 无错误) │ └─ 返回 { access_token, refresh_token, user } ``` ### 登录/登出记录 ```text ┌─────────────────────────────────────────────────────────────┐ │ 登录成功 │ │ LoginLogic::login() → 成功 → recordLoginLog(username, 1, 0) │ │ LoginController::login() → 成功日志 │ ├─────────────────────────────────────────────────────────────┤ │ 登录失败 │ │ LoginLogic::login() → 异常 → recordLoginLog(username, 0, 1) │ │ LoginController::login() → 失败日志 │ ├─────────────────────────────────────────────────────────────┤ │ 登出 │ │ LoginController::logout() → recordLoginLog(username, 2, 0) │ └─────────────────────────────────────────────────────────────┘ ``` ## 记录字段 | 字段 | 类型 | 说明 | 示例 | |------|------|------|------| | `username` | VARCHAR(50) | 用户名(即使登录失败也记录) | admin | | `type` | TINYINT | 类型:1=登录 2=登出 | 1 | | `status` | TINYINT | 状态:0=失败 1=成功 | 1 | | `message` | VARCHAR(200) | 结果消息 | 登录成功 / 密码错误 | | `ip` | VARCHAR(50) | 客户端 IP | 192.168.1.100 | | `location` | VARCHAR(100) | IP 归属地 | 江苏省南京市 | | `os` | VARCHAR(100) | 操作系统 | Windows 10 | | `browser` | VARCHAR(100) | 浏览器 | Chrome 120 | | `create_time` | DATETIME | 登录时间 | 2026-09-22 10:30:00 | ## 核心代码 ### 登录记录 ```php // app/logic/LoginLogic.php public function login(string $username, string $password, string $captchaKey, string $captchaCode): array { $requestInfo = RequestInfoService::create(request()); try { // 验证验证码 if (!CaptchaService::check($captchaKey, $captchaCode)) { throw new \Exception('验证码错误'); } // 调用 JwtService 登录 $result = $this->jwtService->login($username, $password); // 记录成功日志 $this->recordLoginLog($username, 1, 0, '', '登录成功'); return $result; } catch (\Exception $e) { // 记录失败日志 $this->recordLoginLog($username, 1, 1, $e->getMessage(), '登录失败'); throw $e; } } ``` ### 统一记录方法 ```php /** * 记录登录日志 * * @param string $username 用户名 * @param int $type 类型:1=登录 2=登出 * @param int $status 状态:0=成功 1=失败 * @param string $error 错误信息(成功时为空) * @param string $message 结果消息 */ public function recordLoginLog(string $username, int $type, int $status, string $error = '', string $message = ''): void { $requestInfo = RequestInfoService::create(request()); try { LoginLog::create([ 'username' => $username, 'type' => $type, 'status' => $status, 'message' => $message, 'ip' => $requestInfo->getIp(), 'location' => $requestInfo->getIpLocation(), 'os' => $requestInfo->getOs(), 'browser' => $requestInfo->getBrowser(), ]); } catch (\Exception $e) { // 日志记录失败不影响业务 } } ``` ### 登出记录 ```php // app/controller/LoginController.php #[Log('系统登录-用户登出', Log::TYPE_LOGOUT)] public function logout(): Json { $token = \jwt\Jwt::getTokenFromHeader(); if ($token) { $this->logic->recordLoginLog($this->getLoginUsername(), 2, 0, '', '退出成功'); } return $this->success(null, '登出成功'); } ``` ## API 接口 | 方法 | 路径 | 权限码 | 说明 | |------|------|--------|------| | GET | `/api/login/log/page` | sys:login:log:list | 分页列表 | | GET | `/api/login/log/detail/:id` | sys:login:log:detail | 详情 | | DELETE | `/api/login/log/delete/:id` | sys:login:log:delete | 删除 | | DELETE | `/api/login/log/batchDelete` | sys:login:log:delete | 批量删除 | ## 安全分析 ### 暴力破解检测 ```sql -- 短时间内同一 IP 大量失败登录(5分钟内失败超过10次) SELECT ip, COUNT(*) as fail_count, MIN(create_time) as first_time, MAX(create_time) as last_time FROM think_login_log WHERE status = 0 AND create_time > DATE_SUB(NOW(), INTERVAL 5 MINUTE) GROUP BY ip HAVING fail_count >= 10 ORDER BY fail_count DESC; ``` ### 异常账号检测 ```sql -- 同一账号短时间内大量失败(可能被暴力破解) SELECT username, COUNT(*) as fail_count FROM think_login_log WHERE status = 0 AND create_time > DATE_SUB(NOW(), INTERVAL 1 HOUR) GROUP BY username HAVING fail_count >= 5 ORDER BY fail_count DESC; ``` ### 异地登录检测 ```sql -- 同一用户在不同城市短时间内登录 SELECT username, location, COUNT(*) as login_count FROM think_login_log WHERE status = 1 AND create_time > DATE_SUB(NOW(), INTERVAL 1 DAY) GROUP BY username, location HAVING login_count >= 1; ``` ### 高频恶意 IP ```sql -- 统计失败登录最多的 IP(最近7天) SELECT ip, location, COUNT(*) as fail_count FROM think_login_log WHERE status = 0 AND create_time > DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY ip, location ORDER BY fail_count DESC LIMIT 20; ``` ### 成功率分析 ```sql -- 最近30天登录成功率 SELECT DATE(create_time) as date, COUNT(*) as total, SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success, ROUND(SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) as success_rate FROM think_login_log WHERE type = 1 AND create_time > DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY DATE(create_time) ORDER BY date; ``` ## 与操作日志的区别 | 维度 | 登录日志 | 操作日志 | |------|---------|---------| | 表 | `think_login_log` | `think_operation_log` | | 记录方 | LoginLogic 手动记录 | LogMiddleware 自动记录 | | 记录内容 | 仅登录/登出 | 所有写操作 + 导出/导入 | | 触发方式 | 代码中调用 `recordLoginLog()` | `#[Log]` 注解 | | 用途 | 登录安全审计 | 业务操作审计 | | 失败也记录 | ✓(登录失败必须记录) | ✗(仅记录成功的写操作) | ## 日志保留策略 | 策略 | 说明 | 建议保留时间 | |------|------|-------------| | 按时间清理 | 定期删除 N 天前的日志 | 180 天(6个月) | | 安全合规 | 某些行业要求保留更长时间 | 按合规要求 | ```sql -- 示例:清理 180 天前的登录日志 DELETE FROM think_login_log WHERE create_time < DATE_SUB(NOW(), INTERVAL 180 DAY); ``` ## 最佳实践 | 实践 | 说明 | |------|------| | 成功和失败都记录 | 安全审计需要完整的登录行为记录 | | 统一错误提示 | "用户名或密码错误"不区分用户不存在和密码错误 | | 定期分析 | 定期检查异常登录模式(暴力破解、异地登录) | | IP 黑名单 | 将高频恶意 IP 加入黑名单(Nginx/防火墙层) | | 配合验证码 | 验证码防刷 + 登录日志分析形成双重防护 | | 日志不可篡改 | 登录日志仅 INSERT,无 UPDATE 接口 |