Files
2026-10-08 17:50:29 +08:00

23 KiB
Raw Permalink Blame History

4.10 安全架构设计

:::tip 概述 本章详细阐述系统的安全架构设计,围绕认证、授权、防护、审计四大安全领域展开,旨在帮助开发者全面理解系统的安全保障体系。 :::

认证(Authentication):系统采用基于 JWT 的双令牌认证机制,用户通过用户名密码登录后获取 Token,后续请求通过 Authorization: Bearer <token> 头传递,由 Authenticate 中间件(别名 auth.jwt)统一拦截并验证 Token 的有效性(签名校验、有效期检查),签名密钥启动时 fail-close 校验。

授权(Authorization):基于 RBAC 模型设计,权限控制粒度细化至接口级。用户通过角色继承权限节点(如 sys:user:add),由 #[Permission] 注解在控制器层声明,Authenticate 中间件反射校验(三级缓存:Redis → Laravel Cache → 数据库)。超级管理员(uid=1)自动放行。

防护(Protection):系统从多维度构建安全防护体系。参数校验通过 FormRequest 在 HTTP 入口自动拦截非法输入;敏感密码采用 bcrypt 双重加盐哈希存储;演示模式下 DemoMiddleware 统一拦截写操作;跨域配置精确控制可信域名;Nginx 仅允许入口 PHP 执行。

审计(Audit):系统提供完备的操作审计能力。LogOperation 中间件自动记录所有写操作的请求路径、参数(排除敏感字段)、操作人、IP 归属地及执行结果;登录日志独立记录每次登录尝试,便于安全事件追溯。

:::tip 安全设计原则

  • 纵深防御:认证、授权、参数校验多层防护,单一防线失效时仍有其他机制兜底
  • 最小权限:用户仅能访问已授权接口,角色权限按需分配
  • 数据不落地:敏感信息(Token、密码)不在日志中明文记录(操作日志 param 排除 password/token),密码哈希不可逆
  • 安全默认:JWT_SECRET 弱密钥拒绝启动(fail-close),生产 APP_DEBUG=false 隐藏错误详情 :::

安全架构总览

┌─────────────────────────────────────────────────────────────────┐
│                         安全防御层次                              │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │ 网络层防护                                                 │  │
│  │ ┌──────────┐ ┌──────────────┐ ┌────────────────────────┐ │  │
│  │ │ CORS     │ │ 上传体积限制 │ │ Nginx 仅入口 index.php │ │  │
│  │ │ 跨域控制 │ │ DoS 防御     │ │ 可见性攻击防护         │ │  │
│  │ └──────────┘ └──────────────┘ └────────────────────────┘ │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │ 认证层                                                     │  │
│  │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐  │  │
│  │ │ JWT 认证 │ │ 验证码   │ │ 双令牌   │ │ 密钥 fail-   │  │  │
│  │ │ Bearer   │ │ 图形验证 │ │ access+  │ │ close 启动   │  │  │
│  │ │          │ │ + IP限流 │ │ refresh  │ │ 校验         │  │  │
│  │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘  │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │ 授权层                                                     │  │
│  │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐   │  │
│  │ │ RBAC 权限    │ │ 演示模式保护 │ │ 超级管理员跳过   │   │  │
│  │ │ #[Permission]│ │ DemoMiddleware│ │ uid=1 bypass     │   │  │
│  │ └──────────────┘ └──────────────┘ └──────────────────┘   │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │ 数据层                                                     │  │
│  │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐  │  │
│  │ │ 密码加密 │ │ 参数校验 │ │ SQL 注入 │ │ 操作日志     │  │  │
│  │ │ 双重加盐 │ │FormRequest│ │ ORM 防护 │ │ 审计追踪     │  │  │
│  │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘  │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

一、认证机制

登录流程

1. 前端提交 { username, password, key, code }
   (验证码参数 key/code 与 /api/captcha 响应字段一致)
                          │
                          ▼
2. CaptchaService::verify(key, code)      ← 验证码校验(配置 CAPTCHA_LENGTH>0 时启用)
   │ 失败 → 返回"验证码错误"
   ▼
3. JwtService::login(username, password)  ← 查询用户 + 密码校验
   │ 用户不存在/密码错误 → 统一提示(不暴露账号是否存在)
   ▼
4. password_verify(password + salt, hash) ← bcrypt 双重加盐校验
   │ 不匹配 → 统一提示
   ▼
5. 检查 user.status == 1                  ← 账号状态
   │ 非启用 → 返回"账号已被禁用"
   ▼
6. 签发双令牌 + 写登录日志(成功/失败均记录)
   │
   ▼
7. 返回 { access_token, refresh_token, token_type, expires_in, user }

JWT 双令牌认证

[前端]                          [后端]
   │                               │
   │── POST /api/login ──────────→│  (公开接口,中间件组外)
   │   {username, password,        │── 验证码校验(key/code)
   │    key, code}                 │── 密码校验(双重加盐 bcrypt)
   │                               │── 签发 access_token + refresh_token
   │←─ {access_token, ─────────────│
   │    refresh_token}             │
   │                               │
   │── GET /api/xxx ─────────────→│  (认证接口,auth.jwt 组内)
   │   Authorization:              │── JwtService::parseToken
   │   Bearer <access_token>       │── 校验签名 + 有效期 + type=access
   │                               │── 注入 attributes['userInfo']
   │←─ {data} ────────────────────│
   │                               │
   │── access_token 过期 ─────────→│
   │── POST /api/refresh ─────────→│  (公开接口)
   │   {refresh_token}             │── 校验 refresh 类型 + Redis jti(一次性消费)
   │←─ {新令牌对} ─────────────────│

Token 设计

Token 类型 默认有效期 用途 存储
access_token 2 小时(JWT_ACCESS_TTL=7200) 接口认证 无状态(JWT)
refresh_token 7 天(JWT_REFRESH_TTL=604800) 刷新令牌 Redis 有状态(支持踢下线、改密全端失效、一次性消费)

安全要点

  1. 密码错误统一提示:无论用户不存在还是密码错误,统一提示,避免暴露账号是否存在
  2. 账号状态校验:status != 1 时禁止登录
  3. Token 类型校验:access 与 refresh 通过 type 字段区分,不可混用(refresh 令牌不能访问业务接口)
  4. 密钥 fail-close:JWT_SECRET 为空或不足 32 字符时 AppServiceProvider 抛异常拒绝启动——弱密钥无法上线
  5. 验证码限流:按 IP 限流(CAPTCHA_RATE_WINDOW / CAPTCHA_RATE_LIMIT),万能码仅 APP_DEBUG=true 生效

二、授权机制

RBAC 权限模型(菜单即权限)

┌─────────┐    ┌────────────┐    ┌─────────┐    ┌────────────┐    ┌─────────┐
│  User   │───►│ UserRole   │───►│  Role   │───►│ RoleMenu   │───►│  Menu   │
│         │    │            │    │         │    │            │    │         │
│ id      │    │ user_id    │    │ id      │    │ role_id    │    │ id      │
│ username│    │ role_id    │    │ name    │    │ menu_id    │    │permission│
└─────────┘    └────────────┘    │ code    │   └────────────┘    │ type    │
                                 └─────────┘                     └─────────┘
  • 菜单表的 permission 字段即权限编码(sys:{模块}:{操作})
  • type=1(菜单)且 permission 非空的节点参与接口鉴权
  • 角色-菜单变更时 RoleMenuObserver / MenuObserver 自动清除权限缓存

权限校验流程(Authenticate::checkPermission)

// app/Http/Middleware/Authenticate.php(节选)

protected function checkPermission(int $uid, string $permissionCode): bool
{
    // 超级管理员跳过
    if ($uid === 1) {
        return true;
    }

    // 从 Redis 获取权限(一级缓存)
    try {
        $cached = Redis::get("perms:{$uid}");
        if ($cached) {
            $perms = json_decode($cached, true) ?? [];
            // 拥有通配权限或包含目标编码即通过
            return in_array('*:*:*', $perms) || in_array($permissionCode, $perms);
        }
    } catch (\Exception $e) {
        // Redis 不可用时继续降级
    }

    // 二级缓存:Laravel Cache(5 分钟)
    $cacheKey = "perms_fallback:{$uid}";
    $permissions = Cache::remember($cacheKey, 300, function () use ($uid) {
        return $this->loadPermissionsFromDb($uid);
    });

    return in_array('*:*:*', $permissions) || in_array($permissionCode, $permissions);
}

// 数据库加载:user_role → role_menu join menu(未删除、type=1、permission 非空)
protected function loadPermissionsFromDb(int $uid): array
{
    $roleIds = UserRole::where('user_id', $uid)->pluck('role_id')->toArray();
    if (empty($roleIds)) {
        return [];
    }

    return RoleMenu::join('menu', 'role_menu.menu_id', '=', 'menu.id')
        ->whereIn('role_menu.role_id', $roleIds)
        ->where('menu.is_delete', 0)
        ->where('menu.type', 1)
        ->where('menu.permission', '!=', '')
        ->pluck('menu.permission')
        ->toArray();
}

数据权限

HasDataScope 支持三种数据范围(当前 laravel_role 表不含 data_scope 字段,实际一律按"全部数据"执行):

data_scope 范围 实现(HasDataScope)
1 全部数据 不过滤
2 本部门数据 WHERE dept_id = 用户部门ID
3 仅本人数据(默认) WHERE create_user = 用户名

三、防护机制

密码加密(bcrypt 双重加盐)

密码存储流程:
  原始密码 + 外部 salt(随机字符串,存 user.salt 字段)
      │
      ▼ password_hash($password . $salt, PASSWORD_BCRYPT, ['cost' => 12])
      │
      ▼ bcrypt 内部自动生成 salt 并嵌入哈希
      │
      ▼ 存储:password 字段(60 字符哈希)+ salt 字段(独立存储)

密码校验流程:
  用户输入密码 + 数据库 salt
      │
      ▼ password_verify($input . $salt, $hashed)
      │
      ▼ true / false
  • 成本因子固定为 12(PasswordService::BCRYPT_ROUNDS,对应 .env 的 BCRYPT_ROUNDS=12)
  • 双重加盐:外部 salt 存独立字段,数据库泄露后仍需同时获取 salt 才能暴力破解;bcrypt 内部 salt 进一步增加彩虹表攻击难度

参数校验(FormRequest 入口拦截)

通过控制器方法类型注入 FormRequest,进入业务逻辑前完成格式校验:

class UserRequest extends BaseRequest
{
    public function rules(): array
    {
        return [
            'username' => 'required|string|min:3|max:50',
            'realname' => 'required|string|max:50',
            'mobile' => 'nullable|max:20',
            'email' => 'nullable|email',
        ];
    }
}

校验失败由 failedValidation() 直接返回 code=1 + 字段错误明细,不进入控制器方法;唯一性等业务规则由 Service 层兜底(防止绕过格式校验的组合攻击)。

SQL 注入防护

Eloquent 查询构造器使用参数绑定,自动防止 SQL 注入:

// ✅ ORM 查询(安全,自动参数绑定)
User::where('username', $username)->first();
// 生成:SELECT * FROM laravel_user WHERE username = ?  [$username]

// ✅ 原生查询(手动绑定参数)
DB::select("SELECT * FROM laravel_user WHERE username = ?", [$username]);

// ❌ 危险!字符串拼接(禁止)
DB::select("SELECT * FROM laravel_user WHERE username = '$username'");

查询排序也受白名单保护:前端 orderField 必须命中 pageOrderBy 与 orderFieldAllow 的并集,否则忽略(防排序注入)。

:::warning 安全红线 永远不要将用户输入直接拼接到 SQL 字符串中。即使使用原生 SQL,也必须使用参数绑定(? 占位符)。 :::

文件上传安全

上传文件安全检查链:
    │
    ▼ 1. 文件大小限制(config/file.php → FILE_MAX_SIZE,默认 10MB)
    │
    ▼ 2. 文件扩展名白名单(FILE_IMAGE_EXT / FILE_EXT)
    │     (svg 已移除:内嵌 JS 导致 XSS 风险)
    │
    ▼ 3. MIME 类型真实检测(fileinfo 扩展,_import 等场景强制校验)
    │
    ▼ 4. 存储路径由服务端生成(防止路径遍历)
    │
    ▼ 5. Nginx 仅允许入口 index.php 执行 —— 上传目录中的 .php 一律 404
# docker/nginx.conf:只有入口可执行,其余 .php 一律拒绝
location = /index.php { /* FastCGI 转发 */ }
location ~ \.php$ {
    return 404;
}

XSS 防护

防护措施:
  1. 前端:Vue 模板插值默认转义({{ }}),避免注入
  2. 后端:响应强制 Content-Type: application/json(ForceJsonResponse 中间件),
     浏览器不会按 HTML 渲染
  3. 富文本:字段独立处理链(contentFields),内容过滤与占位符机制
  4. 上传白名单移除 svg(内嵌脚本风险)

演示模式保护

// DemoMiddleware 拦截写操作
// .env: DEMO_MODE=true(读取 config('app.demo_mode'))

// 受保护:POST/PUT/DELETE/PATCH 请求 → 返回 403
// 放行:GET 请求 + 标注 #[DemoAllow] 注解的方法
// 标注 #[DemoAllow] 的方法在演示模式下仍可执行
#[DemoAllow]
public function login(LoginRequest $request): JsonResponse { ... }

CORS 跨域控制

# .env
# 开发环境:允许所有(仅限开发)
CORS_ALLOW_ORIGIN=*

# 生产环境:限制具体域名(多个来源用逗号分隔)
CORS_ALLOW_ORIGIN=https://admin.example.com
// Cors 中间件处理逻辑(见 4.2 请求处理链路):
// 1. OPTIONS 预检请求 → 直接返回 204 + CORS 头(不进业务逻辑)
// 2. 凭证模式下不能用 * → 回显具体 Origin + Vary: Origin(防 CDN 缓存串扰)
// 3. 仅 Origin 被允许时才设置其余 CORS 头

四、审计机制

操作日志(laravel_operation_log)

LogOperation 中间件基于 #[OperationLog] 注解自动记录:

#[OperationLog('用户管理-新增', OperationLog::TYPE_ADD, '新增用户:{username}')]
public function add(UserRequest $request): JsonResponse { ... }

记录字段:

字段 说明 示例
title 操作标题(来自注解,无注解回退请求路径) 用户管理-新增
type 操作类型(30 种常量,无注解按 HTTP 方法推断) 1=新增 2=修改 3=删除 4=查询 5=导入 6=导出
method HTTP 方法 POST
request_method 控制器方法名 add
url 请求路径 /api/user/add
param 请求参数(JSON,排除 password/token,截断 2000 字) {"username":"admin"}
result 响应内容(截断 2000 字) {"code":0,...}
ip / location 客户端 IP / 归属地(ip2region) 192.168.1.100 / 江苏省南京市
os / browser 操作系统 / 浏览器 Windows 11 / Chrome 128
create_user 操作人(userInfo.username) admin
status 执行状态(code=0/200 → 0,其余 → 1) 0=成功 1=失败
error 错误信息(失败时取 msg) 名称已存在
consume_time 执行耗时(ms) 125

记录规则:

  • 写操作(POST/PUT/DELETE/PATCH)始终记录
  • GET 仅在注解类型为 EXPORT / DOWNLOAD / IMPORT 时记录
  • handle 阶段采集、terminate 阶段落库(响应后执行,不阻塞业务;写入失败静默)

登录日志(laravel_login_log)

// AuthController::writeLoginLog(节选)—— 成功与失败均记录
$data = [
    'username' => $username,
    'log_type' => 1,                    // 1=登录 2=登出
    'source' => 0,                      // 来源端:0=后台
    'method' => $request->method(),
    'request_method' => $entry,         // login / refresh 等入口
    'url' => $request->fullUrl(),
    'param' => json_encode(['username' => $username], ...),   // 只记录用户名
    'result' => json_encode(['code' => $success ? 0 : 1, 'msg' => $message], ...),
    'ip' => $requestInfo->getIp(),
    'location' => $requestInfo->getIpLocation(),
    // + os / browser / 耗时
];

记录字段:

字段 说明
username 登录用户名(无论成功失败都记录)
log_type 1=登录 2=登出
status / result 0=成功 1=失败 + 结果消息
ip / location 客户端 IP / 归属地
os / browser 操作系统 / 浏览器

日志安全要点

要点 说明
密码与令牌不记录 操作日志 param 排除 password、token;登录日志只记录用户名
参数截断 param / result 截断 2000 字,防止超长内容撑爆字段
独立存储 操作日志和登录日志分别存储在不同表
失败不阻断 日志写入异常静默处理,不影响主业务

五、安全配置清单

环境变量(.env)

配置项 开发环境 生产环境 说明
APP_DEBUG true false 关闭调试模式,异常不返回真实消息
JWT_SECRET 强随机 强随机(≥32 字符) 不足 32 字符拒绝启动(fail-close)
CORS_ALLOW_ORIGIN * 指定域名 如 https://admin.example.com
DEMO_MODE false false 演示环境按需开启
FILE_MAX_SIZE 10 合理限制 防止大文件 DoS
CAPTCHA_BYPASS_CODE 可设 必须留空 万能验证码仅 debug 生效,生产留空
TRUSTED_PROXIES 空 CDN/负载均衡地址 防止伪造代理头获取错误 IP
BCRYPT_ROUNDS 12 12+ 密码哈希成本因子
API_CAMEL_SNAKE_CONVERT true true 驼峰转换开关

Nginx 配置

# 1. 启用 HTTPS(生产必须)
server {
    listen 443 ssl;
    ssl_certificate     /etc/ssl/cert.pem;
    ssl_certificate_key /etc/ssl/key.pem;
}

# 2. 仅入口 PHP 可执行(docker/nginx.conf 已内置)
location = /index.php { /* FastCGI 转发 */ }
location ~ \.php$ { return 404; }

# 3. 禁止访问隐藏文件(.env / .git 等,docker/nginx.conf 已内置)
location ~ /\. { deny all; }

# 4. 安全响应头(按需追加)
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header Referrer-Policy strict-origin-when-cross-origin;

安全检查清单

上线前逐项检查:

□ .env 中 APP_DEBUG = false
□ .env 中 JWT_SECRET 已更换为 ≥32 字符强随机密钥(否则应用拒绝启动)
□ .env 中 CORS_ALLOW_ORIGIN 指定具体域名
□ .env 中 CAPTCHA_BYPASS_CODE 留空
□ Nginx 已配置 HTTPS
□ Nginx 确认仅入口 index.php 可执行、隐藏文件已拒绝
□ 数据库密码为强密码,Redis 已设置 requirepass 并同步 REDIS_PASSWORD
□ 默认管理员密码已修改(admin / 123456)
□ 演示模式已关闭(除非需要)
□ 后端 /api/* 路由确认在认证中间件组内(公开接口仅 login/refresh/captcha/oauth2)
□ 日志中不包含密码等敏感信息

总结

系统通过认证、授权、防护、审计四层安全架构,实现了从网络层到数据层的纵深防御。JWT 双令牌 + Redis 有状态保证认证安全且支持会话管控,RBAC 权限控制细化到按钮级(三级缓存保证性能),bcrypt 双重加盐保护密码安全,操作日志与登录日志实现全链路审计。各层安全机制相互独立又协同配合,确保系统在企业级应用场景下的安全可靠性。