核心特性详解

9 大企业级能力的实现原理、代码示例与关键文件,每个特性都来自实际生产验证

01

🔑 JWT 多密钥轮换

三密钥 V1/V2/V3 + payload key_ver 字段,支持密钥滚动无感升级

📋 实现说明

配置三个密钥 JWT_KEY_V1/V2/V3,当前版本由 JWT_CURRENT_VERSION 控制。

生成 token 时在 payload 注入 key_ver 字段,标记使用的密钥版本。

验证 token 时通过 getKeyByVersion 解码 payload 取 key_ver 选择对应密钥。

密钥滚动流程:新增 V4 → 切换 JWT_CURRENT_VERSION=v4 → 旧 V1 token 仍可验证,新签发用 V4。

💻 代码示例

json
// JWT Payload 结构(HS256 签名)
{
  "user_id": 1,
  "login_type": "admin",        // admin 或 wechat
  "type": "access",             // access 或 refresh
  "key_ver": "v1",              // 密钥版本,用于滚动升级
  "ip": "192.168.1.1",          // 签发 IP
  "exp": 1735689600,            // 过期时间戳
  "iat": 1735675200             // 签发时间
}

// 过期时间策略
// wechat access: 7200s (2h)   refresh: 604800s (7d)
// admin   access: 14400s (4h) refresh: 1209600s (14d)
📄 关键文件backend/app/common/service/JwtService.php
02

🗄️ 双端缓存隔离

admin: 与 wx: 前缀分离管理后台与小程序登录态,互不干扰

📋 实现说明

CacheService::getPrefix(loginType) 返回 admin: 或 wx: 前缀。

token / refresh_token / device / user_info 四类键均带前缀。

管理后台与小程序登录态完全隔离,同一用户两端登录互不影响。

过期时间区分:WX access 7200s / refresh 604800s;ADMIN access 14400s / refresh 1209600s。

💻 代码示例

text
// Redis 缓存键结构
{prefix}token:{userId}:{deviceId}          // 访问令牌
{prefix}refresh_token:{userId}:{deviceId}  // 刷新令牌
{prefix}device:{userId}:{tokenId}          // 设备绑定
{prefix}user:{userId}                       // 用户信息(JSON)

// 前缀示例
admin:token:1:abc123      // 管理后台用户 1 的 token
wx:token:3:def456         // 小程序用户 3 的 token
user:perm_codes:1         // 权限码缓存(无前缀,1 小时过期)
📄 关键文件backend/app/common/service/CacheService.php
03

📲 设备指纹绑定

登录缓存 device 键,每次请求校验 device-id 头,实现单设备登录限制

📋 实现说明

登录时缓存 {prefix}device:{userId}:{tokenId} → deviceId。

AuthMiddleware 每次请求校验 device-id 头与 Redis 设备键。

同用户在新设备登录会 invalidate 旧设备的 token,实现单设备登录限制。

前端生成 deviceId:管理后台用 crypto.randomUUID(),小程序用 uni.getDeviceInfo()。

💻 代码示例

http
// 请求头示例
GET /user/my-info HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
device-id: 550e8400-e29b-41d4-a716-446655440000

// Redis 设备键
admin:device:1:token_abc123  →  "550e8400-e29b-41d4-a716-446655440000"

// 校验流程
// 1. AuthMiddleware 提取 Authorization Bearer token
// 2. JwtService::verifyToken 解码得到 user_id + tokenId
// 3. 从 device-id 头取值,与 Redis {prefix}device:{userId}:{tokenId} 比对
// 4. 不匹配返回 4003 DEVICE_VERIFY_FAILED
📄 关键文件backend/app/middleware/AuthMiddleware.php
04

🔄 401 刷新队列

单例锁 + 等待队列,并发 401 合并为一次刷新,避免重复请求

📋 实现说明

单例锁 isRefreshing:第一个 401 触发刷新,后续 401 进入队列等待。

队列 requestQueue:存 { originalRequest, resolve, reject }。

刷新成功后 flush 队列,每个等待请求标 _retry=true 重发。

刷新失败 reject 队列所有 Promise,防止永久 pending,并跳转登录页。

管理后台(axios)与小程序(uni.request)均实现此机制。

💻 代码示例

javascript
// 小程序 request.js 核心逻辑
let isRefreshing = false
const requestQueue = []

function handle401Error(originalRequest) {
  if (isRefreshing) {
    // 已在刷新中,入队等待
    return new Promise((resolve, reject) => {
      requestQueue.push({ originalRequest, resolve, reject })
    })
  }
  if (originalRequest._retry) {
    // 已重试过仍 401,跳登录
    return redirectToLogin()
  }
  isRefreshing = true
  return refreshTokenApi({ refresh_token: getRefreshToken() })
    .then(res => {
      // 写入新 token
      saveToken(res.data)
      // flush 队列,每个请求标 _retry=true 重发
      requestQueue.forEach(({ originalRequest, resolve, reject }) => {
        originalRequest._retry = true
        request(originalRequest).then(resolve).catch(reject)
      })
      requestQueue.length = 0
    })
    .catch(() => {
      // 刷新失败,reject 队列
      requestQueue.forEach(({ reject }) => reject(new Error('登录已过期')))
      requestQueue.length = 0
      redirectToLogin()
    })
    .finally(() => { isRefreshing = false })
}
📄 关键文件wechat-miniprogram/api/request.js
05

👥 RBAC 四要素

用户-部门-角色-权限四表关联,多部门多角色 + 默认身份

📋 实现说明

system_user / system_department / system_role / system_permission 四实体表。

system_user_department_role 三元关联表,is_default 标识默认身份。

system_role_permission 角色-权限关联表。

一个用户可在多个部门拥有不同角色,登录时按默认身份构建 active_* 字段。

前端 active_permission_codes 同时包含角色权限与直接授权权限。

💻 代码示例

javascript
// 登录返回的 userInfo 结构(节选)
{
  "user_id": 1,
  "real_name": "张三",
  "active_department_id": 1,
  "active_role_id": 2,
  "active_role_code": "system_admin",
  "active_permission_codes": [
    "system:user:view",
    "system:user:create",
    "system:user:update",
    "system:department:view",
    "feedback:reply"
  ],
  "departments": [
    {
      "department_id": 1,
      "department_name": "技术部",
      "is_default": 1,
      "roles": [{ "role_id": 2, "role_name": "系统管理员", "is_default": 1 }]
    }
  ]
}

// 管理后台按钮级权限指令
<el-button v-permission="'system:user:create'">新增用户</el-button>
<el-button v-permission="['system:user:update', 'system:user:delete']">操作</el-button>
📄 关键文件backend/app/common/traits/HasPermissions.php
06

🏢 部门隔离与单独授权

三向关联 + 直接授权,最终权限 = 角色权限 ∪ 直接授权

📋 实现说明

system_user_department_role 三向关联表实现部门隔离,用户在不同部门有不同角色。

system_user_permission 直接授权表,绕过角色直接给用户授权(仅 can_direct_assign=1 的权限)。

最终权限 = 角色权限 ∪ 直接授权权限,PermissionMiddleware 用 in_array 校验。

权限码缓存键 user:perm_codes:{userId},1 小时过期,角色/直接权限变更时主动清理。

小程序「部门隔离与单独授权」页可视化演示完整流程。

💻 代码示例

sql
-- 三向关联表:用户在不同部门有不同角色
SELECT u.real_name, d.name AS dept, r.name AS role, udr.is_default
FROM system_user_department_role udr
JOIN system_user u ON udr.user_id = u.id
JOIN system_department d ON udr.department_id = d.id
JOIN system_role r ON udr.role_id = r.id
WHERE udr.user_id = 1;

-- 直接授权表:绕过角色直接授权
SELECT u.real_name, p.code, p.name
FROM system_user_permission up
JOIN system_user u ON up.user_id = u.id
JOIN system_permission p ON up.permission_id = p.id
WHERE p.can_direct_assign = 1;

-- 直接授权 API
POST /admin/system/addDirectPermission    { user_id, permission_id }
POST /admin/system/removeDirectPermission { id }
📄 关键文件backend/app/system/controller/UserController.php
07

🔐 多登录方式

4 种 login_type 覆盖所有场景,统一入口统一返回

📋 实现说明

phone:手机号 + 密码 + 图形验证码(管理后台与小程序通用)。

wechat:微信第一步获取 openid,已绑定直接登录,未绑定返回 need_phone。

wechat_one_click:微信一键登录,同时获取 openid 与手机号(推荐)。

bind_phone:微信绑定手机号,通过手机号查询或创建用户。

IP/手机号双限流:5 次/15 分钟窗口,防止暴力破解。

💻 代码示例

json
// 登录请求(统一入口 POST /public/login)
{
  "login_type": "wechat_one_click",
  "code": "wx_login_code_xxx",
  "phone_code": "wx_phone_code_yyy"
}

// 登录成功返回
{
  "code": 1,
  "msg": "登录成功",
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
    "expires_in": 7200,
    "userInfo": {
      "user_id": 3,
      "real_name": "张三",
      "active_permission_codes": ["feedback:view", "feedback:reply"]
    },
    "need_complete_profile": false
  },
  "timestamp": 1735689600
}
📄 关键文件backend/app/common/controller/AuthController.php
08

🛡️ 安全防护

上传三重校验、AES 加密、SSRF 防护、LIKE 转义、敏感字段过滤、登录限流

📋 实现说明

上传三重校验:扩展名白名单 + MIME 类型 + 魔数校验,GD 重处理剥离恶意代码。

AI Key 加密:AES-256-CBC,密钥 hash('sha256', env('AI_CONFIG_ENCRYPT_KEY'), true),输出 base64(IV + 密文)。

SSRF 防护:validateApiUrl 解析 host → IP,过滤私有/保留 IP 段。

SQL 注入:escapeLikeKeyword 转义 LIKE 通配符 % _ \。

敏感字段:SystemUser 模型 $hidden + filterUserSensitiveFields 移除 password/salt/open_id 等。

登录限流:Cache key login_attempts:{type}:{id},5 次/15 分钟窗口。

💻 代码示例

php
// AI Key 加密/解密(AiConfigService.php)
public function encryptApiKey($apiKey) {
    $key = hash('sha256', env('AI_CONFIG_ENCRYPT_KEY', 'default'), true);
    $iv = random_bytes(16);
    $encrypted = openssl_encrypt($apiKey, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
    return base64_encode($iv . $encrypted);
}

// SSRF 防护
public function validateApiUrl($url) {
    $host = parse_url($url, PHP_URL_HOST);
    $ip = gethostbyname($host);
    // 过滤私有/保留 IP 段
    return filter_var($ip, FILTER_VALIDATE_IP,
        FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
}

// 上传三重校验
// 1. 扩展名白名单:jpg/jpeg/png/gif/webp
// 2. MIME 类型校验:image/jpeg、image/png 等
// 3. 魔数校验:FF D8 FF (JPEG)、89 50 4E 47 (PNG)
// 4. GD 重处理:imagecreatefromjpeg + imagecopy 剥离恶意代码
📄 关键文件backend/app/common/controller/UploadController.php
09

📦 统一响应格式

{code, msg, data, timestamp},code=1 成功、code=0 失败

📋 实现说明

全局函数 show($code, $message, $data, $httpCode) 统一包装 JSON 响应。

业务码分段:1 成功 / 0 失败 / 2-10 系统错误 / 1000 用户错误 / 4000 认证错误。

前端三端共享同一套业务码常量,统一处理逻辑。

异常处理 app/exception/Http.php 自动捕获并包装为统一格式。

💻 代码示例

json
// 成功响应
{
  "code": 1,
  "msg": "操作成功",
  "data": { "id": 1, "name": "张三" },
  "timestamp": 1735689600
}

// 失败响应
{
  "code": 0,
  "msg": "参数缺失",
  "data": null,
  "timestamp": 1735689600
}

// 业务码常量表(部分)
SUCCESS             = 1    // 成功
ERROR               = 0    // 失败
VALIDATE_FAILED     = 7    // 验证失败
USER_DISABLED       = 1000 // 用户已禁用
LOGIN_FAILED        = 4000 // 登录失败
NO_TOKEN            = 4001 // 未提供 token
VALIDATE_TOKEN      = 4002 // token 验证失败
DEVICE_VERIFY_FAILED= 4003 // 设备验证失败
REFRESH_TOKEN_INVALID=4004 // refresh token 无效
📄 关键文件backend/app/common.php