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:
yangwx committed 2026-10-22 00:53:39 +08:00
1 parent 1d49e6f5ee
commit 18101cf11a
636 files changed
+58517 -9519

No files matched your search

+236
View File
@@ -0,0 +1,236 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace ywxapp\library;
/**
* 站点级 layui 主题变量层(整站换肤的唯一入口)。
*
* 与 SkinVariables(模板级、仅插件上下文生效)的定位区别:
* - SkinVariables:某套皮肤自己的变量(--primary / --secondary),
* 由 template.json 声明,用于该皮肤自己的视图与 CSS。
* - SiteTheme:整站 layui 组件的底色(--ywx-primary / --ywx-accent 等),
* 对所有页面生效(后台 / 会员 / 前台 / 插件前台),与皮肤层互不干扰。
*
* 变量名对应见 config/theme.php;CSS 消费方是 public/static/theme/layui-theme.css
* (由 layui.css 自动提取生成:主色硬编码已改写为 var(--ywx-*))。
*
* 安全:变量值会被拼进 <style>,因此写入前必须做白名单校验,
* 防止通过 `}` / `;` 逃逸出 CSS 上下文(CSS injection)。
*
* @author ywxapp <admin@ywxapp.cn>
*/
class SiteTheme
{
/**
* 变量名 => 默认值(与 layui-theme.css 内 :root 保持一致)
*/
public const DEFAULTS = [
'primary' => '#16b777',
'accent' => '#16baaa',
'radius' => '2px',
'font' => "-apple-system, BlinkMacSystemFont, 'Microsoft YaHei', 'PingFang SC', sans-serif",
];
/**
* 主题是否启用。
*/
public static function isEnabled(): bool
{
return (bool) config('theme.enable', true);
}
/**
* 当前生效变量(不含 CSS 变量前缀),即 config/theme.php 的键名 => 值。
* 值缺失(空)时回落到 DEFAULTS,保证后台表单永远有值可回填。
*/
public static function raw(): array
{
$conf = config('theme.variables', []);
if (! is_array($conf)) {
$conf = [];
}
$out = [];
foreach (self::DEFAULTS as $key => $default) {
$val = isset($conf[$key]) ? trim((string) $conf[$key]) : '';
$out[$key] = $val === '' ? $default : $val;
}
return $out;
}
/**
* 启用/禁用整站主题(写回 config/theme.php,用于排查样式冲突)。
*/
public static function setEnabled(bool $enable): void
{
$conf = config('theme', []);
$vars = is_array(config('theme.variables', [])) ? config('theme.variables', []) : [];
self::writeConfig(['enable' => $enable, 'variables' => $vars]);
unset($conf);
}
/**
* 当前生效变量(配置 + 默认值),返回带 -- 前缀的 CSS 变量名 => 值。
*/
public static function values(): array
{
$out = [];
foreach (self::raw() as $key => $val) {
$out['--ywx-' . $key] = $val;
}
return $out;
}
/**
* 生成 :root 内部声明片段(不含 :root{} 包裹)。
*/
public static function declaration(): string
{
$parts = [];
foreach (self::values() as $k => $v) {
$parts[] = $k . ':' . $v;
}
return implode(';', $parts);
}
/**
* 主题层 CSS 的 <link> 标签(须排在各页面自有 CSS 之后加载)。
*/
public static function linkTag(): string
{
return '<link rel="stylesheet" href="/static/theme/layui-theme.css">';
}
/**
* 站点变量 <style> 标签(含唯一 id,便于防重复注入)。
*/
public static function styleTag(): string
{
return '<style id="ywx-site-theme">:root{' . self::declaration() . '}</style>';
}
/**
* 保存后台配置的变量值到 config/theme.php(保留注释头),并同步内存配置。
*
* @param array $vars ['primary' => '#5b8cff', ...];非法值直接丢弃
* @return int 实际写入的变量数
*/
public static function save(array $vars): int
{
$clean = [];
foreach (self::DEFAULTS as $key => $default) {
if (! isset($vars[$key])) {
continue;
}
$val = trim((string) $vars[$key]);
if ($val === '') {
// 显式留空 = 恢复该变量的默认值(raw()/values() 会自动回落)
$clean[$key] = '';
continue;
}
if (! self::validate($key, $val)) {
continue; // 非法值直接丢弃,防止 CSS 注入
}
$clean[$key] = $val;
}
$current = config('theme', []);
$current['variables'] = array_merge(
is_array(config('theme.variables', [])) ? config('theme.variables', []) : [],
$clean
);
$current['enable'] = $current['enable'] ?? true;
self::writeConfig($current);
return count($clean);
}
/**
* 值白名单校验:只允许「CSS 安全」取值,禁止 ; { } < > @ url( 等逃逸字符。
*/
public static function validate(string $key, string $value): bool
{
$v = $value;
// 通用禁止:CSS 上下文逃逸字符
if (preg_match('/[;{}<>]/', $v)) {
return false;
}
if (stripos($v, 'url(') !== false || strpos($v, '@') !== false) {
return false;
}
// 控制字符(换行可注入新行导致解析异常)
if (preg_match('/[\x00-\x1F\x7F]/', $v)) {
return false;
}
switch ($key) {
case 'primary':
case 'accent':
// 颜色:hex / rgb() / rgba() / 颜色关键字
return (bool) preg_match('/^(#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgba?\(\s*[\d.,%\s]+\)|[a-zA-Z]{3,20})$/', $v);
case 'radius':
return (bool) preg_match('/^[\d.]+(?:px|rem|em|%)?$/', $v);
case 'font':
return (bool) preg_match('/^[\w\s,\'"\-]+$/u', $v);
default:
return false;
}
}
/**
* 写回 config/theme.php(含注释头),同步内存配置使当前请求立即生效。
*/
protected static function writeConfig(array $data): void
{
$file = config_path() . 'theme.php';
$vars = is_array($data['variables'] ?? null) ? $data['variables'] : [];
$lines = [];
foreach (self::DEFAULTS as $key => $default) {
$val = isset($vars[$key]) ? (string) $vars[$key] : '';
$lines[] = " '" . $key . "' => '" . addslashes($val) . "',";
}
$linesStr = implode("\n", $lines);
$enable = ! empty($data['enable']) ? 'true' : 'false';
$content = <<<PHP
<?php
// +----------------------------------------------------------------------
// | YwxApp 站点级 layui 主题配置
// +----------------------------------------------------------------------
// | 说明:这是「整站 layui 皮肤」的唯一变量源,对所有页面生效
// | (后台 / 会员中心 / 前台 / 各插件前台),由全局中间件 ThemeInject 注入。
// |
// | 变量名与 public/static/theme/layui-theme.css 中的 --ywx-* 一一对应:
// | primary -> --ywx-primary 主色(按钮/表单聚焦/进度条/时间轴)
// | accent -> --ywx-accent 强调色(导航/分页当前页/上传/logo)
// | radius -> --ywx-radius 统一圆角
// | font -> --ywx-font 正文字体
// |
// | 改这里即可全站换肤,无需改动任何视图文件、也无需清理缓存。
// | 后台可在「模板/主题」界面写入本文件(见 SiteTheme::save())。
// +----------------------------------------------------------------------
return [
// 主题开关:关闭后不注入任何主题样式(用于排查样式冲突)
'enable' => {$enable},
// 站点主题变量(值为空表示使用 layui-theme.css 内的兜底默认值)
'variables' => [
{$linesStr}
],
];
PHP;
file_put_contents($file, $content);
// 刚写入的 PHP 配置可能被 OPcache 缓存 2 秒(revalidate_freq),显式失效保证即时换肤
if (function_exists('opcache_invalidate')) {
@opcache_invalidate($file, true);
}
\think\facade\Config::set(['enable' => ! empty($data['enable']), 'variables' => $vars], 'theme');
}
}