235 lines
84 KiB
JavaScript
235 lines
84 KiB
JavaScript
import{_ as i,o as a,c as n,a2 as t}from"./chunks/framework.B-QBKwc1.js";const o=JSON.parse('{"title":"安全架构设计","description":"本章详细阐述系统的安全架构设计,围绕认证、授权、防护、审计四大安全领域展开,旨在帮助开发者全面理解系统的安全保障体系。","frontmatter":{},"headers":[],"relativePath":"zh/architecture/security.md","filePath":"zh/architecture/security.md"}'),p={name:"zh/architecture/security.md"};function l(h,s,e,k,d,r){return a(),n("div",null,[...s[0]||(s[0]=[t(`<h1 id="_4-10-安全架构设计" tabindex="-1">4.10 安全架构设计 <a class="header-anchor" href="#_4-10-安全架构设计" aria-label="Permalink to "4.10 安全架构设计""></a></h1><div class="tip custom-block"><p class="custom-block-title">概述</p><p>本章详细阐述系统的安全架构设计,围绕认证、授权、防护、审计四大安全领域展开,旨在帮助开发者全面理解系统的安全保障体系。</p></div><p><strong>认证(Authentication)</strong>:系统采用基于 JWT 的双令牌认证机制,用户通过用户名密码登录后获取 Token,后续请求通过 <code>Authorization: Bearer <token></code> 头传递,由 <code>Authenticate</code> 中间件(别名 <code>auth.jwt</code>)统一拦截并验证 Token 的有效性(签名校验、有效期检查),签名密钥启动时 fail-close 校验。</p><p><strong>授权(Authorization)</strong>:基于 RBAC 模型设计,权限控制粒度细化至接口级。用户通过角色继承权限节点(如 <code>sys:user:add</code>),由 <code>#[Permission]</code> 注解在控制器层声明,<code>Authenticate</code> 中间件反射校验(三级缓存:Redis → Laravel Cache → 数据库)。超级管理员(<code>uid=1</code>)自动放行。</p><p><strong>防护(Protection)</strong>:系统从多维度构建安全防护体系。参数校验通过 FormRequest 在 HTTP 入口自动拦截非法输入;敏感密码采用 bcrypt 双重加盐哈希存储;演示模式下 <code>DemoMiddleware</code> 统一拦截写操作;跨域配置精确控制可信域名;Nginx 仅允许入口 PHP 执行。</p><p><strong>审计(Audit)</strong>:系统提供完备的操作审计能力。<code>LogOperation</code> 中间件自动记录所有写操作的请求路径、参数(排除敏感字段)、操作人、IP 归属地及执行结果;登录日志独立记录每次登录尝试,便于安全事件追溯。</p><div class="tip custom-block"><p class="custom-block-title">安全设计原则</p><ul><li><strong>纵深防御</strong>:认证、授权、参数校验多层防护,单一防线失效时仍有其他机制兜底</li><li><strong>最小权限</strong>:用户仅能访问已授权接口,角色权限按需分配</li><li><strong>数据不落地</strong>:敏感信息(Token、密码)不在日志中明文记录(操作日志 <code>param</code> 排除 <code>password</code>/<code>token</code>),密码哈希不可逆</li><li><strong>安全默认</strong>:<code>JWT_SECRET</code> 弱密钥拒绝启动(fail-close),生产 <code>APP_DEBUG=false</code> 隐藏错误详情</li></ul></div><h2 id="安全架构总览" tabindex="-1">安全架构总览 <a class="header-anchor" href="#安全架构总览" aria-label="Permalink to "安全架构总览""></a></h2><div class="language-text vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">text</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span>┌─────────────────────────────────────────────────────────────────┐</span></span>
|
||
<span class="line"><span>│ 安全防御层次 │</span></span>
|
||
<span class="line"><span>│ │</span></span>
|
||
<span class="line"><span>│ ┌───────────────────────────────────────────────────────────┐ │</span></span>
|
||
<span class="line"><span>│ │ 网络层防护 │ │</span></span>
|
||
<span class="line"><span>│ │ ┌──────────┐ ┌──────────────┐ ┌────────────────────────┐ │ │</span></span>
|
||
<span class="line"><span>│ │ │ CORS │ │ 上传体积限制 │ │ Nginx 仅入口 index.php │ │ │</span></span>
|
||
<span class="line"><span>│ │ │ 跨域控制 │ │ DoS 防御 │ │ 可见性攻击防护 │ │ │</span></span>
|
||
<span class="line"><span>│ │ └──────────┘ └──────────────┘ └────────────────────────┘ │ │</span></span>
|
||
<span class="line"><span>│ └───────────────────────────────────────────────────────────┘ │</span></span>
|
||
<span class="line"><span>│ │</span></span>
|
||
<span class="line"><span>│ ┌───────────────────────────────────────────────────────────┐ │</span></span>
|
||
<span class="line"><span>│ │ 认证层 │ │</span></span>
|
||
<span class="line"><span>│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │</span></span>
|
||
<span class="line"><span>│ │ │ JWT 认证 │ │ 验证码 │ │ 双令牌 │ │ 密钥 fail- │ │ │</span></span>
|
||
<span class="line"><span>│ │ │ Bearer │ │ 图形验证 │ │ access+ │ │ close 启动 │ │ │</span></span>
|
||
<span class="line"><span>│ │ │ │ │ + IP限流 │ │ refresh │ │ 校验 │ │ │</span></span>
|
||
<span class="line"><span>│ │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │</span></span>
|
||
<span class="line"><span>│ └───────────────────────────────────────────────────────────┘ │</span></span>
|
||
<span class="line"><span>│ │</span></span>
|
||
<span class="line"><span>│ ┌───────────────────────────────────────────────────────────┐ │</span></span>
|
||
<span class="line"><span>│ │ 授权层 │ │</span></span>
|
||
<span class="line"><span>│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │</span></span>
|
||
<span class="line"><span>│ │ │ RBAC 权限 │ │ 演示模式保护 │ │ 超级管理员跳过 │ │ │</span></span>
|
||
<span class="line"><span>│ │ │ #[Permission]│ │ DemoMiddleware│ │ uid=1 bypass │ │ │</span></span>
|
||
<span class="line"><span>│ │ └──────────────┘ └──────────────┘ └──────────────────┘ │ │</span></span>
|
||
<span class="line"><span>│ └───────────────────────────────────────────────────────────┘ │</span></span>
|
||
<span class="line"><span>│ │</span></span>
|
||
<span class="line"><span>│ ┌───────────────────────────────────────────────────────────┐ │</span></span>
|
||
<span class="line"><span>│ │ 数据层 │ │</span></span>
|
||
<span class="line"><span>│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │</span></span>
|
||
<span class="line"><span>│ │ │ 密码加密 │ │ 参数校验 │ │ SQL 注入 │ │ 操作日志 │ │ │</span></span>
|
||
<span class="line"><span>│ │ │ 双重加盐 │ │FormRequest│ │ ORM 防护 │ │ 审计追踪 │ │ │</span></span>
|
||
<span class="line"><span>│ │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │</span></span>
|
||
<span class="line"><span>│ └───────────────────────────────────────────────────────────┘ │</span></span>
|
||
<span class="line"><span>└─────────────────────────────────────────────────────────────────┘</span></span></code></pre></div><h2 id="一、认证机制" tabindex="-1">一、认证机制 <a class="header-anchor" href="#一、认证机制" aria-label="Permalink to "一、认证机制""></a></h2><h3 id="登录流程" tabindex="-1">登录流程 <a class="header-anchor" href="#登录流程" aria-label="Permalink to "登录流程""></a></h3><div class="language-text vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">text</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span>1. 前端提交 { username, password, key, code }</span></span>
|
||
<span class="line"><span> (验证码参数 key/code 与 /api/captcha 响应字段一致)</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼</span></span>
|
||
<span class="line"><span>2. CaptchaService::verify(key, code) ← 验证码校验(配置 CAPTCHA_LENGTH>0 时启用)</span></span>
|
||
<span class="line"><span> │ 失败 → 返回"验证码错误"</span></span>
|
||
<span class="line"><span> ▼</span></span>
|
||
<span class="line"><span>3. JwtService::login(username, password) ← 查询用户 + 密码校验</span></span>
|
||
<span class="line"><span> │ 用户不存在/密码错误 → 统一提示(不暴露账号是否存在)</span></span>
|
||
<span class="line"><span> ▼</span></span>
|
||
<span class="line"><span>4. password_verify(password + salt, hash) ← bcrypt 双重加盐校验</span></span>
|
||
<span class="line"><span> │ 不匹配 → 统一提示</span></span>
|
||
<span class="line"><span> ▼</span></span>
|
||
<span class="line"><span>5. 检查 user.status == 1 ← 账号状态</span></span>
|
||
<span class="line"><span> │ 非启用 → 返回"账号已被禁用"</span></span>
|
||
<span class="line"><span> ▼</span></span>
|
||
<span class="line"><span>6. 签发双令牌 + 写登录日志(成功/失败均记录)</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼</span></span>
|
||
<span class="line"><span>7. 返回 { access_token, refresh_token, token_type, expires_in, user }</span></span></code></pre></div><h3 id="JWT-双令牌认证" tabindex="-1">JWT 双令牌认证 <a class="header-anchor" href="#JWT-双令牌认证" aria-label="Permalink to "JWT 双令牌认证""></a></h3><div class="language-text vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">text</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span>[前端] [后端]</span></span>
|
||
<span class="line"><span> │ │</span></span>
|
||
<span class="line"><span> │── POST /api/login ──────────→│ (公开接口,中间件组外)</span></span>
|
||
<span class="line"><span> │ {username, password, │── 验证码校验(key/code)</span></span>
|
||
<span class="line"><span> │ key, code} │── 密码校验(双重加盐 bcrypt)</span></span>
|
||
<span class="line"><span> │ │── 签发 access_token + refresh_token</span></span>
|
||
<span class="line"><span> │←─ {access_token, ─────────────│</span></span>
|
||
<span class="line"><span> │ refresh_token} │</span></span>
|
||
<span class="line"><span> │ │</span></span>
|
||
<span class="line"><span> │── GET /api/xxx ─────────────→│ (认证接口,auth.jwt 组内)</span></span>
|
||
<span class="line"><span> │ Authorization: │── JwtService::parseToken</span></span>
|
||
<span class="line"><span> │ Bearer <access_token> │── 校验签名 + 有效期 + type=access</span></span>
|
||
<span class="line"><span> │ │── 注入 attributes['userInfo']</span></span>
|
||
<span class="line"><span> │←─ {data} ────────────────────│</span></span>
|
||
<span class="line"><span> │ │</span></span>
|
||
<span class="line"><span> │── access_token 过期 ─────────→│</span></span>
|
||
<span class="line"><span> │── POST /api/refresh ─────────→│ (公开接口)</span></span>
|
||
<span class="line"><span> │ {refresh_token} │── 校验 refresh 类型 + Redis jti(一次性消费)</span></span>
|
||
<span class="line"><span> │←─ {新令牌对} ─────────────────│</span></span></code></pre></div><h3 id="Token-设计" tabindex="-1">Token 设计 <a class="header-anchor" href="#Token-设计" aria-label="Permalink to "Token 设计""></a></h3><table tabindex="0"><thead><tr><th>Token 类型</th><th>默认有效期</th><th>用途</th><th>存储</th></tr></thead><tbody><tr><td>access_token</td><td>2 小时(<code>JWT_ACCESS_TTL=7200</code>)</td><td>接口认证</td><td>无状态(JWT)</td></tr><tr><td>refresh_token</td><td>7 天(<code>JWT_REFRESH_TTL=604800</code>)</td><td>刷新令牌</td><td><strong>Redis 有状态</strong>(支持踢下线、改密全端失效、一次性消费)</td></tr></tbody></table><h3 id="安全要点" tabindex="-1">安全要点 <a class="header-anchor" href="#安全要点" aria-label="Permalink to "安全要点""></a></h3><ol><li><strong>密码错误统一提示</strong>:无论用户不存在还是密码错误,统一提示,避免暴露账号是否存在</li><li><strong>账号状态校验</strong>:<code>status != 1</code> 时禁止登录</li><li><strong>Token 类型校验</strong>:access 与 refresh 通过 <code>type</code> 字段区分,不可混用(refresh 令牌不能访问业务接口)</li><li><strong>密钥 fail-close</strong>:<code>JWT_SECRET</code> 为空或不足 32 字符时 <code>AppServiceProvider</code> 抛异常<strong>拒绝启动</strong>——弱密钥无法上线</li><li><strong>验证码限流</strong>:按 IP 限流(<code>CAPTCHA_RATE_WINDOW</code> / <code>CAPTCHA_RATE_LIMIT</code>),万能码仅 <code>APP_DEBUG=true</code> 生效</li></ol><h2 id="二、授权机制" tabindex="-1">二、授权机制 <a class="header-anchor" href="#二、授权机制" aria-label="Permalink to "二、授权机制""></a></h2><h3 id="RBAC-权限模型-菜单即权限-" tabindex="-1">RBAC 权限模型(菜单即权限) <a class="header-anchor" href="#RBAC-权限模型-菜单即权限-" aria-label="Permalink to "RBAC 权限模型(菜单即权限)""></a></h3><div class="language-text vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">text</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span>┌─────────┐ ┌────────────┐ ┌─────────┐ ┌────────────┐ ┌─────────┐</span></span>
|
||
<span class="line"><span>│ User │───►│ UserRole │───►│ Role │───►│ RoleMenu │───►│ Menu │</span></span>
|
||
<span class="line"><span>│ │ │ │ │ │ │ │ │ │</span></span>
|
||
<span class="line"><span>│ id │ │ user_id │ │ id │ │ role_id │ │ id │</span></span>
|
||
<span class="line"><span>│ username│ │ role_id │ │ name │ │ menu_id │ │permission│</span></span>
|
||
<span class="line"><span>└─────────┘ └────────────┘ │ code │ └────────────┘ │ type │</span></span>
|
||
<span class="line"><span> └─────────┘ └─────────┘</span></span></code></pre></div><ul><li>菜单表的 <code>permission</code> 字段即权限编码(<code>sys:{模块}:{操作}</code>)</li><li><code>type=1</code>(菜单)且 <code>permission</code> 非空的节点参与接口鉴权</li><li>角色-菜单变更时 <code>RoleMenuObserver</code> / <code>MenuObserver</code> 自动清除权限缓存</li></ul><h3 id="权限校验流程-Authenticate-checkPermission-" tabindex="-1">权限校验流程(Authenticate::checkPermission) <a class="header-anchor" href="#权限校验流程-Authenticate-checkPermission-" aria-label="Permalink to "权限校验流程(Authenticate::checkPermission)""></a></h3><div class="language-php vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">php</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// app/Http/Middleware/Authenticate.php(节选)</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">protected</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> function</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;"> checkPermission</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">int</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">uid</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> string</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">permissionCode</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">:</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> bool</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">{</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> // 超级管理员跳过</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> if</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> ($</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">uid</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> ===</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#2F798A;"> 1</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> {</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> return</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#1E754F;"> true</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">;</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> }</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> // 从 Redis 获取权限(一级缓存)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> try</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> {</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">cached</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#998418;--shiki-light-font-style:inherit;"> Redis</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">::</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">get</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">perms:</span><span style="--shiki-dark:#E7EE98;--shiki-light:#999999;">{</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">$</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B56959;">uid</span><span style="--shiki-dark:#E7EE98;--shiki-light:#999999;">}</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">);</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> if</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> ($</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">cached</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> {</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">perms</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;"> json_decode</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">($</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">cached</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#1E754F;"> true</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> ??</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> [];</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> // 拥有通配权限或包含目标编码即通过</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> return</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;"> in_array</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">*:*:*</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">perms</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> ||</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;"> in_array</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">($</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">permissionCode</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">perms</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">);</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> }</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> }</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> catch</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> (</span><span style="--shiki-dark:#F286C4;--shiki-dark-font-style:italic;--shiki-light:#999999;--shiki-light-font-style:inherit;">\\</span><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#998418;--shiki-light-font-style:inherit;">Exception</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">e</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> {</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> // Redis 不可用时继续降级</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> }</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> // 二级缓存:Laravel Cache(5 分钟)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">cacheKey</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> "</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">perms_fallback:</span><span style="--shiki-dark:#E7EE98;--shiki-light:#999999;">{</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">$</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B56959;">uid</span><span style="--shiki-dark:#E7EE98;--shiki-light:#999999;">}</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">;</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">permissions</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#998418;--shiki-light-font-style:inherit;"> Cache</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">::</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">remember</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">($</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">cacheKey</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#2F798A;"> 300</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> function</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> ()</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> use</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> ($</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">uid</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> {</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> return</span><span style="--shiki-dark:#BF9EEE;--shiki-dark-font-style:italic;--shiki-light:#999999;--shiki-light-font-style:inherit;"> $</span><span style="--shiki-dark:#BF9EEE;--shiki-dark-font-style:italic;--shiki-light:#A65E2B;--shiki-light-font-style:inherit;">this</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">-></span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">loadPermissionsFromDb</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">($</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">uid</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">);</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> });</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> return</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;"> in_array</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">*:*:*</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">permissions</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> ||</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;"> in_array</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">($</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">permissionCode</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">permissions</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">);</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">}</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// 数据库加载:user_role → role_menu join menu(未删除、type=1、permission 非空)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">protected</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> function</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;"> loadPermissionsFromDb</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">int</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">uid</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">:</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> array</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">{</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">roleIds</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#998418;--shiki-light-font-style:inherit;"> UserRole</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">::</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">where</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">user_id</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">uid</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">-></span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">pluck</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">role_id</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">-></span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">toArray</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">();</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> if</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> (</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">empty</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">($</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">roleIds</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">))</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> {</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> return</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> [];</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> }</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> return</span><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#998418;--shiki-light-font-style:inherit;"> RoleMenu</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">::</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">join</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">menu</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">role_menu.menu_id</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">=</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">menu.id</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> -></span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">whereIn</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">role_menu.role_id</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">roleIds</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> -></span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">where</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">menu.is_delete</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#2F798A;"> 0</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> -></span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">where</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">menu.type</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#2F798A;"> 1</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> -></span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">where</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">menu.permission</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">!=</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> ''</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> -></span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">pluck</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">menu.permission</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> -></span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">toArray</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">();</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">}</span></span></code></pre></div><h3 id="数据权限" tabindex="-1">数据权限 <a class="header-anchor" href="#数据权限" aria-label="Permalink to "数据权限""></a></h3><p><code>HasDataScope</code> 支持三种数据范围(当前 <code>laravel_role</code> 表不含 <code>data_scope</code> 字段,实际一律按"全部数据"执行):</p><table tabindex="0"><thead><tr><th>data_scope</th><th>范围</th><th>实现(HasDataScope)</th></tr></thead><tbody><tr><td>1</td><td>全部数据</td><td>不过滤</td></tr><tr><td>2</td><td>本部门数据</td><td><code>WHERE dept_id = 用户部门ID</code></td></tr><tr><td>3</td><td>仅本人数据(默认)</td><td><code>WHERE create_user = 用户名</code></td></tr></tbody></table><h2 id="三、防护机制" tabindex="-1">三、防护机制 <a class="header-anchor" href="#三、防护机制" aria-label="Permalink to "三、防护机制""></a></h2><h3 id="密码加密-bcrypt-双重加盐-" tabindex="-1">密码加密(bcrypt 双重加盐) <a class="header-anchor" href="#密码加密-bcrypt-双重加盐-" aria-label="Permalink to "密码加密(bcrypt 双重加盐)""></a></h3><div class="language-text vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">text</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span>密码存储流程:</span></span>
|
||
<span class="line"><span> 原始密码 + 外部 salt(随机字符串,存 user.salt 字段)</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼ password_hash($password . $salt, PASSWORD_BCRYPT, ['cost' => 12])</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼ bcrypt 内部自动生成 salt 并嵌入哈希</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼ 存储:password 字段(60 字符哈希)+ salt 字段(独立存储)</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span>密码校验流程:</span></span>
|
||
<span class="line"><span> 用户输入密码 + 数据库 salt</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼ password_verify($input . $salt, $hashed)</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼ true / false</span></span></code></pre></div><ul><li>成本因子固定为 12(<code>PasswordService::BCRYPT_ROUNDS</code>,对应 <code>.env</code> 的 <code>BCRYPT_ROUNDS=12</code>)</li><li><strong>双重加盐</strong>:外部 salt 存独立字段,数据库泄露后仍需同时获取 salt 才能暴力破解;bcrypt 内部 salt 进一步增加彩虹表攻击难度</li></ul><h3 id="参数校验-FormRequest-入口拦截-" tabindex="-1">参数校验(FormRequest 入口拦截) <a class="header-anchor" href="#参数校验-FormRequest-入口拦截-" aria-label="Permalink to "参数校验(FormRequest 入口拦截)""></a></h3><p>通过控制器方法类型注入 FormRequest,进入业务逻辑前完成格式校验:</p><div class="language-php vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">php</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">class</span><span style="--shiki-dark:#97E1F1;--shiki-light:#2E8F82;"> UserRequest</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> extends</span><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#59873A;--shiki-light-font-style:inherit;"> BaseRequest</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">{</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> public</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> function</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;"> rules</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">()</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">:</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> array</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> {</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> return</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> [</span></span>
|
||
<span class="line"><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">username</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =></span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">required|string|min:3|max:50</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
|
||
<span class="line"><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">realname</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =></span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">required|string|max:50</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
|
||
<span class="line"><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">mobile</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =></span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">nullable|max:20</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
|
||
<span class="line"><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">email</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =></span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">nullable|email</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> ];</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> }</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">}</span></span></code></pre></div><p>校验失败由 <code>failedValidation()</code> 直接返回 <code>code=1</code> + 字段错误明细,<strong>不进入控制器方法</strong>;唯一性等业务规则由 Service 层兜底(防止绕过格式校验的组合攻击)。</p><h3 id="SQL-注入防护" tabindex="-1">SQL 注入防护 <a class="header-anchor" href="#SQL-注入防护" aria-label="Permalink to "SQL 注入防护""></a></h3><p>Eloquent 查询构造器使用参数绑定,自动防止 SQL 注入:</p><div class="language-php vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">php</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// ✅ ORM 查询(安全,自动参数绑定)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#998418;--shiki-light-font-style:inherit;">User</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">::</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">where</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">username</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">username</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">-></span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">first</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">();</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// 生成:SELECT * FROM laravel_user WHERE username = ? [$username]</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// ✅ 原生查询(手动绑定参数)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#998418;--shiki-light-font-style:inherit;">DB</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">::</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">select</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">SELECT</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> *</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> FROM</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;"> laravel_user </span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">WHERE</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;"> username </span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">=</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;"> ?</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> [$</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">username</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">]);</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// ❌ 危险!字符串拼接(禁止)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#998418;--shiki-light-font-style:inherit;">DB</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">::</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">select</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">SELECT</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> *</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> FROM</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;"> laravel_user </span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">WHERE</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;"> username </span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">=</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;"> '</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">$</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B56959;">username</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">'</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">);</span></span></code></pre></div><p>查询排序也受白名单保护:前端 <code>orderField</code> 必须命中 <code>pageOrderBy</code> 与 <code>orderFieldAllow</code> 的并集,否则忽略(防排序注入)。</p><div class="warning custom-block"><p class="custom-block-title">安全红线</p><p>永远不要将用户输入直接拼接到 SQL 字符串中。即使使用原生 SQL,也必须使用参数绑定(<code>?</code> 占位符)。</p></div><h3 id="文件上传安全" tabindex="-1">文件上传安全 <a class="header-anchor" href="#文件上传安全" aria-label="Permalink to "文件上传安全""></a></h3><div class="language-text vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">text</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span>上传文件安全检查链:</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼ 1. 文件大小限制(config/file.php → FILE_MAX_SIZE,默认 10MB)</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼ 2. 文件扩展名白名单(FILE_IMAGE_EXT / FILE_EXT)</span></span>
|
||
<span class="line"><span> │ (svg 已移除:内嵌 JS 导致 XSS 风险)</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼ 3. MIME 类型真实检测(fileinfo 扩展,_import 等场景强制校验)</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼ 4. 存储路径由服务端生成(防止路径遍历)</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼ 5. Nginx 仅允许入口 index.php 执行 —— 上传目录中的 .php 一律 404</span></span></code></pre></div><div class="language-nginx vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">nginx</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"># docker/nginx.conf:只有入口可执行,其余 .php 一律拒绝</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">location</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =</span><span style="--shiki-dark:#E7EE98;--shiki-light:#AB5E3F;"> /index.php </span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">{ /* </span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">FastCGI</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;"> 转发 */ }</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">location</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> ~</span><span style="--shiki-dark:#E7EE98;--shiki-light:#AB5E3F;"> \\.php$ </span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">{</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> return</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#2F798A;"> 404</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">;</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">}</span></span></code></pre></div><h3 id="XSS-防护" tabindex="-1">XSS 防护 <a class="header-anchor" href="#XSS-防护" aria-label="Permalink to "XSS 防护""></a></h3><div class="language-text vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">text</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span>防护措施:</span></span>
|
||
<span class="line"><span> 1. 前端:Vue 模板插值默认转义({{ }}),避免注入</span></span>
|
||
<span class="line"><span> 2. 后端:响应强制 Content-Type: application/json(ForceJsonResponse 中间件),</span></span>
|
||
<span class="line"><span> 浏览器不会按 HTML 渲染</span></span>
|
||
<span class="line"><span> 3. 富文本:字段独立处理链(contentFields),内容过滤与占位符机制</span></span>
|
||
<span class="line"><span> 4. 上传白名单移除 svg(内嵌脚本风险)</span></span></code></pre></div><h3 id="演示模式保护" tabindex="-1">演示模式保护 <a class="header-anchor" href="#演示模式保护" aria-label="Permalink to "演示模式保护""></a></h3><div class="language-php vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">php</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// DemoMiddleware 拦截写操作</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// .env: DEMO_MODE=true(读取 config('app.demo_mode'))</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// 受保护:POST/PUT/DELETE/PATCH 请求 → 返回 403</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// 放行:GET 请求 + 标注 #[DemoAllow] 注解的方法</span></span></code></pre></div><div class="language-php vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">php</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// 标注 #[DemoAllow] 的方法在演示模式下仍可执行</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">#[</span><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#998418;--shiki-light-font-style:inherit;">DemoAllow</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">]</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">public</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> function</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;"> login</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#998418;--shiki-light-font-style:inherit;">LoginRequest</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">request</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">:</span><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#998418;--shiki-light-font-style:inherit;"> JsonResponse</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> {</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> ...</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> }</span></span></code></pre></div><h3 id="CORS-跨域控制" tabindex="-1">CORS 跨域控制 <a class="header-anchor" href="#CORS-跨域控制" aria-label="Permalink to "CORS 跨域控制""></a></h3><div class="language-ini vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ini</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"># .env</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"># 开发环境:允许所有(仅限开发)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">CORS_ALLOW_ORIGIN</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">=</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">*</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"># 生产环境:限制具体域名(多个来源用逗号分隔)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">CORS_ALLOW_ORIGIN</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">=</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">https://admin.example.com</span></span></code></pre></div><div class="language-php vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">php</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// Cors 中间件处理逻辑(见 4.2 请求处理链路):</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// 1. OPTIONS 预检请求 → 直接返回 204 + CORS 头(不进业务逻辑)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// 2. 凭证模式下不能用 * → 回显具体 Origin + Vary: Origin(防 CDN 缓存串扰)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// 3. 仅 Origin 被允许时才设置其余 CORS 头</span></span></code></pre></div><h2 id="四、审计机制" tabindex="-1">四、审计机制 <a class="header-anchor" href="#四、审计机制" aria-label="Permalink to "四、审计机制""></a></h2><h3 id="操作日志-laravel-operation-log-" tabindex="-1">操作日志(laravel_operation_log) <a class="header-anchor" href="#操作日志-laravel-operation-log-" aria-label="Permalink to "操作日志(laravel_operation_log)""></a></h3><p><code>LogOperation</code> 中间件基于 <code>#[OperationLog]</code> 注解自动记录:</p><div class="language-php vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">php</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">#[</span><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#998418;--shiki-light-font-style:inherit;">OperationLog</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">用户管理-新增</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#998418;--shiki-light-font-style:inherit;"> OperationLog</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">::</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#A65E2B;">TYPE_ADD</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">新增用户:{username}</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">]</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">public</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> function</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;"> add</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#998418;--shiki-light-font-style:inherit;">UserRequest</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">request</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">:</span><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#998418;--shiki-light-font-style:inherit;"> JsonResponse</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> {</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> ...</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> }</span></span></code></pre></div><p><strong>记录字段:</strong></p><table tabindex="0"><thead><tr><th>字段</th><th>说明</th><th>示例</th></tr></thead><tbody><tr><td><code>title</code></td><td>操作标题(来自注解,无注解回退请求路径)</td><td>用户管理-新增</td></tr><tr><td><code>type</code></td><td>操作类型(30 种常量,无注解按 HTTP 方法推断)</td><td>1=新增 2=修改 3=删除 4=查询 5=导入 6=导出</td></tr><tr><td><code>method</code></td><td>HTTP 方法</td><td>POST</td></tr><tr><td><code>request_method</code></td><td>控制器方法名</td><td>add</td></tr><tr><td><code>url</code></td><td>请求路径</td><td>/api/user/add</td></tr><tr><td><code>param</code></td><td>请求参数(JSON,<strong>排除 password/token</strong>,截断 2000 字)</td><td>{"username":"admin"}</td></tr><tr><td><code>result</code></td><td>响应内容(截断 2000 字)</td><td>{"code":0,...}</td></tr><tr><td><code>ip</code> / <code>location</code></td><td>客户端 IP / 归属地(ip2region)</td><td>192.168.1.100 / 江苏省南京市</td></tr><tr><td><code>os</code> / <code>browser</code></td><td>操作系统 / 浏览器</td><td>Windows 11 / Chrome 128</td></tr><tr><td><code>create_user</code></td><td>操作人(userInfo.username)</td><td>admin</td></tr><tr><td><code>status</code></td><td>执行状态(code=0/200 → 0,其余 → 1)</td><td>0=成功 1=失败</td></tr><tr><td><code>error</code></td><td>错误信息(失败时取 msg)</td><td>名称已存在</td></tr><tr><td><code>consume_time</code></td><td>执行耗时(ms)</td><td>125</td></tr></tbody></table><p><strong>记录规则:</strong></p><ul><li>写操作(POST/PUT/DELETE/PATCH)始终记录</li><li>GET 仅在注解类型为 <code>EXPORT / DOWNLOAD / IMPORT</code> 时记录</li><li><code>handle</code> 阶段采集、<code>terminate</code> 阶段落库(响应后执行,不阻塞业务;写入失败静默)</li></ul><h3 id="登录日志-laravel-login-log-" tabindex="-1">登录日志(laravel_login_log) <a class="header-anchor" href="#登录日志-laravel-login-log-" aria-label="Permalink to "登录日志(laravel_login_log)""></a></h3><div class="language-php vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">php</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// AuthController::writeLoginLog(节选)—— 成功与失败均记录</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">$</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">data</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> [</span></span>
|
||
<span class="line"><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">username</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =></span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">username</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
|
||
<span class="line"><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">log_type</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =></span><span style="--shiki-dark:#BF9EEE;--shiki-light:#2F798A;"> 1</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> // 1=登录 2=登出</span></span>
|
||
<span class="line"><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">source</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =></span><span style="--shiki-dark:#BF9EEE;--shiki-light:#2F798A;"> 0</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> // 来源端:0=后台</span></span>
|
||
<span class="line"><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">method</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =></span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">request</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">-></span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">method</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(),</span></span>
|
||
<span class="line"><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">request_method</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =></span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">entry</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> // login / refresh 等入口</span></span>
|
||
<span class="line"><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">url</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =></span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">request</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">-></span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">fullUrl</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(),</span></span>
|
||
<span class="line"><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">param</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =></span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;"> json_encode</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">([</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">username</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =></span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">username</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">],</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> ...</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">),</span><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> // 只记录用户名</span></span>
|
||
<span class="line"><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">result</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =></span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;"> json_encode</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">([</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">code</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =></span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">success</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> ?</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#2F798A;"> 0</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> :</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#2F798A;"> 1</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">msg</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =></span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">message</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">],</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> ...</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">),</span></span>
|
||
<span class="line"><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">ip</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =></span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">requestInfo</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">-></span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">getIp</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(),</span></span>
|
||
<span class="line"><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">location</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =></span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">requestInfo</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">-></span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">getIpLocation</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(),</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> // + os / browser / 耗时</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">];</span></span></code></pre></div><p><strong>记录字段:</strong></p><table tabindex="0"><thead><tr><th>字段</th><th>说明</th></tr></thead><tbody><tr><td><code>username</code></td><td>登录用户名(无论成功失败都记录)</td></tr><tr><td><code>log_type</code></td><td>1=登录 2=登出</td></tr><tr><td><code>status</code> / <code>result</code></td><td>0=成功 1=失败 + 结果消息</td></tr><tr><td><code>ip</code> / <code>location</code></td><td>客户端 IP / 归属地</td></tr><tr><td><code>os</code> / <code>browser</code></td><td>操作系统 / 浏览器</td></tr></tbody></table><h3 id="日志安全要点" tabindex="-1">日志安全要点 <a class="header-anchor" href="#日志安全要点" aria-label="Permalink to "日志安全要点""></a></h3><table tabindex="0"><thead><tr><th>要点</th><th>说明</th></tr></thead><tbody><tr><td>密码与令牌不记录</td><td>操作日志 <code>param</code> 排除 <code>password</code>、<code>token</code>;登录日志只记录用户名</td></tr><tr><td>参数截断</td><td><code>param</code> / <code>result</code> 截断 2000 字,防止超长内容撑爆字段</td></tr><tr><td>独立存储</td><td>操作日志和登录日志分别存储在不同表</td></tr><tr><td>失败不阻断</td><td>日志写入异常静默处理,不影响主业务</td></tr></tbody></table><h2 id="五、安全配置清单" tabindex="-1">五、安全配置清单 <a class="header-anchor" href="#五、安全配置清单" aria-label="Permalink to "五、安全配置清单""></a></h2><h3 id="环境变量-env-" tabindex="-1">环境变量(.env) <a class="header-anchor" href="#环境变量-env-" aria-label="Permalink to "环境变量(.env)""></a></h3><table tabindex="0"><thead><tr><th>配置项</th><th>开发环境</th><th>生产环境</th><th>说明</th></tr></thead><tbody><tr><td><code>APP_DEBUG</code></td><td><code>true</code></td><td><strong><code>false</code></strong></td><td>关闭调试模式,异常不返回真实消息</td></tr><tr><td><code>JWT_SECRET</code></td><td>强随机</td><td><strong>强随机(≥32 字符)</strong></td><td>不足 32 字符<strong>拒绝启动</strong>(fail-close)</td></tr><tr><td><code>CORS_ALLOW_ORIGIN</code></td><td><code>*</code></td><td><strong>指定域名</strong></td><td>如 <code>https://admin.example.com</code></td></tr><tr><td><code>DEMO_MODE</code></td><td><code>false</code></td><td><code>false</code></td><td>演示环境按需开启</td></tr><tr><td><code>FILE_MAX_SIZE</code></td><td>10</td><td>合理限制</td><td>防止大文件 DoS</td></tr><tr><td><code>CAPTCHA_BYPASS_CODE</code></td><td>可设</td><td><strong>必须留空</strong></td><td>万能验证码仅 debug 生效,生产留空</td></tr><tr><td><code>TRUSTED_PROXIES</code></td><td>空</td><td>CDN/负载均衡地址</td><td>防止伪造代理头获取错误 IP</td></tr><tr><td><code>BCRYPT_ROUNDS</code></td><td>12</td><td>12+</td><td>密码哈希成本因子</td></tr><tr><td><code>API_CAMEL_SNAKE_CONVERT</code></td><td><code>true</code></td><td><code>true</code></td><td>驼峰转换开关</td></tr></tbody></table><h3 id="Nginx-配置" tabindex="-1">Nginx 配置 <a class="header-anchor" href="#Nginx-配置" aria-label="Permalink to "Nginx 配置""></a></h3><div class="language-nginx vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">nginx</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"># 1. 启用 HTTPS(生产必须)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">server</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;"> {</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> listen </span><span style="--shiki-dark:#BF9EEE;--shiki-light:#2F798A;">443</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;"> ssl</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">;</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> ssl_certificate </span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;"> /etc/ssl/cert.pem</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">;</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> ssl_certificate_key </span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">/etc/ssl/key.pem</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">;</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">}</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"># 2. 仅入口 PHP 可执行(docker/nginx.conf 已内置)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">location</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =</span><span style="--shiki-dark:#E7EE98;--shiki-light:#AB5E3F;"> /index.php </span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">{ /* </span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">FastCGI</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;"> 转发 */ }</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">location</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> ~</span><span style="--shiki-dark:#E7EE98;--shiki-light:#AB5E3F;"> \\.php$ </span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">{ </span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">return</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#2F798A;"> 404</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">; }</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"># 3. 禁止访问隐藏文件(.env / .git 等,docker/nginx.conf 已内置)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">location</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> ~</span><span style="--shiki-dark:#E7EE98;--shiki-light:#AB5E3F;"> /\\. </span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">{</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> deny </span><span style="--shiki-dark:#BF9EEE;--shiki-light:#1E754F;">all</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;"> }</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"># 4. 安全响应头(按需追加)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">add_header </span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">X-Content-Type-Options nosniff</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">;</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">add_header </span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">X-Frame-Options DENY</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">;</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">add_header </span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">Referrer-Policy strict-origin-when-cross-origin</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">;</span></span></code></pre></div><h3 id="安全检查清单" tabindex="-1">安全检查清单 <a class="header-anchor" href="#安全检查清单" aria-label="Permalink to "安全检查清单""></a></h3><div class="language-text vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">text</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span>上线前逐项检查:</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span>□ .env 中 APP_DEBUG = false</span></span>
|
||
<span class="line"><span>□ .env 中 JWT_SECRET 已更换为 ≥32 字符强随机密钥(否则应用拒绝启动)</span></span>
|
||
<span class="line"><span>□ .env 中 CORS_ALLOW_ORIGIN 指定具体域名</span></span>
|
||
<span class="line"><span>□ .env 中 CAPTCHA_BYPASS_CODE 留空</span></span>
|
||
<span class="line"><span>□ Nginx 已配置 HTTPS</span></span>
|
||
<span class="line"><span>□ Nginx 确认仅入口 index.php 可执行、隐藏文件已拒绝</span></span>
|
||
<span class="line"><span>□ 数据库密码为强密码,Redis 已设置 requirepass 并同步 REDIS_PASSWORD</span></span>
|
||
<span class="line"><span>□ 默认管理员密码已修改(admin / 123456)</span></span>
|
||
<span class="line"><span>□ 演示模式已关闭(除非需要)</span></span>
|
||
<span class="line"><span>□ 后端 /api/* 路由确认在认证中间件组内(公开接口仅 login/refresh/captcha/oauth2)</span></span>
|
||
<span class="line"><span>□ 日志中不包含密码等敏感信息</span></span></code></pre></div><h2 id="总结" tabindex="-1">总结 <a class="header-anchor" href="#总结" aria-label="Permalink to "总结""></a></h2><p>系统通过认证、授权、防护、审计四层安全架构,实现了从网络层到数据层的纵深防御。JWT 双令牌 + Redis 有状态保证认证安全且支持会话管控,RBAC 权限控制细化到按钮级(三级缓存保证性能),bcrypt 双重加盐保护密码安全,操作日志与登录日志实现全链路审计。各层安全机制相互独立又协同配合,确保系统在企业级应用场景下的安全可靠性。</p>`,74)])])}const c=i(p,[["render",l]]);export{o as __pageData,c as default};
|