Become a sponsor

概述
本章详细阐述系统的安全架构设计,围绕认证、授权、防护、审计四大安全领域展开,旨在帮助开发者全面理解系统的安全保障体系。
认证(Authentication):系统采用基于 JWT 的无状态认证机制,用户通过用户名密码登录后获取 Token,后续请求通过 Authorization: Bearer <token> 头传递,由 AuthMiddleware 统一拦截并验证 Token 的有效性(签名校验、有效期检查)。
授权(Authorization):基于 RBAC 模型设计,权限控制粒度细化至接口级。用户通过角色继承权限节点(如 sys:user:add),由 #[Permission] 注解在控制器层声明,AuthMiddleware 统一校验。管理员(uid=1)自动放行。
防护(Protection):系统从多维度构建安全防护体系。参数校验通过 Validate 自动拦截非法输入;敏感密码采用 bcrypt 双重加盐哈希存储;演示模式下 DemoMiddleware 统一拦截写操作;跨域配置精确控制可信域名。
审计(Audit):系统提供完备的操作审计能力。LogMiddleware 自动记录所有写操作的请求路径、参数、操作人、IP 地址及执行结果;登录日志独立记录每次登录尝试,便于安全事件追溯。
安全设计原则
┌─────────────────────────────────────────────────────────────────┐
│ 安全防御层次 │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 网络层防护 │ │
│ │ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ CORS │ │ 上传体积限制 │ │ │
│ │ │ 跨域控制 │ │ DoS 防御 │ │ │
│ │ └──────────┘ └──────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 认证层 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ JWT 认证 │ │ 验证码 │ │ 双令牌 │ │ │
│ │ │ Bearer │ │ 图形验证 │ │ access+ │ │ │
│ │ │ │ │ │ │ refresh │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 授权层 │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │
│ │ │ RBAC 权限 │ │ 演示模式保护 │ │ 超级管理员跳过 │ │ │
│ │ │ #[Permission]│ │ DemoMiddleware│ │ uid=1 bypass │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 数据层 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ 密码加密 │ │ 参数校验 │ │ SQL 注入 │ │ 操作日志 │ │ │
│ │ │ bcrypt │ │ Validate │ │ ORM 防护 │ │ 审计追踪 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘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 }[前端] [后端]
│ │
│── 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 类型 | 有效期 | 用途 | payload.type |
|---|---|---|---|
| access_token | 2 小时 | 接口认证 | access |
| refresh_token | 7 天 | 刷新令牌 | refresh |
status != 1 时禁止登录type 字段区分,不可混用.env 配置,生产环境务必修改为强随机字符串┌─────────┐ ┌────────────┐ ┌─────────┐ ┌────────────┐ ┌─────────┐
│ User │───►│ UserRole │───►│ Role │───►│ RoleMenu │───►│ Menu │
│ │ │ │ │ │ │ │ │ │
│ id │ │ user_id │ │ id │ │ role_id │ │ id │
│ username│ │ role_id │ │ name │ │ menu_id │ │ perm │
└─────────┘ └────────────┘ │data_scope│ └────────────┘ │ type │
└─────────┘ └─────────┘// 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 = 用户名 |
密码存储流程:
原始密码 + 外部 salt(10位随机字符串)
│
▼ password_hash($combined, PASSWORD_BCRYPT, ['cost' => 12])
│
▼ bcrypt 内部自动生成 salt 并嵌入哈希
│
▼ 存储:password 字段(60字符)+ salt 字段(10字符)
密码校验流程:
用户输入密码 + 数据库 salt
│
▼ password_verify($input . $salt, $hashed)
│
▼ true/false// 加密
$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 时自动校验参数格式:
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,不进入业务逻辑层。
ThinkPHP ORM 使用参数绑定,自动防止 SQL 注入:
// ✅ 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,也必须使用参数绑定(? 占位符)。
上传文件安全检查链:
│
▼ 1. 文件大小限制(config/file.php → max_size,默认 10MB)
│
▼ 2. 文件扩展名白名单(config/file.php → allow_ext)
│
▼ 3. MIME 类型检查(fileinfo 扩展)
│
▼ 4. 生成随机文件名(防止路径遍历攻击)
│
▼ 5. 存储到 Web 根目录之外(或 Nginx 禁止执行 PHP)# Nginx 配置:上传目录禁止执行 PHP
location ~* /uploads/.*\.php$ {
deny all;
}防护措施:
1. 前端:用户输入转义显示(Vue 默认转义 {{ }} 插值)
2. 后端:富文本内容通过 save_content() 处理,过滤危险标签
3. 响应头:Content-Type: application/json(非 text/html)// DemoMiddleware 拦截写操作
// .env: [APP] DEMO = true
// 受保护:POST/PUT/DELETE 请求 → 返回 403
// 放行:GET 请求 + 标注 #[DemoAllow] 的方法// 标注 #[DemoAllow] 的方法在演示模式下仍可执行
#[DemoAllow]
public function login(): Json { ... }; .env
; 生产环境:限制具体域名
[CORS]
ALLOW_ORIGIN = https://admin.example.com
; 开发环境:允许所有(仅限开发)
[CORS]
ALLOW_ORIGIN = *// CorsMiddleware 处理逻辑:
// 1. OPTIONS 预检请求 → 直接返回 204 + CORS 头
// 2. 普通请求 → 校验 Origin 是否在白名单
// 3. 设置 Access-Control-Allow-Origin / Allow-Methods / Allow-HeadersLogMiddleware 基于 #[Log] 注解自动记录所有写操作:
#[Log('用户管理-新增记录', Log::TYPE_ADD, '新增用户:{username}')]
public function add(): Json { ... }记录字段:
| 字段 | 说明 | 示例 |
|---|---|---|
title | 操作标题(来自注解) | 用户管理-新增记录 |
type | 操作类型(常量) | 1=新增 2=修改 3=删除 4=查询 5=导入 6=导出 |
method | HTTP 方法 | POST |
url | 请求路径 | /api/user/add |
params | 请求参数(JSON) | {"username":"admin"} |
ip | 客户端 IP | 192.168.1.100 |
location | IP 归属地 | 江苏省南京市 |
os | 操作系统 | Windows 10 |
browser | 浏览器 | Chrome 120 |
username | 操作人 | admin |
status | 执行状态 | 0=成功 1=失败 |
error_msg | 错误信息(失败时) | 用户名已存在 |
duration | 执行耗时(ms) | 125 |
create_time | 操作时间 | 2026-09-22 10:30:00 |
记录规则:
#[Log] 指定 TYPE_QUERY 时记录config/middleware.php → log_except 中的路径不记录// LoginLogic 中独立记录
$requestInfo = RequestInfoService::create(request());
LoginLog::create([
'username' => $username,
'type' => 1, // 1=登录 2=登出
'status' => $success ? 0 : 1, // 0=成功 1=失败
'message' => $msg,
'ip' => $requestInfo->getIp(),
'location' => $requestInfo->getIpLocation(),
'os' => $requestInfo->getOs(),
'browser' => $requestInfo->getBrowser(),
]);记录字段:
| 字段 | 说明 |
|---|---|
username | 登录用户名(无论成功失败都记录) |
type | 1=登录 2=登出 |
status | 0=成功 1=失败 |
message | 结果消息(如"登录成功"、"密码错误") |
ip | 客户端 IP |
location | IP 归属地 |
os | 操作系统 |
browser | 浏览器 |
| 要点 | 说明 |
|---|---|
| 密码不记录 | 登录日志不记录密码字段 |
| 参数可脱敏 | 敏感字段建议在 Controller 层处理后再传入 Logic |
| 独立存储 | 操作日志和登录日志分别存储在不同表 |
| 不可篡改 | 日志仅 INSERT,无 UPDATE/DELETE 接口 |
| 配置项 | 开发环境 | 生产环境 | 说明 |
|---|---|---|---|
APP_DEBUG | true | false | 关闭调试模式,隐藏详细错误 |
SHOW_ERROR_MSG | true | false | 隐藏原始错误信息 |
JWT.SECRET | 默认值 | 强随机密钥 | 32 字节以上随机字符串 |
CORS.ALLOW_ORIGIN | * | 指定域名 | 如 https://admin.example.com |
APP.DEMO | false | false | 演示环境按需开启 |
FILE.MAX_SIZE | 10MB | 合理限制 | 防止大文件 DoS |
API.CAMEL_SNAKE_CONVERT | true | true | 驼峰转换开关 |
# 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;上线前逐项检查:
□ .env 中 APP_DEBUG = false
□ .env 中 SHOW_ERROR_MSG = false
□ .env 中 JWT.SECRET 已更换为强随机密钥
□ .env 中 CORS.ALLOW_ORIGIN 指定具体域名
□ Nginx 已配置 HTTPS
□ Nginx 上传目录已禁止执行 PHP
□ 数据库密码为强密码
□ Redis 已设置密码(如使用)
□ 默认管理员密码已修改
□ 演示模式已关闭(除非需要)
□ 日志中不包含密码等敏感信息系统通过认证、授权、防护、审计四层安全架构,实现了从网络层到数据层的纵深防御。JWT 双令牌认证保证无状态安全,RBAC 权限控制细化到按钮级,bcrypt 双重加盐保护密码安全,操作日志实现全链路审计。各层安全机制相互独立又协同配合,确保系统在企业级应用场景下的安全可靠性。