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

+185
View File
@@ -0,0 +1,185 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
// | 语义化版本约束判定(Composer 约束语法子集)
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace ywxapp\utils;
/**
* 版本约束工具
*
* 支持的约束写法:
* 比较符 >=1.2.0 >1.0 <=2.0 ==1.0.0 =1.0.0 !=1.0.0
* 裸版本号 1.2.0(等价 ==1.2.0)
* caret ^1.2.3 => >=1.2.3 <2.0.0 ;^0.2.3 => >=0.2.3 <0.3.0(不跨段原则)
* tilde ~1.2.3 => >=1.2.3 <1.3.0 ;~1.2 => >=1.2.0 <2.0.0
* 通配符 1.2.* => >=1.2.0 <1.3.0 ;1.* => >=1.0.0 <2.0.0
* 与 AND '>=1.2 <2.0'(也可逗号分隔)
* 或 OR '<1.0 || >=2.0'(写作 | 亦可)
*
* 设计取舍:解析不出合法约束时一律**放行**(返回 true)。
* 依赖声明写错不该让整站无法安装,真实缺失由下游的实际报错兜底——这与上游
* AddonService::versionSatisfies() 的既有行为保持一致。
*/
class Version
{
/**
* 判定版本是否满足约束
*/
public static function satisfies(string $version, string $constraint): bool
{
$constraint = trim($constraint);
// 版本号缺失(依赖插件没写 version)或约束为空:一律放行,交给下游的真实报错兜底
if (trim($version) === '' || $constraint === '') {
return true;
}
$version = self::clean($version);
// PHP 版本这类写法(8.1、8.1.0)与插件版本一致处理
foreach (self::splitOr($constraint) as $orGroup) {
if ($orGroup === []) {
continue;
}
$ok = true;
foreach ($orGroup as $single) {
if (!self::matchSingle($version, $single)) {
$ok = false;
break;
}
}
if ($ok) {
return true;
}
}
return false;
}
/**
* 拆 OR:`||` / `|` 分隔
*
* @return array<int, array<int, string>> 每组是 AND 条件数组
*/
private static function splitOr(string $constraint): array
{
$groups = [];
foreach (preg_split('/\s*\|\|?\s*/', $constraint) ?: [] as $group) {
$group = trim($group);
if ($group === '') {
continue;
}
// AND:空白或逗号分隔
$ands = array_values(array_filter(array_map(
'trim',
preg_split('/[\s,]+/', $group) ?: []
)));
$groups[] = $ands;
}
return $groups;
}
/**
* 判定单个条件
*/
private static function matchSingle(string $version, string $single): bool
{
if (!preg_match('/^(<=|>=|==|!=|<|>|=|\^|~)?\s*v?(\d+(?:\.\d+)*(?:\.\*)?)$/i', $single, $m)) {
return true; // 非法约束:放行
}
$op = strtolower($m[1] ?? '');
$target = self::clean($m[2]);
$targetW = str_replace('.*', '', $m[2]); // 通配符段
switch ($op) {
case '^':
return version_compare($version, $target, '>=')
&& version_compare($version, self::upperCaret($targetW), '<');
case '~':
// ~1.2(两位)=> <2.0.0;~1.2.3(三位)=> <1.3.0
$upper = substr_count($targetW, '.') >= 1
? self::upperTilde($targetW)
: ((string) (((int) $targetW) + 1) . '.0.0');
return version_compare($version, $target, '>=')
&& version_compare($version, $upper, '<');
case '':
// 裸版本号:1.2.0 等价 ==;1.2.* 是通配
if (str_contains($m[2], '*')) {
return self::matchWildcard($version, $targetW);
}
return version_compare($version, $target, '==');
case '=':
return version_compare($version, $target, '==');
default:
// 通配符与 ^ / ~ 组合意义不明,退化为纯比较
return version_compare($version, $target, $op);
}
}
/**
* 通配在新判定:1.2.* => [1.2.0, 1.3.0);1.* => [1.0.0, 2.0.0)
*/
private static function matchWildcard(string $version, string $base): bool
{
$parts = explode('.', $base);
$lower = self::pad(implode('.', $parts));
$depth = count($parts);
$parts[$depth - 1] = (string) (((int) end($parts)) + 1);
$upper = self::pad(implode('.', array_slice($parts, 0, $depth)));
return version_compare($version, $lower, '>=') && version_compare($version, $upper, '<');
}
/**
* caret 上界:不改变最左侧非零位
* 1.2.3 -> 2.0.0 ;0.2.3 -> 0.3.0 ;0.0.3 -> 0.0.4
*/
private static function upperCaret(string $v): string
{
$parts = array_map('intval', explode('.', $v));
foreach ($parts as $i => $num) {
if ($num !== 0 || $i === count($parts) - 1) {
$parts[$i] = $num + 1;
$parts = array_slice($parts, 0, $i + 1);
return self::pad(implode('.', $parts));
}
}
return '9999999.0.0';
}
/**
* tilde 上界:递增最后给出的次末位(~1.2.3 -> 1.3.0)
*/
private static function upperTilde(string $v): string
{
$parts = explode('.', $v);
$parts[count($parts) - 2] = (string) (((int) $parts[count($parts) - 2]) + 1);
return self::pad(implode('.', array_slice($parts, 0, count($parts) - 1) + []));
}
/**
* 清理版本号:去 v 前缀,补齐到三段便于比较
*/
private static function clean(string $v): string
{
$v = strtolower(trim(ltrim(trim($v), 'vV')));
$v = preg_replace('/[^0-9.]/', '', $v) ?? '';
return self::pad($v);
}
/**
* 补齐 1.2 -> 1.2.0(version_compare 认为 1.2 == 1.2.0,但用于展示/拼接时统一)
*/
private static function pad(string $v): string
{
$parts = explode('.', $v === '' ? '0' : $v);
while (count($parts) < 3) {
$parts[] = '0';
}
return implode('.', $parts);
}
}