9.9 KiB
9.9 KiB
5.15 登录日志
:::tip 概述
登录日志记录用户的登录/登出行为,成功与失败均记录,便于安全审计和异常行为分析。与操作日志不同,登录日志由 AuthController 手动记录(writeLoginLog),而非中间件自动记录;登录日志的 param 只记录用户名,不涉及密码。
:::
记录流程
用户提交登录
│
▼
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——登录尝试必留痕(含失败原因),这是登录安全审计的基础。
:::
登录/登出记录
┌─────────────────────────────────────────────────────────────┐
│ 登录成功 │
│ 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)
// 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)
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 | 批量删除 |
安全分析
暴力破解检测
-- 短时间内同一 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;
异常账号检测
-- 同一账号短时间内大量失败(可能被暴力破解)
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
-- 统计失败登录最多的 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;
成功率分析
-- 最近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个月) |
| 安全合规 | 某些行业要求保留更长时间 | 按合规要求 |
-- 示例:清理 180 天前的登录日志
DELETE FROM laravel_login_log WHERE create_time < DATE_SUB(NOW(), INTERVAL 180 DAY);
最佳实践
| 实践 | 说明 |
|---|---|
| 成功和失败都记录 | 安全审计需要完整的登录行为记录 |
| 统一错误提示 | "用户名或密码错误"不区分用户不存在和密码错误 |
| 定期分析 | 定期检查异常登录模式(暴力破解、异地登录) |
| IP 黑名单 | 将高频恶意 IP 加入黑名单(Nginx/防火墙层) |
| 配合验证码 | 验证码防刷 + 登录日志分析形成双重防护(配合 IP 限流,见 5.3 验证码机制) |