Files
easygoadmin-wiki/EasyGoAdmin_Beego_NaiveVue/zh/architecture/security.md
T
2026-10-20 21:19:44 +08:00

472 lines
19 KiB
Markdown
Raw Blame History

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