# 5.15 登录日志 :::tip 概述 登录日志记录用户的登录/登出行为,**成功与失败均记录**,便于安全审计和异常行为分析。与操作日志不同,登录日志由 `AuthController` 手动记录(`writeLoginLog`),而非中间件自动记录;登录日志的 `param` 只记录用户名,不涉及密码。 ::: ## 记录流程 ```text 用户提交登录 │ ▼ AuthController::login(LoginRequest) │ ├─ LoginRequest 参数格式校验 │ ├─ CaptchaService::verify(key, code) │ └─ 失败 → 写登录日志(status=1 失败, error="验证码错误")→ 返回失败 │ ├─ JwtService::login() │ ├─ 查询用户 → 不存在 → BizException → 写登录日志(失败, "用户名或密码错误") │ ├─ 校验状态 → 禁用 → BizException → 写登录日志(失败, "账号已被禁用") │ ├─ 校验密码 → 错误 → BizException → 写登录日志(失败, "用户名或密码错误") │ └─ 签发 Token → 成功 │ ├─ 写登录日志(status=0 成功,含耗时/归属地/浏览器) │ └─ 返回 { access_token, refresh_token, token_type, expires_in, user } ← raw snake_case ``` :::tip 双写保险 无论走成功分支还是异常分支,`AuthController` 都会调用 `writeLoginLog`——**登录尝试必留痕**(含失败原因),这是登录安全审计的基础。 ::: ### 登录/登出记录 ```text ┌─────────────────────────────────────────────────────────────┐ │ 登录成功 │ │ AuthController::login() 成功 → writeLoginLog(成功) │ ├─────────────────────────────────────────────────────────────┤ │ 登录失败(验证码/密码/状态任一失败) │ │ AuthController::login() 异常 → writeLoginLog(失败 + error) │ ├─────────────────────────────────────────────────────────────┤ │ 登出 │ │ AuthController::logout() → revokeToken() 吊销令牌 │ │ → LoginLog::create(log_type=2) │ └─────────────────────────────────────────────────────────────┘ ``` ## 记录字段 | 字段 | 类型 | 说明 | 示例 | |------|------|------|------| | `username` | VARCHAR(50) | 用户名(即使登录失败也记录) | admin | | `log_type` | TINYINT | 类型:1=登录 2=登出 | 1 | | `source` | TINYINT | 来源端:0=后台 1=前台 | 0 | | `method` | VARCHAR(10) | HTTP 方法 | POST | | `request_method` | VARCHAR(50) | 入口标识 | login | | `url` | VARCHAR(255) | 请求 URL | /api/login | | `param` | TEXT | 请求参数——**只记录用户名**(JSON) | {"username":"admin"} | | `result` | TEXT | 结果(JSON:code + msg) | {"code":0,"msg":"登录成功"} | | `status` | TINYINT | 状态:0=成功 1=失败 | 0 | | `error` | VARCHAR(200) | 错误信息(仅失败时写入) | 用户名或密码错误 | | `ip` | VARCHAR(50) | 客户端 IP(经 ProxyMiddleware 还原) | 192.168.1.100 | | `location` | VARCHAR(100) | IP 归属地(ip2region) | 江苏省南京市 | | `os` | VARCHAR(100) | 操作系统 | Windows 11 | | `browser` | VARCHAR(100) | 浏览器 | Chrome 128 | | `consume_time` | INT | 登录耗时(毫秒) | 260 | | `create_time` | DATETIME | 登录时间 | 2026-10-02 10:30:00 | :::tip 耗时字段的用途 登录耗时异常增大可能是 bcrypt 暴力破解尝试、数据库慢查询或 Redis 阻塞的信号——安全巡检时可按 `consume_time` 排序排查。 ::: ## 核心代码 ### 登录记录(AuthController) ```php // app/Http/Controllers/Api/AuthController.php(节选) public function login(LoginRequest $request): JsonResponse { // ... 参数与验证码校验 ... $startTime = microtime(true); try { $result = $this->jwtService->login($username, $password); $this->writeLoginLog($request, $username, 'login', true, '登录成功', $startTime); return $this->success($result, '登录成功'); } catch (\Exception $e) { $this->writeLoginLog($request, $username, 'login', false, $e->getMessage(), $startTime); return $this->fail($e->getMessage()); } } private function writeLoginLog(Request $request, string $username, string $entry, bool $success, string $message, float $startTime): void { $duration = round((microtime(true) - $startTime) * 1000); $requestInfo = RequestInfoService::create($request); $data = [ 'username' => $username, 'log_type' => 1, // 1=登录 'source' => 0, // 0=后台 'method' => $request->method(), 'request_method' => $entry, // login 等入口 'url' => $request->fullUrl(), 'param' => json_encode(['username' => $username], JSON_UNESCAPED_UNICODE), // 只记录用户名 'result' => json_encode(['code' => $success ? 0 : 1, 'msg' => $message], JSON_UNESCAPED_UNICODE), 'ip' => $requestInfo->getIp(), 'location' => $requestInfo->getIpLocation(), 'os' => $requestInfo->getOs(), 'browser' => $requestInfo->getBrowser(), 'consume_time' => $duration, 'status' => $success ? 0 : 1, // 0=成功 1=失败 'create_time' => now(), ]; if (! $success) { $data['error'] = $message; } LoginLog::create($data); } ``` ### 登出记录(AuthController::logout) ```php public function logout(Request $request): JsonResponse { $token = $request->bearerToken(); if ($token) { $this->jwtService->revokeToken($token); // 吊销 refresh_token(即时失效) $requestInfo = RequestInfoService::create($request); $userInfo = $request->attributes->get('userInfo'); // 登出日志:log_type=2 LoginLog::create([ 'username' => $userInfo->username ?? '', 'log_type' => 2, 'source' => 0, // ... 其余字段同登录记录 ... ]); } return $this->success(null, '登出成功'); } ``` ## API 接口 | 方法 | 路径 | 权限码 | 说明 | |------|------|--------|------| | GET | `/api/login/log/page` | sys:loginLog:page | 分页列表 | | GET | `/api/login/log/detail/{id}` | sys:loginLog:detail | 详情 | | DELETE | `/api/login/log/delete/{id}` | sys:loginLog:delete | 删除 | | DELETE | `/api/login/log/batchDelete` | sys:loginLog:batchDelete | 批量删除 | ## 安全分析 ### 暴力破解检测 ```sql -- 短时间内同一 IP 大量失败登录(5分钟内失败超过10次) SELECT ip, COUNT(*) as fail_count, MIN(create_time) as first_time, MAX(create_time) as last_time FROM laravel_login_log WHERE status = 1 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 laravel_login_log WHERE status = 1 AND create_time > DATE_SUB(NOW(), INTERVAL 1 HOUR) GROUP BY username HAVING fail_count >= 5 ORDER BY fail_count DESC; ``` ### 高频恶意 IP ```sql -- 统计失败登录最多的 IP(最近7天) SELECT ip, location, COUNT(*) as fail_count FROM laravel_login_log WHERE status = 1 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 = 0 THEN 1 ELSE 0 END) as success, ROUND(SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) as success_rate FROM laravel_login_log WHERE log_type = 1 AND create_time > DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY DATE(create_time) ORDER BY date; ``` :::tip status 语义提醒 本项目 `status`:**0=成功,1=失败**(与操作日志一致)。上面的检测 SQL 已按此约定编写。 ::: ## 与操作日志的区别 | 维度 | 登录日志 | 操作日志 | |------|---------|---------| | 表 | `laravel_login_log` | `laravel_operation_log` | | 记录方 | `AuthController` 手动记录 | `LogOperation` 中间件(terminate 落库) | | 记录内容 | 仅登录/登出 | 写操作 + 导出/导入/下载 | | 触发方式 | 代码中 `writeLoginLog()` | `#[OperationLog]` 注解 | | 用途 | 登录安全审计 | 业务操作审计 | | 失败也记录 | ✓(登录失败必须记录) | ✓(响应失败状态也记录) | ## 日志保留策略 | 策略 | 说明 | 建议保留时间 | |------|------|-------------| | 按时间清理 | 定期删除 N 天前的日志 | 180 天(6个月) | | 安全合规 | 某些行业要求保留更长时间 | 按合规要求 | ```sql -- 示例:清理 180 天前的登录日志 DELETE FROM laravel_login_log WHERE create_time < DATE_SUB(NOW(), INTERVAL 180 DAY); ``` ## 最佳实践 | 实践 | 说明 | |------|------| | 成功和失败都记录 | 安全审计需要完整的登录行为记录 | | 统一错误提示 | "用户名或密码错误"不区分用户不存在和密码错误 | | 定期分析 | 定期检查异常登录模式(暴力破解、异地登录) | | IP 黑名单 | 将高频恶意 IP 加入黑名单(Nginx/防火墙层) | | 配合验证码 | 验证码防刷 + 登录日志分析形成双重防护(配合 IP 限流,见 [5.3 验证码机制](/zh/core/captcha.html)) |