feat: 上线前安全加固与发布工程化;appmall 由插件迁移为独立应用
安全(P0) - 发布包签名:新增 scripts/package_keygen.php、scripts/package_verify.php、 ywxapp\service\PackageVerifier;FrameworkService::apply() 解压前强制验签 (ywxapp.framework_package_verify,默认开启) - JWT 收紧:config/jwt.php 移除公开默认密钥,JwtService::assertStrongSecret() 校验非空 / 长度≥32 / 已知弱值;.env.production.example 键名由 JWT_SECRET_KEY 修正为 JWT_SECRET - /api/v1/Article:免登录白名单收窄为只读接口,写操作字段白名单 + assertOwner() 越权校验 - 开发者令牌:查找接口不再回显明文、统一失败文案 + 限流 - TemplateInstaller:文件名白名单 + realpath 归一化 + 目录内断言,修复 Zip Slip;补 use ZipArchive - 后台登录:接入验证码、失败锁定与登录限流(config/ratelimit.php) - 缓存 / Session 驱动可由 .env 覆盖(CACHE_DRIVER / SESSION_TYPE),集群部署改 redis - paycenter 支付日志开关改为 PAY_LOG,不再跟随 APP_DEBUG 工程化 / 交付 - 新增 CHANGELOG.md 与 .env.production.example;composer.lock 入库锁定依赖版本 - 插件离线安装 CLI 增加 --uid,后台上传安装透传 uid,付费插件授权校验可通过 - 授权拒绝文案区分「未携带 uid」与「未找到有效授权」,便于排障 - .gitignore 放行 .env.production.example / .env.addon.example / .gitattributes / public/.htaccess 结构 - appmall 由 addon/appmall 迁移为独立应用 app/appmall(市场 / 开发者中心 / 审核 / 分润)
This commit is contained in:
1 parent
1d49e6f5ee
commit
18101cf11a
636 files changed
+58517
-9519
No files matched your search
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
/**
|
||||
* 一次性脚本:把「系统信息」页的权限节点改成控制台的清理权限。
|
||||
*
|
||||
* 背景:内置「系统信息」页(System 控制器 / view/system / modules/system.js / install.sql 的 system:index)
|
||||
* 已整体下线,清理能力迁到 Console 控制器:
|
||||
* - 页面节点 system:index ——> 删除(连同挂它下面的子权限与角色授权)
|
||||
* - 按钮节点 system:clean ——> 就地把 name/route 改成 console:clean,并挂到「控制台」菜单下
|
||||
*
|
||||
* 为什么要就地改名而不是新建:新节点的 id 是新的,角色授权表(backend_role_power)里没有它,
|
||||
* 非超管账号会因为拿不到这个按钮权限而被 auth.filterDom() 摘掉按钮。
|
||||
* 保留原 id 就等于保留全部既有授权,老库零感知。
|
||||
*
|
||||
* 用法:
|
||||
* php scripts/migrate_console_clean.php # 预演:只统计,不动库(默认)
|
||||
* php scripts/migrate_console_clean.php --force # 真正执行
|
||||
*
|
||||
* 幂等:跑第二次时 system:* 已不存在,只要 console:clean 还在「控制台」下就什么都不做。
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
$root = dirname(__DIR__) . DIRECTORY_SEPARATOR;
|
||||
$force = in_array('--force', $argv ?? [], true);
|
||||
|
||||
echo $force
|
||||
? "[模式] 执行:以下动作会真实落库\n"
|
||||
: "[模式] 预演(dry-run):只统计不改动,加 --force 才执行\n";
|
||||
|
||||
/* ------------------------- 读配置:极简 env() + include database.php ------------------------- */
|
||||
|
||||
$GLOBALS['__env'] = [];
|
||||
foreach ([$root . '.env', $root . '.env.local'] as $envFile) {
|
||||
if (! is_file($envFile)) {
|
||||
continue;
|
||||
}
|
||||
foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
$pos = strpos($line, '=');
|
||||
if ($pos === false) {
|
||||
continue;
|
||||
}
|
||||
$GLOBALS['__env'][trim(substr($line, 0, $pos))] = trim(trim(substr($line, $pos + 1)), "\"'");
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('env')) {
|
||||
function env(string $key = '', $default = null)
|
||||
{
|
||||
return $GLOBALS['__env'][$key] ?? $default;
|
||||
}
|
||||
}
|
||||
|
||||
$dbConfFile = $root . 'config' . DIRECTORY_SEPARATOR . 'database.php';
|
||||
if (! is_file($dbConfFile)) {
|
||||
fwrite(STDERR, "找不到配置文件: {$dbConfFile}\n");
|
||||
exit(1);
|
||||
}
|
||||
$dbConf = include $dbConfFile;
|
||||
$conn = $dbConf['connections'][$dbConf['default'] ?? 'mysql'] ?? [];
|
||||
|
||||
/* ------------------------- 连库 ------------------------- */
|
||||
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
|
||||
$conn['hostname'] ?? '127.0.0.1',
|
||||
(int) ($conn['hostport'] ?? 3306),
|
||||
$conn['database'] ?? '',
|
||||
$conn['charset'] ?: 'utf8mb4'
|
||||
);
|
||||
|
||||
try {
|
||||
$pdo = new PDO($dsn, $conn['username'] ?? 'root', $conn['password'] ?? '', [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
fwrite(STDERR, '数据库连接失败: ' . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$prefix = (string) ($conn['prefix'] ?? '');
|
||||
$powerTable = $prefix . 'backend_power';
|
||||
$rolePowerTable = $prefix . 'backend_role_power';
|
||||
$roleTable = $prefix . 'backend_role';
|
||||
|
||||
echo '[连接] ' . ($conn['hostname'] ?? '') . ':' . ($conn['hostport'] ?? 3306)
|
||||
. ' / ' . ($conn['database'] ?? '') . " (表前缀 {$prefix})\n";
|
||||
|
||||
$tableExists = static function (string $table) use ($pdo): bool {
|
||||
return (bool) $pdo->query("SHOW TABLES LIKE '{$table}'")->fetchColumn();
|
||||
};
|
||||
|
||||
if (! $tableExists($powerTable)) {
|
||||
echo "[权限] {$powerTable} 不存在,没什么可迁移的\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/* ------------------------- 1. 「控制台」节点:新权限的挂载点 ------------------------- */
|
||||
|
||||
$consoleRow = $pdo->query(
|
||||
"SELECT `id`, `title` FROM `{$powerTable}` WHERE `name` = 'console:index' ORDER BY `id` LIMIT 1"
|
||||
)->fetch();
|
||||
$consoleId = $consoleRow ? (int) $consoleRow['id'] : 0;
|
||||
|
||||
echo $consoleId
|
||||
? "[挂载点] 控制台 #{$consoleId}({$consoleRow['title']})\n"
|
||||
: "[挂载点] 找不到 console:index 节点,新节点将挂到根节点(pid=0)\n";
|
||||
|
||||
/* ------------------------- 2. 系统信息页节点:删除 ------------------------- */
|
||||
|
||||
$menuRow = $pdo->query(
|
||||
"SELECT `id`, `name`, `title` FROM `{$powerTable}` WHERE `name` = 'system:index' ORDER BY `id` LIMIT 1"
|
||||
)->fetch();
|
||||
|
||||
if ($menuRow) {
|
||||
$menuId = (int) $menuRow['id'];
|
||||
echo "[下线] 找到「{$menuRow['title']}」#{$menuId}({$menuRow['name']})\n";
|
||||
|
||||
$children = $pdo->query(
|
||||
"SELECT `id`, `name`, `title` FROM `{$powerTable}` WHERE `pid` = {$menuId} ORDER BY `id`"
|
||||
)->fetchAll();
|
||||
foreach ($children as $row) {
|
||||
echo sprintf(" 子节点 #%d %s (%s)\n", (int) $row['id'], $row['name'], $row['title']);
|
||||
}
|
||||
|
||||
$ids = array_merge([$menuId], array_map('intval', array_column($children, 'id')));
|
||||
$in = implode(',', $ids);
|
||||
|
||||
$hasRolePower = $tableExists($rolePowerTable);
|
||||
$grantRows = $hasRolePower
|
||||
? (int) $pdo->query("SELECT COUNT(*) FROM `{$rolePowerTable}` WHERE `power_id` IN ({$in})")->fetchColumn()
|
||||
: 0;
|
||||
|
||||
echo "[下线] 待删权限节点 " . count($ids) . " 个、角色授权 {$grantRows} 条\n";
|
||||
|
||||
if ($force) {
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
if ($hasRolePower) {
|
||||
$pdo->exec("DELETE FROM `{$rolePowerTable}` WHERE `power_id` IN ({$in})");
|
||||
}
|
||||
// system:clean 要在下一步改造成 console:clean,别在这里跟着父节点一起删了
|
||||
$pdo->exec(
|
||||
"DELETE FROM `{$powerTable}` WHERE `id` IN ({$in}) AND `name` NOT IN ('system:clean', 'console:clean')"
|
||||
);
|
||||
$pdo->commit();
|
||||
echo "[执行] 页面节点与其下无用子节点已删除(system:clean 保留待改造)\n";
|
||||
} catch (Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
fwrite(STDERR, '[失败] 删除回滚: ' . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo "[下线] 没有 system:index 节点,跳过\n";
|
||||
}
|
||||
|
||||
/* ------------------------- 3. 清理按钮权限:system:clean → console:clean ------------------------- */
|
||||
|
||||
$oldClean = $pdo->query(
|
||||
"SELECT `id`, `name`, `title` FROM `{$powerTable}` WHERE `name` = 'system:clean' ORDER BY `id` LIMIT 1"
|
||||
)->fetch();
|
||||
$newClean = $pdo->query(
|
||||
"SELECT `id`, `name`, `title` FROM `{$powerTable}` WHERE `name` = 'console:clean' ORDER BY `id` LIMIT 1"
|
||||
)->fetch();
|
||||
|
||||
if ($oldClean && $newClean) {
|
||||
// 两边都有:把授权合并到 console:clean,删掉旧的那个
|
||||
$oldId = (int) $oldClean['id'];
|
||||
$newId = (int) $newClean['id'];
|
||||
echo "[合并] system:clean #{$oldId} 与 console:clean #{$newId} 并存,授权合并到后者并删除旧节点\n";
|
||||
|
||||
if ($force && $tableExists($rolePowerTable)) {
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
// 目标节点已有授权的行会撞唯一键,先去重再迁移
|
||||
$owned = $pdo->query(
|
||||
"SELECT `role_id` FROM `{$rolePowerTable}` WHERE `power_id` = {$newId}"
|
||||
)->fetchAll(PDO::FETCH_COLUMN);
|
||||
$ownedIds = $owned ? implode(',', array_map('intval', $owned)) : '0';
|
||||
$pdo->exec(
|
||||
"DELETE FROM `{$rolePowerTable}` WHERE `power_id` = {$oldId} AND `role_id` IN ({$ownedIds})"
|
||||
);
|
||||
$pdo->exec("UPDATE `{$rolePowerTable}` SET `power_id` = {$newId} WHERE `power_id` = {$oldId}");
|
||||
$pdo->exec("DELETE FROM `{$powerTable}` WHERE `id` = {$oldId}");
|
||||
$pdo->commit();
|
||||
echo "[执行] 授权迁移完成,旧节点已删除\n";
|
||||
} catch (Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
fwrite(STDERR, '[失败] 合并回滚: ' . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
} elseif ($oldClean) {
|
||||
$oldId = (int) $oldClean['id'];
|
||||
echo "[改名] system:clean #{$oldId} → console:clean,路由 system/clean → console/clean,挂到 #{$consoleId}\n";
|
||||
if ($force) {
|
||||
$pdo->prepare(
|
||||
"UPDATE `{$powerTable}`
|
||||
SET `name` = 'console:clean', `route` = 'console/clean', `pid` = ?, `update_at` = ?
|
||||
WHERE `id` = ?"
|
||||
)->execute([$consoleId, time(), $oldId]);
|
||||
echo "[执行] 已改名(id 不变 ⇒ 角色授权自动保留)\n";
|
||||
}
|
||||
} elseif ($newClean) {
|
||||
echo "[跳过] console:clean #" . (int) $newClean['id'] . " 已就位\n";
|
||||
} else {
|
||||
echo "[补建] 没有 system:clean,也没有 console:clean —— 补建一个并授权给所有角色\n";
|
||||
if ($force) {
|
||||
$now = time();
|
||||
$newId = (int) $pdo->query("SELECT IFNULL(MAX(`id`), 0) + 1 FROM `{$powerTable}`")->fetchColumn();
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO `{$powerTable}`
|
||||
(`id`, `pid`, `title`, `name`, `type`, `route`, `icon`, `sort`, `addon`, `status`, `create_at`, `update_at`, `delete_at`)
|
||||
VALUES (?, ?, '清理缓存', 'console:clean', 3, 'console/clean', '', 1, NULL, 1, ?, ?, 0)"
|
||||
);
|
||||
$stmt->execute([$newId, $consoleId, $now, $now]);
|
||||
echo "[执行] 已补建 console:clean #{$newId}\n";
|
||||
|
||||
// 新节点不在任何角色的授权列表里 ⇒ 非超管看不见按钮,必须全角色补授权
|
||||
if ($tableExists($roleTable) && $tableExists($rolePowerTable)) {
|
||||
$roles = $pdo->query("SELECT `id` FROM `{$roleTable}`")->fetchAll(PDO::FETCH_COLUMN);
|
||||
$ins = $pdo->prepare(
|
||||
"INSERT INTO `{$rolePowerTable}` (`role_id`, `power_id`, `create_at`) VALUES (?, ?, ?)"
|
||||
);
|
||||
$n = 0;
|
||||
foreach ($roles as $roleId) {
|
||||
$ins->execute([(int) $roleId, $newId, $now]);
|
||||
$n++;
|
||||
}
|
||||
echo "[执行] 已授权给 {$n} 个角色\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo $force
|
||||
? "[完成] 权限迁移结束(控制台菜单与按钮权限每次请求读库,无需清缓存)\n"
|
||||
: "[提示] 以上为预演结果,未做任何改动。确认后执行: php scripts/migrate_console_clean.php --force\n";
|
||||
Reference in new issue
Block a user