Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_NaiveVue/.vitepress/dist/assets/zh_architecture_security.md.zD5PJKA2.lean.js
T
2026-10-11 19:06:33 +08:00

235 lines
84 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import{_ as i,o as a,c as n,a2 as t}from"./chunks/framework.BbmSSMjL.js";const o=JSON.parse('{"title":"4.10 安全架构设计","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 &quot;4.10 安全架构设计&quot;">​</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 &lt;token&gt;</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 &quot;安全架构总览&quot;">​</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 &quot;一、认证机制&quot;">​</a></h2><h3 id="登录流程" tabindex="-1">登录流程 <a class="header-anchor" href="#登录流程" aria-label="Permalink to &quot;登录流程&quot;">​</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&gt;0 时启用)</span></span>
<span class="line"><span> │ 失败 → 返回&quot;验证码错误&quot;</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> │ 非启用 → 返回&quot;账号已被禁用&quot;</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 &quot;JWT 双令牌认证&quot;">​</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 &lt;access_token&gt; │── 校验签名 + 有效期 + type=access</span></span>
<span class="line"><span> │ │── 注入 attributes[&#39;userInfo&#39;]</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 &quot;Token 设计&quot;">​</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 &quot;安全要点&quot;">​</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 &quot;二、授权机制&quot;">​</a></h2><h3 id="RBAC-权限模型-菜单即权限-" tabindex="-1">RBAC 权限模型(菜单即权限) <a class="header-anchor" href="#RBAC-权限模型-菜单即权限-" aria-label="Permalink to &quot;RBAC 权限模型(菜单即权限)&quot;">​</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 &quot;权限校验流程(Authenticate::checkPermission)&quot;">​</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;">&quot;</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;">&quot;</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">*:*:*</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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;"> &quot;</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;">&quot;</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;">-&gt;</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">*:*:*</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">user_id</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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;">-&gt;</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">role_id</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">-&gt;</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">menu</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">role_menu.menu_id</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">=</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">menu.id</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span></span>
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> -&gt;</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">role_menu.role_id</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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;"> -&gt;</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">menu.is_delete</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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;"> -&gt;</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">menu.type</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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;"> -&gt;</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">menu.permission</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">!=</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &#39;&#39;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span></span>
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> -&gt;</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">menu.permission</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span></span>
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> -&gt;</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 &quot;数据权限&quot;">​</a></h3><p><code>HasDataScope</code> 支持三种数据范围(当前 <code>laravel_role</code> 表不含 <code>data_scope</code> 字段,实际一律按&quot;全部数据&quot;执行):</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 &quot;三、防护机制&quot;">​</a></h2><h3 id="密码加密-bcrypt-双重加盐-" tabindex="-1">密码加密(bcrypt 双重加盐) <a class="header-anchor" href="#密码加密-bcrypt-双重加盐-" aria-label="Permalink to &quot;密码加密(bcrypt 双重加盐)&quot;">​</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, [&#39;cost&#39; =&gt; 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 &quot;参数校验(FormRequest 入口拦截)&quot;">​</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;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">username</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =&gt;</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">required|string|min:3|max:50</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
<span class="line"><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">realname</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =&gt;</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">required|string|max:50</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
<span class="line"><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">mobile</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =&gt;</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">nullable|max:20</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
<span class="line"><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">email</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =&gt;</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">nullable|email</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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 &quot;SQL 注入防护&quot;">​</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">username</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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;">-&gt;</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;">&quot;</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;">&quot;</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;">&quot;</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;"> &#39;</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;">&#39;</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&quot;</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 &quot;文件上传安全&quot;">​</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 &quot;XSS 防护&quot;">​</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 &quot;演示模式保护&quot;">​</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(&#39;app.demo_mode&#39;))</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 &quot;CORS 跨域控制&quot;">​</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 &quot;四、审计机制&quot;">​</a></h2><h3 id="操作日志-laravel-operation-log-" tabindex="-1">操作日志(laravel_operation_log) <a class="header-anchor" href="#操作日志-laravel-operation-log-" aria-label="Permalink to &quot;操作日志(laravel_operation_log)&quot;">​</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">用户管理-新增</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">新增用户:{username}</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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>{&quot;username&quot;:&quot;admin&quot;}</td></tr><tr><td><code>result</code></td><td>响应内容(截断 2000 字)</td><td>{&quot;code&quot;: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 &quot;登录日志(laravel_login_log)&quot;">​</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;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">username</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =&gt;</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;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">log_type</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =&gt;</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;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">source</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =&gt;</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;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">method</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =&gt;</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;">-&gt;</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;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">request_method</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =&gt;</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;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">url</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =&gt;</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;">-&gt;</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;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">param</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =&gt;</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">username</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =&gt;</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;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">result</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =&gt;</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">code</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =&gt;</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;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">msg</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =&gt;</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;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">ip</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =&gt;</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;">-&gt;</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;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">location</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> =&gt;</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;">-&gt;</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 &quot;日志安全要点&quot;">​</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 &quot;五、安全配置清单&quot;">​</a></h2><h3 id="环境变量-env-" tabindex="-1">环境变量(.env) <a class="header-anchor" href="#环境变量-env-" aria-label="Permalink to &quot;环境变量(.env)&quot;">​</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 &quot;Nginx 配置&quot;">​</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 &quot;安全检查清单&quot;">​</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 &quot;总结&quot;">​</a></h2><p>系统通过认证、授权、防护、审计四层安全架构,实现了从网络层到数据层的纵深防御。JWT 双令牌 + Redis 有状态保证认证安全且支持会话管控,RBAC 权限控制细化到按钮级(三级缓存保证性能),bcrypt 双重加盐保护密码安全,操作日志与登录日志实现全链路审计。各层安全机制相互独立又协同配合,确保系统在企业级应用场景下的安全可靠性。</p>`,74)])])}const c=i(p,[["render",l]]);export{o as __pageData,c as default};