# 4.10 安全架构设计 :::tip 概述 本章详细阐述系统的安全架构设计,围绕认证、授权、防护、审计四大安全领域展开,旨在帮助开发者全面理解系统的安全保障体系。 ::: **认证(Authentication)**:系统采用基于 JWT 的无状态认证机制,用户通过用户名密码登录后获取 Token,后续请求通过 `Authorization: Bearer ` 头传递,由 `AuthMiddleware` 统一拦截并验证 Token 的有效性(签名校验、有效期检查)。 **授权(Authorization)**:基于 RBAC 模型设计,权限控制粒度细化至接口级。用户通过角色继承权限节点(如 `sys:user:add`),由 `#[Permission]` 注解在控制器层声明,`AuthMiddleware` 统一校验。管理员(`uid=1`)自动放行。 **防护(Protection)**:系统从多维度构建安全防护体系。参数校验通过 Validate 自动拦截非法输入;敏感密码采用 bcrypt 双重加盐哈希存储;演示模式下 `DemoMiddleware` 统一拦截写操作;跨域配置精确控制可信域名。 **审计(Audit)**:系统提供完备的操作审计能力。`LogMiddleware` 自动记录所有写操作的请求路径、参数、操作人、IP 地址及执行结果;登录日志独立记录每次登录尝试,便于安全事件追溯。 :::tip 安全设计原则 - **纵深防御**:认证、授权、参数校验多层防护,单一防线失效时仍有其他机制兜底 - **最小权限**:用户仅能访问已授权接口,角色权限按需分配 - **数据不落地**:敏感信息(Token、密码)不在日志中明文记录,密码哈希不可逆。**注意**:当前 LogMiddleware 会记录完整请求参数,敏感字段(如密码)建议在业务层脱敏后再传入 - **安全默认**:框架默认配置为安全模式(DEBUG 关闭、错误信息隐藏) ::: ## 安全架构总览 ```text ┌─────────────────────────────────────────────────────────────────┐ │ 安全防御层次 │ │ │ │ ┌───────────────────────────────────────────────────────────┐ │ │ │ 网络层防护 │ │ │ │ ┌──────────┐ ┌──────────────┐ │ │ │ │ │ CORS │ │ 上传体积限制 │ │ │ │ │ │ 跨域控制 │ │ DoS 防御 │ │ │ │ │ └──────────┘ └──────────────┘ │ │ │ └───────────────────────────────────────────────────────────┘ │ │ │ │ ┌───────────────────────────────────────────────────────────┐ │ │ │ 认证层 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ │ JWT 认证 │ │ 验证码 │ │ 双令牌 │ │ │ │ │ │ Bearer │ │ 图形验证 │ │ access+ │ │ │ │ │ │ │ │ │ │ refresh │ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ └───────────────────────────────────────────────────────────┘ │ │ │ │ ┌───────────────────────────────────────────────────────────┐ │ │ │ 授权层 │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │ │ │ │ RBAC 权限 │ │ 演示模式保护 │ │ 超级管理员跳过 │ │ │ │ │ │ #[Permission]│ │ DemoMiddleware│ │ uid=1 bypass │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────────┘ │ │ │ └───────────────────────────────────────────────────────────┘ │ │ │ │ ┌───────────────────────────────────────────────────────────┐ │ │ │ 数据层 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │ │ │ 密码加密 │ │ 参数校验 │ │ SQL 注入 │ │ 操作日志 │ │ │ │ │ │ bcrypt │ │ Validate │ │ ORM 防护 │ │ 审计追踪 │ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │ │ └───────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` ## 一、认证机制 ### 登录流程 ```text 1. 前端提交 { username, password, code, key } │ ▼ 2. CaptchaService::check(code, key) ← 验证码校验 │ 失败 → 返回"验证码错误" ▼ 3. User::where('username', $username) ← 查询用户 │ 不存在 → 统一提示"用户名或密码错误" ▼ 4. password_verify($password . $salt, $hashed) ← bcrypt 校验 │ 不匹配 → 统一提示"用户名或密码错误" ▼ 5. 检查 user.status == 1 ← 账号状态 │ 非启用 → 返回"账号已被禁用" ▼ 6. JwtService::createTokenPair($uid) ← 签发双令牌 │ ▼ 7. 返回 { access_token, refresh_token, expires_in } ``` ### JWT 双令牌认证 ```html [前端] [后端] │ │ │── POST /api/login ────→│ │ {username, password} │ │ │── 验证码校验 │ │── 密码校验(bcrypt) │ │── 签发 access_token + refresh_token │←─ {access_token, ───────│ │ refresh_token} │ │ │ │── GET /api/xxx ────────→│ │ Authorization: │ │ Bearer │ │ │── 解析 JWT │ │── 校验签名 + 有效期 │ │── 注入 userInfo │←─ {data} ──────────────│ │ │ │── Token 过期 ──────────→│ │ │ │── POST /api/oauth2/ ──→│ │ token │ │ {refresh_token} │── 验证 refresh 类型 │ │── 签发新令牌对 │←─ {new_access_token, ──│ │ new_refresh_token} │ ``` ### Token 设计 | Token 类型 | 有效期 | 用途 | payload.type | |-----------|--------|------|-------------| | access_token | 2 小时 | 接口认证 | `access` | | refresh_token | 7 天 | 刷新令牌 | `refresh` | ### 安全要点 1. **密码错误统一提示**:无论用户不存在还是密码错误,统一提示"用户名或密码错误",避免暴露账号是否存在 2. **账号状态校验**:`status != 1` 时禁止登录 3. **Token 类型校验**:access_token 和 refresh_token 通过 `type` 字段区分,不可混用 4. **密钥安全**:JWT 密钥通过 `.env` 配置,生产环境务必修改为强随机字符串 ## 二、授权机制 ### RBAC 权限模型 ```typescript ┌─────────┐ ┌────────────┐ ┌─────────┐ ┌────────────┐ ┌─────────┐ │ User │───►│ UserRole │───►│ Role │───►│ RoleMenu │───►│ Menu │ │ │ │ │ │ │ │ │ │ │ │ id │ │ user_id │ │ id │ │ role_id │ │ id │ │ username│ │ role_id │ │ name │ │ menu_id │ │ perm │ └─────────┘ └────────────┘ │data_scope│ └────────────┘ │ type │ └─────────┘ └─────────┘ ``` ### 权限校验流程 ```php // AuthMiddleware::checkPermission() protected function checkPermission(int $uid, string $permissionCode): bool { // 1. 超级管理员(uid=1)跳过校验 if ($uid === 1) { return true; } // 2. 查询用户角色 $roleIds = UserRole::where('user_id', $uid)->column('role_id'); if (empty($roleIds)) { return false; } // 3. 通过角色关联获取权限节点 $permissions = RoleMenu::alias('rm') ->join('think_menu m', 'rm.menu_id = m.id') ->whereIn('rm.role_id', $roleIds) ->where('m.is_delete', 0) ->where('m.type', 1) // type=1 为权限节点 ->column('m.permission'); // 4. 判断是否包含目标权限 return in_array($permissionCode, $permissions); } ``` ### 数据权限 | data_scope | 范围 | 实现 | |-----------|------|------| | 1 | 全部数据 | 不过滤 | | 2 | 本部门数据 | WHERE dept_id = 用户部门ID | | 3 | 仅本人数据 | WHERE create_user = 用户名 | ## 三、防护机制 ### 密码加密(bcrypt 双重加盐) ```text 密码存储流程: 原始密码 + 外部 salt(10位随机字符串) │ ▼ password_hash($combined, PASSWORD_BCRYPT, ['cost' => 12]) │ ▼ bcrypt 内部自动生成 salt 并嵌入哈希 │ ▼ 存储:password 字段(60字符)+ salt 字段(10字符) 密码校验流程: 用户输入密码 + 数据库 salt │ ▼ password_verify($input . $salt, $hashed) │ ▼ true/false ``` ```php // 加密 $password = '123456'; $salt = 'xK7pMn3qRt'; // 外部盐(10位随机) $combined = $password . $salt; // 拼接 $hashed = password_hash($combined, PASSWORD_BCRYPT, ['cost' => 12]); // 结果:$2y$12$...(60字符) // 校验 $inputCombined = $userInput . $dbSalt; $isValid = password_verify($inputCombined, $dbHashed); ``` :::tip 为什么双重加盐? 外部 salt 存储在独立字段,即使数据库泄露,攻击者也需要同时获取 salt 才能进行暴力破解。bcrypt 内部 salt 进一步增加彩虹表攻击难度。 ::: ### 参数校验(自动拦截) 通过 Logic 层 `$validateClass` 配置,add/update 时自动校验参数格式: ```php class UserValidate extends Validate { protected $rule = [ 'username' => 'require|unique:user', 'realname' => 'require', 'mobile' => 'mobile', 'email' => 'email', ]; protected $scene = [ 'add' => ['username', 'realname', 'mobile', 'email'], 'update' => ['realname', 'mobile', 'email'], ]; } ``` 校验失败直接返回 422,不进入业务逻辑层。 ### SQL 注入防护 ThinkPHP ORM 使用参数绑定,自动防止 SQL 注入: ```php // ✅ ORM 查询(安全,自动参数绑定) User::where('username', $username)->find(); // 生成:SELECT * FROM think_user WHERE username = ? [$username] // ✅ 原生查询(手动绑定参数) Db::query("SELECT * FROM think_user WHERE username = ?", [$username]); // ❌ 危险!字符串拼接(禁止) Db::query("SELECT * FROM think_user WHERE username = '$username'"); ``` :::warning 安全红线 永远不要将用户输入直接拼接到 SQL 字符串中。即使使用原生 SQL,也必须使用参数绑定(`?` 占位符)。 ::: ### 文件上传安全 ```text 上传文件安全检查链: │ ▼ 1. 文件大小限制(config/file.php → max_size,默认 10MB) │ ▼ 2. 文件扩展名白名单(config/file.php → allow_ext) │ ▼ 3. MIME 类型检查(fileinfo 扩展) │ ▼ 4. 生成随机文件名(防止路径遍历攻击) │ ▼ 5. 存储到 Web 根目录之外(或 Nginx 禁止执行 PHP) ``` ```ini # Nginx 配置:上传目录禁止执行 PHP location ~* /uploads/.*\.php$ { deny all; } ``` ### XSS 防护 ```text 防护措施: 1. 前端:用户输入转义显示(Vue 默认转义 {{ }} 插值) 2. 后端:富文本内容通过 save_content() 处理,过滤危险标签 3. 响应头:Content-Type: application/json(非 text/html) ``` ### 演示模式保护 ```php // DemoMiddleware 拦截写操作 // .env: [APP] DEMO = true // 受保护:POST/PUT/DELETE 请求 → 返回 403 // 放行:GET 请求 + 标注 #[DemoAllow] 的方法 ``` ```php // 标注 #[DemoAllow] 的方法在演示模式下仍可执行 #[DemoAllow] public function login(): Json { ... } ``` ### CORS 跨域控制 ```ini ; .env ; 生产环境:限制具体域名 [CORS] ALLOW_ORIGIN = https://admin.example.com ; 开发环境:允许所有(仅限开发) [CORS] ALLOW_ORIGIN = * ``` ```php // CorsMiddleware 处理逻辑: // 1. OPTIONS 预检请求 → 直接返回 204 + CORS 头 // 2. 普通请求 → 校验 Origin 是否在白名单 // 3. 设置 Access-Control-Allow-Origin / Allow-Methods / Allow-Headers ``` ## 四、审计机制 ### 操作日志(think_operation_log) `LogMiddleware` 基于 `#[Log]` 注解自动记录所有写操作: ```php #[Log('用户管理-新增记录', Log::TYPE_ADD, '新增用户:{username}')] public function add(): Json { ... } ``` **记录字段:** | 字段 | 说明 | 示例 | |------|------|------| | `title` | 操作标题(来自注解) | 用户管理-新增记录 | | `type` | 操作类型(常量) | 1=新增 2=修改 3=删除 4=查询 5=导入 6=导出 | | `method` | HTTP 方法 | POST | | `url` | 请求路径 | /api/user/add | | `params` | 请求参数(JSON) | {"username":"admin"} | | `ip` | 客户端 IP | 192.168.1.100 | | `location` | IP 归属地 | 江苏省南京市 | | `os` | 操作系统 | Windows 10 | | `browser` | 浏览器 | Chrome 120 | | `username` | 操作人 | admin | | `status` | 执行状态 | 0=成功 1=失败 | | `error_msg` | 错误信息(失败时) | 用户名已存在 | | `duration` | 执行耗时(ms) | 125 | | `create_time` | 操作时间 | 2026-09-22 10:30:00 | **记录规则:** - 写操作(POST/PUT/DELETE)始终记录 - GET 查询操作仅在 `#[Log]` 指定 `TYPE_QUERY` 时记录 - 排除列表:`config/middleware.php` → `log_except` 中的路径不记录 ### 登录日志(think_login_log) ```php // LoginLogic 中独立记录 $requestInfo = RequestInfoService::create(request()); LoginLog::create([ 'username' => $username, 'type' => 1, // 1=登录 2=登出 'status' => $success ? 0 : 1, // 0=成功 1=失败 'message' => $msg, 'ip' => $requestInfo->getIp(), 'location' => $requestInfo->getIpLocation(), 'os' => $requestInfo->getOs(), 'browser' => $requestInfo->getBrowser(), ]); ``` **记录字段:** | 字段 | 说明 | |------|------| | `username` | 登录用户名(无论成功失败都记录) | | `type` | 1=登录 2=登出 | | `status` | 0=成功 1=失败 | | `message` | 结果消息(如"登录成功"、"密码错误") | | `ip` | 客户端 IP | | `location` | IP 归属地 | | `os` | 操作系统 | | `browser` | 浏览器 | ### 日志安全要点 | 要点 | 说明 | |------|------| | 密码不记录 | 登录日志不记录密码字段 | | 参数可脱敏 | 敏感字段建议在 Controller 层处理后再传入 Logic | | 独立存储 | 操作日志和登录日志分别存储在不同表 | | 不可篡改 | 日志仅 INSERT,无 UPDATE/DELETE 接口 | ## 五、安全配置清单 ### 环境变量(.env) | 配置项 | 开发环境 | 生产环境 | 说明 | |--------|---------|---------|------| | `APP_DEBUG` | `true` | **`false`** | 关闭调试模式,隐藏详细错误 | | `SHOW_ERROR_MSG` | `true` | **`false`** | 隐藏原始错误信息 | | `JWT.SECRET` | 默认值 | **强随机密钥** | 32 字节以上随机字符串 | | `CORS.ALLOW_ORIGIN` | `*` | **指定域名** | 如 `https://admin.example.com` | | `APP.DEMO` | `false` | `false` | 演示环境按需开启 | | `FILE.MAX_SIZE` | 10MB | 合理限制 | 防止大文件 DoS | | `API.CAMEL_SNAKE_CONVERT` | `true` | `true` | 驼峰转换开关 | ### Nginx 配置 ```nginx # 1. 启用 HTTPS server { listen 443 ssl; ssl_certificate /etc/ssl/cert.pem; ssl_certificate_key /etc/ssl/key.pem; } # 2. 上传目录禁止执行 PHP location ~* /uploads/.*\.php$ { deny all; } # 3. 禁止访问隐藏文件 location ~ /\. { deny all; } # 4. 安全响应头 add_header X-Content-Type-Options nosniff; add_header X-Frame-Options DENY; add_header X-XSS-Protection "1; mode=block"; add_header Referrer-Policy strict-origin-when-cross-origin; ``` ### 安全检查清单 ```text 上线前逐项检查: □ .env 中 APP_DEBUG = false □ .env 中 SHOW_ERROR_MSG = false □ .env 中 JWT.SECRET 已更换为强随机密钥 □ .env 中 CORS.ALLOW_ORIGIN 指定具体域名 □ Nginx 已配置 HTTPS □ Nginx 上传目录已禁止执行 PHP □ 数据库密码为强密码 □ Redis 已设置密码(如使用) □ 默认管理员密码已修改 □ 演示模式已关闭(除非需要) □ 日志中不包含密码等敏感信息 ``` ## 总结 系统通过认证、授权、防护、审计四层安全架构,实现了从网络层到数据层的纵深防御。JWT 双令牌认证保证无状态安全,RBAC 权限控制细化到按钮级,bcrypt 双重加盐保护密码安全,操作日志实现全链路审计。各层安全机制相互独立又协同配合,确保系统在企业级应用场景下的安全可靠性。