核心特性详解
9 大企业级能力的实现原理、代码示例与关键文件,每个特性都来自实际生产验证
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。
💻 代码示例
// 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双端缓存隔离
admin: 与 wx: 前缀分离管理后台与小程序登录态,互不干扰
📋 实现说明
CacheService::getPrefix(loginType) 返回 admin: 或 wx: 前缀。
token / refresh_token / device / user_info 四类键均带前缀。
管理后台与小程序登录态完全隔离,同一用户两端登录互不影响。
过期时间区分:WX access 7200s / refresh 604800s;ADMIN access 14400s / refresh 1209600s。
💻 代码示例
// 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设备指纹绑定
登录缓存 device 键,每次请求校验 device-id 头,实现单设备登录限制
📋 实现说明
登录时缓存 {prefix}device:{userId}:{tokenId} → deviceId。
AuthMiddleware 每次请求校验 device-id 头与 Redis 设备键。
同用户在新设备登录会 invalidate 旧设备的 token,实现单设备登录限制。
前端生成 deviceId:管理后台用 crypto.randomUUID(),小程序用 uni.getDeviceInfo()。
💻 代码示例
// 请求头示例
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_FAILEDbackend/app/middleware/AuthMiddleware.php401 刷新队列
单例锁 + 等待队列,并发 401 合并为一次刷新,避免重复请求
📋 实现说明
单例锁 isRefreshing:第一个 401 触发刷新,后续 401 进入队列等待。
队列 requestQueue:存 { originalRequest, resolve, reject }。
刷新成功后 flush 队列,每个等待请求标 _retry=true 重发。
刷新失败 reject 队列所有 Promise,防止永久 pending,并跳转登录页。
管理后台(axios)与小程序(uni.request)均实现此机制。
💻 代码示例
// 小程序 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.jsRBAC 四要素
用户-部门-角色-权限四表关联,多部门多角色 + 默认身份
📋 实现说明
system_user / system_department / system_role / system_permission 四实体表。
system_user_department_role 三元关联表,is_default 标识默认身份。
system_role_permission 角色-权限关联表。
一个用户可在多个部门拥有不同角色,登录时按默认身份构建 active_* 字段。
前端 active_permission_codes 同时包含角色权限与直接授权权限。
💻 代码示例
// 登录返回的 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部门隔离与单独授权
三向关联 + 直接授权,最终权限 = 角色权限 ∪ 直接授权
📋 实现说明
system_user_department_role 三向关联表实现部门隔离,用户在不同部门有不同角色。
system_user_permission 直接授权表,绕过角色直接给用户授权(仅 can_direct_assign=1 的权限)。
最终权限 = 角色权限 ∪ 直接授权权限,PermissionMiddleware 用 in_array 校验。
权限码缓存键 user:perm_codes:{userId},1 小时过期,角色/直接权限变更时主动清理。
小程序「部门隔离与单独授权」页可视化演示完整流程。
💻 代码示例
-- 三向关联表:用户在不同部门有不同角色
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多登录方式
4 种 login_type 覆盖所有场景,统一入口统一返回
📋 实现说明
phone:手机号 + 密码 + 图形验证码(管理后台与小程序通用)。
wechat:微信第一步获取 openid,已绑定直接登录,未绑定返回 need_phone。
wechat_one_click:微信一键登录,同时获取 openid 与手机号(推荐)。
bind_phone:微信绑定手机号,通过手机号查询或创建用户。
IP/手机号双限流:5 次/15 分钟窗口,防止暴力破解。
💻 代码示例
// 登录请求(统一入口 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安全防护
上传三重校验、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 分钟窗口。
💻 代码示例
// 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统一响应格式
{code, msg, data, timestamp},code=1 成功、code=0 失败
📋 实现说明
全局函数 show($code, $message, $data, $httpCode) 统一包装 JSON 响应。
业务码分段:1 成功 / 0 失败 / 2-10 系统错误 / 1000 用户错误 / 4000 认证错误。
前端三端共享同一套业务码常量,统一处理逻辑。
异常处理 app/exception/Http.php 自动捕获并包装为统一格式。
💻 代码示例
// 成功响应
{
"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