chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+238
View File
@@ -0,0 +1,238 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace addon\mqttbroker\service;
use think\facade\Db;
/**
* MQTT 认证与 ACL 服务
*
* 认证:
* - allow_anonymous=1:不强制账号;若客户端提供了用户名,则仍校验其密码(存在账号时)。
* - allow_anonymous=0:必须提供用户名+密码且匹配启用中的账号。
* 密码兼容 bcrypt 哈希与明文(明文仅便于开发,生产请用哈希)。
*
* ACLacl_enabled=1 时生效):
* - 规则表按 sort 升序、id 升序匹配,命中第一条即决定 allow/deny。
* - target_typeall(全部) / user(按用户名) / client(按 clientId)。
* - topic 支持 MQTT 通配符(+ / #)与占位符 %u(用户名) %c(clientId)。
* - access1 订阅 / 2 发布 / 3 两者。
* - 超级用户(is_superuser=1)跳过 ACL。
* - $SYS/# 始终允许订阅、禁止发布。
* - 无命中时采用默认策略 acl_defaultallow/deny)。
*
* 表结构见 install.sql;本类启动时 ensureTables() 自愈建表。
*/
class Auth
{
const ACT_SUB = 1;
const ACT_PUB = 2;
/** @var array 运行配置 */
protected $config;
/** @var bool 是否启用 ACL */
protected $aclEnabled;
/** @var bool ACL 默认放行 */
protected $aclDefaultAllow;
/** @var array ACL 规则缓存 */
protected $aclRules = [];
/** @var int ACL 缓存刷新时间戳 */
protected $aclLoadedAt = 0;
/** @var int 缓存有效期(秒) */
protected $aclTtl = 30;
public function __construct(array $config)
{
$this->config = $config;
$this->aclEnabled = (string) ($config['acl_enabled'] ?? '0') === '1';
$this->aclDefaultAllow = (string) ($config['acl_default'] ?? 'allow') !== 'deny';
}
/**
* 建表自愈(未执行 install.sql / 老版本插件也可用)
*/
public function ensureTables(): void
{
try {
Db::execute("CREATE TABLE IF NOT EXISTS `wxapp_mqttbroker_auth` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(191) NOT NULL DEFAULT '',
`password` varchar(191) NOT NULL DEFAULT '',
`is_superuser` tinyint(1) NOT NULL DEFAULT '0',
`status` tinyint(1) NOT NULL DEFAULT '1',
`remark` varchar(255) DEFAULT NULL,
`create_at` int DEFAULT NULL,
`update_at` int DEFAULT NULL,
PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
Db::execute("CREATE TABLE IF NOT EXISTS `wxapp_mqttbroker_acl` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`target_type` varchar(16) NOT NULL DEFAULT 'all',
`target` varchar(191) NOT NULL DEFAULT '',
`topic` varchar(255) NOT NULL DEFAULT '',
`access` tinyint(1) NOT NULL DEFAULT '3',
`allow` tinyint(1) NOT NULL DEFAULT '1',
`sort` int NOT NULL DEFAULT '0',
`remark` varchar(255) DEFAULT NULL,
`create_at` int DEFAULT NULL,
PRIMARY KEY (`id`), KEY `idx_sort` (`sort`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
} catch (\Throwable $e) {
}
}
/**
* 认证
* @return array{ok:bool, superuser:bool, reason:string}
*/
public function authenticate(?string $username, ?string $password, string $clientId): array
{
$anonymous = (string) ($this->config['allow_anonymous'] ?? '1') === '1';
if ($username === null || $username === '') {
// 未提供用户名:匿名开则放行,否则拒绝
return $anonymous
? ['ok' => true, 'superuser' => false, 'reason' => 'anonymous']
: ['ok' => false, 'superuser' => false, 'reason' => 'username required'];
}
// 提供了用户名:查账号
$acc = null;
try {
$acc = Db::name('mqttbroker_auth')->where('username', $username)->find();
} catch (\Throwable $e) {
// 表不存在等异常:匿名开则放行
return $anonymous
? ['ok' => true, 'superuser' => false, 'reason' => 'auth-store-unavailable']
: ['ok' => false, 'superuser' => false, 'reason' => 'auth store unavailable'];
}
if (!$acc) {
// 无此账号:匿名开时视为普通匿名用户(用户名仅作标识),否则拒绝
return $anonymous
? ['ok' => true, 'superuser' => false, 'reason' => 'anonymous-named']
: ['ok' => false, 'superuser' => false, 'reason' => 'account not found'];
}
if ((int) $acc['status'] !== 1) {
return ['ok' => false, 'superuser' => false, 'reason' => 'account disabled'];
}
if (!$this->verifyPassword((string) $password, (string) $acc['password'])) {
return ['ok' => false, 'superuser' => false, 'reason' => 'bad password'];
}
return ['ok' => true, 'superuser' => (int) $acc['is_superuser'] === 1, 'reason' => 'ok'];
}
/**
* 密码校验:bcrypt/argon 走 password_verify,其余按明文比较
*/
protected function verifyPassword(string $input, string $stored): bool
{
if ($stored === '') {
return false;
}
if (preg_match('/^\$(2y|2a|argon2)/', $stored)) {
return password_verify($input, $stored);
}
return hash_equals($stored, $input);
}
/**
* ACL 校验
* @param int $action self::ACT_SUB | self::ACT_PUB
*/
public function checkAcl(?string $username, string $clientId, string $topic, int $action, bool $superuser = false): bool
{
// $SYS 特殊处理:只读
if (strncmp($topic, '$SYS', 4) === 0) {
return $action === self::ACT_SUB;
}
if (!$this->aclEnabled || $superuser) {
return true;
}
$this->loadAclRules();
$username = (string) $username;
foreach ($this->aclRules as $r) {
$acc = (int) $r['access'];
if ($acc !== 3 && $acc !== $action) {
continue;
}
$type = $r['target_type'];
if ($type === 'member' && $r['target'] !== $username) {
continue;
}
if ($type === 'client' && $r['target'] !== $clientId) {
continue;
}
$filter = str_replace(['%u', '%c'], [$username, $clientId], (string) $r['topic']);
if ($this->topicMatch($filter, $topic)) {
return (int) $r['allow'] === 1;
}
}
return $this->aclDefaultAllow;
}
/**
* 加载 ACL 规则(带 TTL 缓存)
*/
protected function loadAclRules(): void
{
$now = time();
if ($now - $this->aclLoadedAt < $this->aclTtl && $this->aclLoadedAt > 0) {
return;
}
try {
$this->aclRules = Db::name('mqttbroker_acl')->order('sort', 'asc')->order('id', 'asc')->select()->toArray();
} catch (\Throwable $e) {
$this->aclRules = [];
}
$this->aclLoadedAt = $now;
}
/**
* 主题过滤器匹配(与 Broker 中一致的 + / # 规则)
*/
protected function topicMatch(string $filter, string $topic): bool
{
if ($filter === $topic) {
return true;
}
$f = explode('/', $filter);
$t = explode('/', $topic);
foreach ($f as $i => $seg) {
if ($seg === '#') {
return true;
}
if ($seg === '+') {
if (!isset($t[$i])) {
return false;
}
continue;
}
if (!isset($t[$i]) || $seg !== $t[$i]) {
return false;
}
}
return count($f) === count($t);
}
/**
* 生成密码哈希(供后台创建账号使用)
*/
public static function hashPassword(string $plain): string
{
return password_hash($plain, PASSWORD_DEFAULT);
}
}