From 4f61c9d7fcd608c9f1295bed9e3f83789b297089 Mon Sep 17 00:00:00 2001 From: zhenhui <1276357500@qq.com> Date: Thu, 19 Mar 2026 18:07:18 +0800 Subject: [PATCH] =?UTF-8?q?API=E6=8E=A5=E5=8F=A3-authtoken=E3=80=81redis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/library/Auth.php | 2 +- app/api/controller/v1/Auth.php | 96 +++++++++++++++++++++++ app/api/lang/en.php | 6 ++ app/api/lang/zh-cn.php | 6 ++ app/common/library/AgentJwt.php | 67 ++++++++++++++++ app/common/library/token/driver/Redis.php | 96 +++++++++++++++++++++++ app/functions.php | 12 +++ config/buildadmin.php | 15 +++- config/route.php | 3 + web/src/stores/adminInfo.ts | 1 + web/src/stores/interface/index.ts | 2 + 11 files changed, 303 insertions(+), 3 deletions(-) create mode 100644 app/api/controller/v1/Auth.php create mode 100644 app/common/library/AgentJwt.php create mode 100644 app/common/library/token/driver/Redis.php diff --git a/app/admin/library/Auth.php b/app/admin/library/Auth.php index 97c8e07..54a0128 100644 --- a/app/admin/library/Auth.php +++ b/app/admin/library/Auth.php @@ -33,7 +33,7 @@ class Auth extends \ba\Auth protected string $refreshToken = ''; protected int $keepTime = 86400; protected int $refreshTokenKeepTime = 2592000; - protected array $allowFields = ['id', 'username', 'nickname', 'avatar', 'last_login_time']; + protected array $allowFields = ['id', 'username', 'nickname', 'avatar', 'last_login_time', 'channel_id']; public function __construct(array $config = []) { diff --git a/app/api/controller/v1/Auth.php b/app/api/controller/v1/Auth.php new file mode 100644 index 0000000..208fa21 --- /dev/null +++ b/app/api/controller/v1/Auth.php @@ -0,0 +1,96 @@ +initializeApi($request); + if ($response !== null) { + return $response; + } + + $signature = $request->post('signature', $request->get('signature', '')); + $secret = $request->post('secret', $request->get('secret', '')); + $agentId = $request->post('agent_id', $request->get('agent_id', '')); + $time = $request->post('time', $request->get('time', '')); + + if ($signature === '' || $secret === '' || $agentId === '' || $time === '') { + return $this->error(__('Parameter %s can not be empty', ['signature/secret/agent_id/time'])); + } + + $timestamp = (int) $time; + if ($timestamp <= 0) { + return $this->error(__('Invalid timestamp')); + } + + $now = time(); + if ($timestamp < $now - $this->timeTolerance || $timestamp > $now + $this->timeTolerance) { + return $this->error(__('Timestamp expired')); + } + + $admin = Admin::where('agent_id', $agentId)->find(); + if (!$admin) { + return $this->error(__('Agent not found')); + } + + $channelId = (int) ($admin->channel_id ?? 0); + if ($channelId <= 0) { + return $this->error(__('Agent not found')); + } + + $channel = ChannelManage::where('id', $channelId)->find(); + if (!$channel || $channel->secret === '') { + return $this->error(__('Agent not found')); + } + + if ($channel->secret !== $secret) { + return $this->error(__('Invalid agent or secret')); + } + + $expectedSignature = hash_hmac('sha256', $agentId . $time, $channel->secret); + if (!hash_equals($expectedSignature, $signature)) { + return $this->error(__('Invalid signature')); + } + + $expire = (int) config('buildadmin.agent_auth.token_expire', 86400); + $payload = [ + 'agent_id' => $agentId, + 'channel_id' => $channel->id, + 'admin_id' => $admin->id, + ]; + $authtoken = AgentJwt::encode($payload, $expire); + + return $this->success('', [ + 'authtoken' => $authtoken, + ]); + } +} diff --git a/app/api/lang/en.php b/app/api/lang/en.php index 42619df..bb3ca44 100644 --- a/app/api/lang/en.php +++ b/app/api/lang/en.php @@ -12,6 +12,12 @@ return [ 'Please login first' => 'Please login first!', 'You have no permission' => 'No permission to operate!', 'Captcha error' => 'Captcha error!', + 'Parameter %s can not be empty' => 'Parameter %s can not be empty', + 'Invalid timestamp' => 'Invalid timestamp', + 'Timestamp expired' => 'Timestamp expired', + 'Invalid agent or secret' => 'Invalid agent or secret', + 'Invalid signature' => 'Invalid signature', + 'Agent not found' => 'Agent not found', // Member center account 'Data updated successfully~' => 'Data updated successfully~', 'Password has been changed~' => 'Password has been changed~', diff --git a/app/api/lang/zh-cn.php b/app/api/lang/zh-cn.php index 0e01a89..79c650a 100644 --- a/app/api/lang/zh-cn.php +++ b/app/api/lang/zh-cn.php @@ -42,6 +42,12 @@ return [ 'Please login first' => '请先登录!', 'You have no permission' => '没有权限操作!', 'Parameter error' => '参数错误!', + 'Parameter %s can not be empty' => '参数%s不能为空', + 'Invalid timestamp' => '无效的时间戳', + 'Timestamp expired' => '时间戳已过期', + 'Invalid agent or secret' => '代理或密钥无效', + 'Invalid signature' => '签名无效', + 'Agent not found' => '代理不存在', 'Token expiration' => '登录态过期,请重新登录!', 'Captcha error' => '验证码错误!', // 会员中心 account diff --git a/app/common/library/AgentJwt.php b/app/common/library/AgentJwt.php new file mode 100644 index 0000000..8cb223f --- /dev/null +++ b/app/common/library/AgentJwt.php @@ -0,0 +1,67 @@ +options = array_merge([ + 'prefix' => 'tk:', + 'expire' => 2592000, + ], $options); + $this->handler = RedisConnection::connection('default'); + } + + public function set(string $token, string $type, int $userId, ?int $expire = null): bool + { + if ($expire === null) { + $expire = $this->options['expire'] ?? 2592000; + } + $expireTime = $expire !== 0 ? time() + $expire : 0; + $key = $this->getKey($token); + $data = [ + 'token' => $token, + 'type' => $type, + 'user_id' => $userId, + 'create_time' => time(), + 'expire_time' => $expireTime, + ]; + $ttl = $expire !== 0 ? $expire : 365 * 86400; + $this->handler->setEx($key, $ttl, json_encode($data)); + return true; + } + + public function get(string $token): array + { + $key = $this->getKey($token); + $raw = $this->handler->get($key); + if ($raw === false || $raw === null) { + return []; + } + $data = json_decode($raw, true); + if (!is_array($data)) { + return []; + } + $data['expires_in'] = $this->getExpiredIn($data['expire_time'] ?? 0); + return $data; + } + + public function check(string $token, string $type, int $userId): bool + { + $data = $this->get($token); + if (!$data || ($data['expire_time'] && $data['expire_time'] <= time())) { + return false; + } + return $data['type'] === $type && (int) $data['user_id'] === $userId; + } + + public function delete(string $token): bool + { + $this->handler->del($this->getKey($token)); + return true; + } + + public function clear(string $type, int $userId): bool + { + $pattern = $this->options['prefix'] . '*'; + $keys = $this->handler->keys($pattern); + foreach ($keys as $key) { + $raw = $this->handler->get($key); + if ($raw !== false && $raw !== null) { + $data = json_decode($raw, true); + if (is_array($data) && ($data['type'] ?? '') === $type && (int) ($data['user_id'] ?? 0) === $userId) { + $this->handler->del($key); + } + } + } + return true; + } + + private function getKey(string $token): string + { + return $this->options['prefix'] . $this->getEncryptedToken($token); + } +} diff --git a/app/functions.php b/app/functions.php index 89a08a7..b89e844 100644 --- a/app/functions.php +++ b/app/functions.php @@ -165,6 +165,18 @@ if (!function_exists('get_auth_token')) { } } +if (!function_exists('get_agent_jwt_payload')) { + /** + * 解析 Agent JWT authtoken,返回 payload(agent_id、channel_id、admin_id 等) + * @param string $token authtoken + * @return array 成功返回 payload,失败返回空数组 + */ + function get_agent_jwt_payload(string $token): array + { + return \app\common\library\AgentJwt::decode($token); + } +} + if (!function_exists('get_controller_path')) { /** * 从 Request 或路由获取控制器路径(等价于 ThinkPHP controllerPath) diff --git a/config/buildadmin.php b/config/buildadmin.php index 5253a9e..7182521 100644 --- a/config/buildadmin.php +++ b/config/buildadmin.php @@ -39,9 +39,9 @@ return [ ], // 代理服务器IP(Request 类将尝试获取这些代理服务器发送过来的真实IP) 'proxy_server_ip' => [], - // Token 配置 + // Token 配置(鉴权接口 authtoken 等高频调用建议使用 redis 提升性能) 'token' => [ - // 默认驱动方式 + // 默认驱动:mysql | redis(redis 需确保 config/redis.php 已配置且 phpredis 扩展可用) 'default' => 'mysql', // 加密key 'key' => 'L1iYVS0PChKA9pjcFdmOGb4zfDIHo5xw', @@ -81,6 +81,17 @@ return [ 'cdn_url' => '', // 内容分发网络URL参数,将自动添加 `?`,之后拼接到 cdn_url 的结尾(例如 `imageMogr2/format/heif`) 'cdn_url_params' => '', + // 代理鉴权配置(/api/v1/authToken) + 'agent_auth' => [ + // agent_id => secret 映射 + 'agents' => [ + // 'agent_001' => 'your_secret_key', + ], + // JWT 签名密钥(留空则使用 token.key) + 'jwt_secret' => '', + // Token 有效期(秒),默认 24 小时 + 'token_expire' => 86400, + ], // 版本号 'version' => 'v2.3.6', // 中心接口地址(用于请求模块市场的数据等用途) diff --git a/config/route.php b/config/route.php index 0313449..79c8286 100644 --- a/config/route.php +++ b/config/route.php @@ -108,6 +108,9 @@ Route::post('/api/account/retrievePassword', [\app\api\controller\Account::class // api/ems Route::post('/api/ems/send', [\app\api\controller\Ems::class, 'send']); +// api/v1 鉴权 +Route::add(['GET', 'POST'], '/api/v1/authToken', [\app\api\controller\v1\Auth::class, 'authToken']); + // ==================== Admin 路由 ==================== // Admin 多为 JSON API,前端可能用 GET 传参查列表、POST 提交表单,使用 any 确保兼容 diff --git a/web/src/stores/adminInfo.ts b/web/src/stores/adminInfo.ts index 65ffd10..2bea1b7 100644 --- a/web/src/stores/adminInfo.ts +++ b/web/src/stores/adminInfo.ts @@ -13,6 +13,7 @@ export const useAdminInfo = defineStore('adminInfo', { token: '', refresh_token: '', super: false, + channel_id: 0, } }, actions: { diff --git a/web/src/stores/interface/index.ts b/web/src/stores/interface/index.ts index 5307183..7770cdf 100644 --- a/web/src/stores/interface/index.ts +++ b/web/src/stores/interface/index.ts @@ -113,6 +113,8 @@ export interface AdminInfo { refresh_token: string // 是否是 superAdmin,用于判定是否显示终端按钮等,不做任何权限判断 super: boolean + // 渠道ID(创建子管理员时默认绑定) + channel_id?: number } export interface UserInfo {