API接口-authtoken、redis
This commit is contained in:
@@ -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 = [])
|
||||
{
|
||||
|
||||
96
app/api/controller/v1/Auth.php
Normal file
96
app/api/controller/v1/Auth.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\api\controller\v1;
|
||||
|
||||
use app\common\controller\Api;
|
||||
use app\common\library\AgentJwt;
|
||||
use app\common\model\ChannelManage;
|
||||
use app\admin\model\Admin;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
/**
|
||||
* API v1 鉴权接口
|
||||
*/
|
||||
class Auth extends Api
|
||||
{
|
||||
/**
|
||||
* Agent Token 类型
|
||||
*/
|
||||
public const TOKEN_TYPE = 'agent';
|
||||
|
||||
/**
|
||||
* 时间戳有效范围(秒),防止重放攻击
|
||||
*/
|
||||
protected int $timeTolerance = 300;
|
||||
|
||||
/**
|
||||
* 获取鉴权 Token
|
||||
* 参数:signature(签名)、secret(密钥)、agent_id(代理)、time(时间戳)
|
||||
* 返回:authtoken;失败返回 code=0 及失败信息
|
||||
*/
|
||||
public function authToken(Request $request): Response
|
||||
{
|
||||
$response = $this->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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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~',
|
||||
|
||||
@@ -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
|
||||
|
||||
67
app/common/library/AgentJwt.php
Normal file
67
app/common/library/AgentJwt.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use Firebase\JWT\ExpiredException;
|
||||
use Firebase\JWT\SignatureInvalidException;
|
||||
|
||||
/**
|
||||
* Agent 鉴权 JWT 工具
|
||||
*/
|
||||
class AgentJwt
|
||||
{
|
||||
public const ALG = 'HS256';
|
||||
|
||||
/**
|
||||
* 生成 JWT authtoken
|
||||
* @param array $payload agent_id, channel_id, admin_id 等
|
||||
* @param int $expire 有效期(秒)
|
||||
*/
|
||||
public static function encode(array $payload, int $expire = 86400): string
|
||||
{
|
||||
$now = time();
|
||||
$payload['iat'] = $now;
|
||||
$payload['exp'] = $now + $expire;
|
||||
$secret = self::getSecret();
|
||||
return JWT::encode($payload, $secret, self::ALG);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析并验证 JWT,返回 payload
|
||||
* @return array payload,失败返回空数组
|
||||
*/
|
||||
public static function decode(string $token): array
|
||||
{
|
||||
if ($token === '') {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
$secret = self::getSecret();
|
||||
$decoded = JWT::decode($token, new Key($secret, self::ALG));
|
||||
return (array) $decoded;
|
||||
} catch (ExpiredException|SignatureInvalidException|\Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 JWT 是否有效
|
||||
*/
|
||||
public static function verify(string $token): bool
|
||||
{
|
||||
return !empty(self::decode($token));
|
||||
}
|
||||
|
||||
private static function getSecret(): string
|
||||
{
|
||||
$secret = config('buildadmin.agent_auth.jwt_secret', '');
|
||||
if ($secret === '') {
|
||||
$secret = config('buildadmin.token.key', '');
|
||||
}
|
||||
return $secret;
|
||||
}
|
||||
}
|
||||
96
app/common/library/token/driver/Redis.php
Normal file
96
app/common/library/token/driver/Redis.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\token\driver;
|
||||
|
||||
use app\common\library\token\Driver;
|
||||
use support\Redis as RedisConnection;
|
||||
|
||||
/**
|
||||
* Token Redis 驱动(提升鉴权接口等高频调用的性能)
|
||||
* @see Driver
|
||||
*/
|
||||
class Redis extends Driver
|
||||
{
|
||||
protected array $options = [];
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user