Skip to content

4.10 安全架构设计 ​

概述

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

认证(Authentication):系统采用基于 JWT 的无状态认证机制,用户通过用户名密码登录后获取 Token,后续请求通过 Authorization: Bearer <token> 头传递,由 AuthMiddleware 统一拦截并验证 Token 的有效性(签名校验、有效期检查)。

授权(Authorization):基于 RBAC 模型设计,权限控制粒度细化至接口级。用户通过角色继承权限节点(如 sys:user:add),由 #[Permission] 注解在控制器层声明,AuthMiddleware 统一校验。管理员(uid=1)自动放行。

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

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

安全设计原则

  • 纵深防御:认证、授权、参数校验多层防护,单一防线失效时仍有其他机制兜底
  • 最小权限:用户仅能访问已授权接口,角色权限按需分配
  • 数据不落地:敏感信息(Token、密码)不在日志中明文记录,密码哈希不可逆。注意:当前 LogMiddleware 会记录完整请求参数,敏感字段(如密码)建议在业务层脱敏后再传入
  • 安全默认:框架默认配置为安全模式(DEBUG 关闭、错误信息隐藏)

安全架构总览 ​

text
┌─────────────────────────────────────────────────────────────────┐
│                         安全防御层次                             │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │ 网络层防护                                                 │  │
│  │ ┌──────────┐ ┌──────────────┐                             │  │
│  │ │ CORS     │ │ 上传体积限制 │                             │  │
│  │ │ 跨域控制 │ │ DoS 防御     │                             │  │
│  │ └──────────┘ └──────────────┘                             │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │ 认证层                                                     │  │
│  │ ┌──────────┐ ┌──────────┐ ┌──────────┐                   │  │
│  │ │ JWT 认证 │ │ 验证码   │ │ 双令牌   │                   │  │
│  │ │ Bearer   │ │ 图形验证 │ │ access+  │                   │  │
│  │ │          │ │          │ │ refresh  │                   │  │
│  │ └──────────┘ └──────────┘ └──────────┘                   │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │ 授权层                                                     │  │
│  │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐   │  │
│  │ │ RBAC 权限    │ │ 演示模式保护 │ │ 超级管理员跳过   │   │  │
│  │ │ #[Permission]│ │ DemoMiddleware│ │ uid=1 bypass     │   │  │
│  │ └──────────────┘ └──────────────┘ └──────────────────┘   │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │ 数据层                                                     │  │
│  │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐  │  │
│  │ │ 密码加密 │ │ 参数校验 │ │ SQL 注入 │ │ 操作日志     │  │  │
│  │ │ bcrypt   │ │ Validate │ │ ORM 防护 │ │ 审计追踪     │  │  │
│  │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘  │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

一、认证机制 ​

登录流程 ​

text
1. 前端提交 { username, password, code, key }
                          │
                          ▼
2. CaptchaService::check(code, key)     ← 验证码校验
   │ 失败 → 返回"验证码错误"
   ▼
3. User::where('username', $username)   ← 查询用户
   │ 不存在 → 统一提示"用户名或密码错误"
   ▼
4. password_verify($password . $salt, $hashed)  ← bcrypt 校验
   │ 不匹配 → 统一提示"用户名或密码错误"
   ▼
5. 检查 user.status == 1                ← 账号状态
   │ 非启用 → 返回"账号已被禁用"
   ▼
6. JwtService::createTokenPair($uid)    ← 签发双令牌
   │
   ▼
7. 返回 { access_token, refresh_token, expires_in }

JWT 双令牌认证 ​

html
[前端]                    [后端]
   │                         │
   │── POST /api/login ────→│
   │   {username, password}  │
   │                         │── 验证码校验
   │                         │── 密码校验(bcrypt)
   │                         │── 签发 access_token + refresh_token
   │←─ {access_token, ───────│
   │    refresh_token}       │
   │                         │
   │── GET /api/xxx ────────→│
   │   Authorization:        │
   │   Bearer <access_token> │
   │                         │── 解析 JWT
   │                         │── 校验签名 + 有效期
   │                         │── 注入 userInfo
   │←─ {data} ──────────────│
   │                         │
   │── Token 过期 ──────────→│
   │                         │
   │── POST /api/oauth2/ ──→│
   │   token                 │
   │   {refresh_token}       │── 验证 refresh 类型
   │                         │── 签发新令牌对
   │←─ {new_access_token, ──│
   │    new_refresh_token}   │

Token 设计 ​

Token 类型有效期用途payload.type
access_token2 小时接口认证access
refresh_token7 天刷新令牌refresh

安全要点 ​

  1. 密码错误统一提示:无论用户不存在还是密码错误,统一提示"用户名或密码错误",避免暴露账号是否存在
  2. 账号状态校验:status != 1 时禁止登录
  3. Token 类型校验:access_token 和 refresh_token 通过 type 字段区分,不可混用
  4. 密钥安全:JWT 密钥通过 .env 配置,生产环境务必修改为强随机字符串

二、授权机制 ​

RBAC 权限模型 ​

typescript
┌─────────┐    ┌────────────┐    ┌─────────┐    ┌────────────┐    ┌─────────┐
│  User   │───►│ UserRole   │───►│  Role   │───►│ RoleMenu   │───►│  Menu   │
│         │    │            │    │         │    │            │    │         │
│ id      │    │ user_id    │    │ id      │    │ role_id    │    │ id      │
│ username│    │ role_id    │    │ name    │    │ menu_id    │    │ perm    │
└─────────┘    └────────────┘    │data_scope│   └────────────┘    │ type    │
                                 └─────────┘                     └─────────┘

权限校验流程 ​

php
// AuthMiddleware::checkPermission()

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

    // 2. 查询用户角色
    $roleIds = UserRole::where('user_id', $uid)->column('role_id');
    if (empty($roleIds)) {
        return false;
    }

    // 3. 通过角色关联获取权限节点
    $permissions = RoleMenu::alias('rm')
        ->join('think_menu m', 'rm.menu_id = m.id')
        ->whereIn('rm.role_id', $roleIds)
        ->where('m.is_delete', 0)
        ->where('m.type', 1)  // type=1 为权限节点
        ->column('m.permission');

    // 4. 判断是否包含目标权限
    return in_array($permissionCode, $permissions);
}

数据权限 ​

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

三、防护机制 ​

密码加密(bcrypt 双重加盐) ​

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

密码校验流程:
  用户输入密码 + 数据库 salt
      │
      ▼ password_verify($input . $salt, $hashed)
      │
      ▼ true/false
php
// 加密
$password = '123456';
$salt = 'xK7pMn3qRt';           // 外部盐(10位随机)
$combined = $password . $salt;   // 拼接
$hashed = password_hash($combined, PASSWORD_BCRYPT, ['cost' => 12]);
// 结果:$2y$12$...(60字符)

// 校验
$inputCombined = $userInput . $dbSalt;
$isValid = password_verify($inputCombined, $dbHashed);

为什么双重加盐?

外部 salt 存储在独立字段,即使数据库泄露,攻击者也需要同时获取 salt 才能进行暴力破解。bcrypt 内部 salt 进一步增加彩虹表攻击难度。

参数校验(自动拦截) ​

通过 Logic 层 $validateClass 配置,add/update 时自动校验参数格式:

php
class UserValidate extends Validate
{
    protected $rule = [
        'username' => 'require|unique:user',
        'realname' => 'require',
        'mobile'   => 'mobile',
        'email'    => 'email',
    ];
    protected $scene = [
        'add'    => ['username', 'realname', 'mobile', 'email'],
        'update' => ['realname', 'mobile', 'email'],
    ];
}

校验失败直接返回 422,不进入业务逻辑层。

SQL 注入防护 ​

ThinkPHP ORM 使用参数绑定,自动防止 SQL 注入:

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

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

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

安全红线

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

文件上传安全 ​

text
上传文件安全检查链:
    │
    ▼ 1. 文件大小限制(config/file.php → max_size,默认 10MB)
    │
    ▼ 2. 文件扩展名白名单(config/file.php → allow_ext)
    │
    ▼ 3. MIME 类型检查(fileinfo 扩展)
    │
    ▼ 4. 生成随机文件名(防止路径遍历攻击)
    │
    ▼ 5. 存储到 Web 根目录之外(或 Nginx 禁止执行 PHP)
ini
# Nginx 配置:上传目录禁止执行 PHP
location ~* /uploads/.*\.php$ {
    deny all;
}

XSS 防护 ​

text
防护措施:
  1. 前端:用户输入转义显示(Vue 默认转义 {{ }} 插值)
  2. 后端:富文本内容通过 save_content() 处理,过滤危险标签
  3. 响应头:Content-Type: application/json(非 text/html)

演示模式保护 ​

php
// DemoMiddleware 拦截写操作
// .env: [APP] DEMO = true

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

CORS 跨域控制 ​

ini
; .env
; 生产环境:限制具体域名
[CORS]
ALLOW_ORIGIN = https://admin.example.com

; 开发环境:允许所有(仅限开发)
[CORS]
ALLOW_ORIGIN = *
php
// CorsMiddleware 处理逻辑:
// 1. OPTIONS 预检请求 → 直接返回 204 + CORS 头
// 2. 普通请求 → 校验 Origin 是否在白名单
// 3. 设置 Access-Control-Allow-Origin / Allow-Methods / Allow-Headers

四、审计机制 ​

操作日志(think_operation_log) ​

LogMiddleware 基于 #[Log] 注解自动记录所有写操作:

php
#[Log('用户管理-新增记录', Log::TYPE_ADD, '新增用户:{username}')]
public function add(): Json { ... }

记录字段:

字段说明示例
title操作标题(来自注解)用户管理-新增记录
type操作类型(常量)1=新增 2=修改 3=删除 4=查询 5=导入 6=导出
methodHTTP 方法POST
url请求路径/api/user/add
params请求参数(JSON){"username":"admin"}
ip客户端 IP192.168.1.100
locationIP 归属地江苏省南京市
os操作系统Windows 10
browser浏览器Chrome 120
username操作人admin
status执行状态0=成功 1=失败
error_msg错误信息(失败时)用户名已存在
duration执行耗时(ms)125
create_time操作时间2026-09-22 10:30:00

记录规则:

  • 写操作(POST/PUT/DELETE)始终记录
  • GET 查询操作仅在 #[Log] 指定 TYPE_QUERY 时记录
  • 排除列表:config/middleware.php → log_except 中的路径不记录

登录日志(think_login_log) ​

php
// LoginLogic 中独立记录
$requestInfo = RequestInfoService::create(request());

LoginLog::create([
    'username' => $username,
    'type'     => 1,              // 1=登录 2=登出
    'status'   => $success ? 0 : 1, // 0=成功 1=失败
    'message'  => $msg,
    'ip'       => $requestInfo->getIp(),
    'location' => $requestInfo->getIpLocation(),
    'os'       => $requestInfo->getOs(),
    'browser'  => $requestInfo->getBrowser(),
]);

记录字段:

字段说明
username登录用户名(无论成功失败都记录)
type1=登录 2=登出
status0=成功 1=失败
message结果消息(如"登录成功"、"密码错误")
ip客户端 IP
locationIP 归属地
os操作系统
browser浏览器

日志安全要点 ​

要点说明
密码不记录登录日志不记录密码字段
参数可脱敏敏感字段建议在 Controller 层处理后再传入 Logic
独立存储操作日志和登录日志分别存储在不同表
不可篡改日志仅 INSERT,无 UPDATE/DELETE 接口

五、安全配置清单 ​

环境变量(.env) ​

配置项开发环境生产环境说明
APP_DEBUGtruefalse关闭调试模式,隐藏详细错误
SHOW_ERROR_MSGtruefalse隐藏原始错误信息
JWT.SECRET默认值强随机密钥32 字节以上随机字符串
CORS.ALLOW_ORIGIN*指定域名如 https://admin.example.com
APP.DEMOfalsefalse演示环境按需开启
FILE.MAX_SIZE10MB合理限制防止大文件 DoS
API.CAMEL_SNAKE_CONVERTtruetrue驼峰转换开关

Nginx 配置 ​

nginx
# 1. 启用 HTTPS
server {
    listen 443 ssl;
    ssl_certificate     /etc/ssl/cert.pem;
    ssl_certificate_key /etc/ssl/key.pem;
}

# 2. 上传目录禁止执行 PHP
location ~* /uploads/.*\.php$ {
    deny all;
}

# 3. 禁止访问隐藏文件
location ~ /\. {
    deny all;
}

# 4. 安全响应头
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy strict-origin-when-cross-origin;

安全检查清单 ​

text
上线前逐项检查:

□ .env 中 APP_DEBUG = false
□ .env 中 SHOW_ERROR_MSG = false
□ .env 中 JWT.SECRET 已更换为强随机密钥
□ .env 中 CORS.ALLOW_ORIGIN 指定具体域名
□ Nginx 已配置 HTTPS
□ Nginx 上传目录已禁止执行 PHP
□ 数据库密码为强密码
□ Redis 已设置密码(如使用)
□ 默认管理员密码已修改
□ 演示模式已关闭(除非需要)
□ 日志中不包含密码等敏感信息

总结 ​

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

小蚂蚁云团队 · 提供技术支持