From 7ffbc6d1733a0dd71e2398a3fdcfbfc4d50c5a48 Mon Sep 17 00:00:00 2001 From: jsasg <735273025@qq.com> Date: Tue, 20 May 2025 14:40:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BC=80=E6=94=BEAPI=E6=8E=88=E6=9D=83?= =?UTF-8?q?=E7=9B=B8=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/openapi/common.php | 2 + app/openapi/controller/v1/Authorize.php | 37 ++++ app/openapi/event.php | 5 + app/openapi/middleware.php | 5 + app/openapi/middleware/Auth.php | 31 ++++ app/openapi/model/OAuthAccessTokenModel.php | 32 ++++ app/openapi/model/OAuthAuthCodeModel.php | 33 ++++ app/openapi/model/OAuthClientModel.php | 33 ++++ app/openapi/model/OAuthRefreshTokenModel.php | 32 ++++ app/openapi/route/v1.php | 35 ++++ composer.json | 3 +- .../20250519083010_create_oauth_client.php | 42 +++++ .../20250519083050_create_oauth_auth_code.php | 41 +++++ ...250519083107_create_oauth_access_token.php | 40 +++++ ...50520014709_create_oauth_refresh_token.php | 40 +++++ extend/oauth/OAuthStorage.php | 167 ++++++++++++++++++ 16 files changed, 577 insertions(+), 1 deletion(-) create mode 100644 app/openapi/common.php create mode 100644 app/openapi/controller/v1/Authorize.php create mode 100644 app/openapi/event.php create mode 100644 app/openapi/middleware.php create mode 100644 app/openapi/middleware/Auth.php create mode 100644 app/openapi/model/OAuthAccessTokenModel.php create mode 100644 app/openapi/model/OAuthAuthCodeModel.php create mode 100644 app/openapi/model/OAuthClientModel.php create mode 100644 app/openapi/model/OAuthRefreshTokenModel.php create mode 100644 app/openapi/route/v1.php create mode 100644 database/migrations/20250519083010_create_oauth_client.php create mode 100644 database/migrations/20250519083050_create_oauth_auth_code.php create mode 100644 database/migrations/20250519083107_create_oauth_access_token.php create mode 100644 database/migrations/20250520014709_create_oauth_refresh_token.php create mode 100644 extend/oauth/OAuthStorage.php diff --git a/app/openapi/common.php b/app/openapi/common.php new file mode 100644 index 00000000..12436156 --- /dev/null +++ b/app/openapi/common.php @@ -0,0 +1,2 @@ +post([ + 'client_id', + 'client_secret', + 'grant_type', + 'refresh_token', + ]); + $server = request()->server(); + $request = new Request([], $post, [], [], [], $server); + $storage = new OAuthStorage; + $oauth = new OAuth2($storage); + $token = $oauth->grantAccessToken($request); + return success('success', json_decode($token->getContent(), true)); + } catch (OAuth2ServerException $e) { + return error($e->getMessage() . ' - ' . $e->getDescription()); + } catch (\Throwable $th) { + return error($th->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/openapi/event.php b/app/openapi/event.php new file mode 100644 index 00000000..4eff8908 --- /dev/null +++ b/app/openapi/event.php @@ -0,0 +1,5 @@ +getBearerToken(); + $oauth->verifyAccessToken($token); + } catch (OAuth2ServerException $e) { + return error('Unauthorized', $e->sendHttpResponse(), 401); + } + + return $next($request); + } +} diff --git a/app/openapi/model/OAuthAccessTokenModel.php b/app/openapi/model/OAuthAccessTokenModel.php new file mode 100644 index 00000000..9c640db9 --- /dev/null +++ b/app/openapi/model/OAuthAccessTokenModel.php @@ -0,0 +1,32 @@ + 'int', + 'client_id' => 'string', + 'user_id' => 'int', + 'access_token' => 'string', + 'expires' => 'int', + 'scope' => 'string', + 'created_at' => 'datetime', + ]; + + // access_token范围查询 + public function scopeAccessToken($query, $access_token) + { + $query->where('access_token', '=', $access_token); + } +} diff --git a/app/openapi/model/OAuthAuthCodeModel.php b/app/openapi/model/OAuthAuthCodeModel.php new file mode 100644 index 00000000..43d23aba --- /dev/null +++ b/app/openapi/model/OAuthAuthCodeModel.php @@ -0,0 +1,33 @@ + 'int', + 'code' => 'string', + 'client_id' => 'string', + 'user_id' => 'int', + 'expires' => 'int', + 'redirect_uri' => 'string', + 'scope' => 'string', + 'created_at' => 'datetime', + ]; + + // code范围查询 + public function scopeCode($query, $code) + { + $query->where('code', '=', $code); + } +} diff --git a/app/openapi/model/OAuthClientModel.php b/app/openapi/model/OAuthClientModel.php new file mode 100644 index 00000000..dc126189 --- /dev/null +++ b/app/openapi/model/OAuthClientModel.php @@ -0,0 +1,33 @@ + 'int', + 'client_id' => 'string', + 'client_secret' => 'string', + 'redirect_uri' => 'string', + 'enabled' => 'int', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime' + ]; + + // client_id范围查询 + public function scopeClientId($query, $client_id) + { + $query->where('client_id', '=', $client_id); + } +} diff --git a/app/openapi/model/OAuthRefreshTokenModel.php b/app/openapi/model/OAuthRefreshTokenModel.php new file mode 100644 index 00000000..c24bc00d --- /dev/null +++ b/app/openapi/model/OAuthRefreshTokenModel.php @@ -0,0 +1,32 @@ + 'int', + 'client_id' => 'string', + 'user_id' => 'int', + 'refresh_token' => 'string', + 'expires' => 'int', + 'scope' => 'string', + 'created_at' => 'datetime' + ]; + + // refresh_token范围查询 + public function scopeRefreshToken($query, $refresh_token) + { + $query->where('refresh_token', '=', $refresh_token); + } +} diff --git a/app/openapi/route/v1.php b/app/openapi/route/v1.php new file mode 100644 index 00000000..765b966d --- /dev/null +++ b/app/openapi/route/v1.php @@ -0,0 +1,35 @@ +middleware(\app\openapi\middleware\Auth::class); +}) +->middleware(\think\middleware\Throttle::class, [ + 'visit_rate' => '5/m', + 'visit_fail_response' => function (\think\middleware\Throttle $throttle, \think\Request $request, int $wait_seconds) { + return \think\Response::create('您的操作过于频繁, 请在 ' . $wait_seconds . ' 秒后再试。')->code(429); + }, +]); \ No newline at end of file diff --git a/composer.json b/composer.json index 5f3d933c..049a9ea8 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,8 @@ "topthink/think-throttle": "^2.0", "intervention/image": "^3.10", "topthink/think-cors": "^1.0", - "phpoffice/phpspreadsheet": "^3.8" + "phpoffice/phpspreadsheet": "^3.8", + "friendsofsymfony/oauth2-php": "^1.3" }, "require-dev": { "symfony/var-dumper": ">=4.2", diff --git a/database/migrations/20250519083010_create_oauth_client.php b/database/migrations/20250519083010_create_oauth_client.php new file mode 100644 index 00000000..57b28a57 --- /dev/null +++ b/database/migrations/20250519083010_create_oauth_client.php @@ -0,0 +1,42 @@ +table('oauth_client', ['engine' => 'MyISAM', 'collation' => 'utf8mb4_general_ci', 'comment' => 'OAuth 客户端表']); + $table->addColumn('agent_id', 'integer', ['limit' => 10, 'null' => true, 'comment' => '代理ID']) + ->addColumn('client_id', 'string', ['limit' => 32, 'comment' => '客户端id']) + ->addColumn('client_secret', 'string', ['limit' => 128, 'comment' => '客户端密钥']) + ->addColumn('redirect_uri', 'string', ['limit' => 255, 'comment' => '回调地址']) + ->addColumn('enabled', 'boolean', ['default' => 1, 'comment' => '是否启用']) + ->addColumn('created_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP', 'comment' => '创建时间']) + ->addColumn('updated_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', 'comment' => '更新时间']) + ->addColumn('deleted_at', 'timestamp', ['null' => true, 'default' => null, 'comment' => '删除时间']) + ->create(); + } +} diff --git a/database/migrations/20250519083050_create_oauth_auth_code.php b/database/migrations/20250519083050_create_oauth_auth_code.php new file mode 100644 index 00000000..4c34f484 --- /dev/null +++ b/database/migrations/20250519083050_create_oauth_auth_code.php @@ -0,0 +1,41 @@ +table('oauth_auth_code', ['engine' => 'MyISAM', 'collation' => 'utf8mb4_general_ci', 'comment' => '授权码表']); + $table->addColumn('code', 'string', ['limit' => 64, 'null' => false, 'default' => '', 'comment' => '授权码']) + ->addColumn('client_id', 'integer', ['limit' => 11, 'null' => false, 'default' => 0, 'comment' => '客户端ID']) + ->addColumn('user_id', 'integer', ['limit' => 11, 'null' => false, 'default' => 0, 'comment' => '用户ID']) + ->addColumn('expires', 'integer', ['null' => true, 'comment' => '过期时间']) + ->addColumn('redirect_uri', 'string', ['limit' => 255, 'null' => false, 'default' => '', 'comment' => '回调地址']) + ->addColumn('scope', 'string', ['limit' => 255, 'null' => false, 'default' => '', 'comment' => '授权范围']) + ->addColumn('created_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP', 'comment' => '创建时间']) + ->create(); + } +} diff --git a/database/migrations/20250519083107_create_oauth_access_token.php b/database/migrations/20250519083107_create_oauth_access_token.php new file mode 100644 index 00000000..633aab7b --- /dev/null +++ b/database/migrations/20250519083107_create_oauth_access_token.php @@ -0,0 +1,40 @@ +table('oauth_access_token', ['engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci', 'comment' => 'OAuth2访问令牌表']); + $table->addColumn('client_id', 'string', ['limit' => 32, 'default' => '', 'comment' => '客户端ID']) + ->addColumn('user_id', 'integer', ['limit' => 10, 'default' => 0, 'comment' => '用户ID']) + ->addColumn('access_token', 'string', ['limit' => 64, 'default' => '', 'comment' => '访问令牌']) + ->addColumn('expires', 'integer', ['limit' => 11, 'default' => 0, 'comment' => '过期时间']) + ->addColumn('scope', 'string', ['limit' => 2000, 'default' => '', 'comment' => '授权范围']) + ->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'comment' => '创建时间']) + ->create(); + } +} diff --git a/database/migrations/20250520014709_create_oauth_refresh_token.php b/database/migrations/20250520014709_create_oauth_refresh_token.php new file mode 100644 index 00000000..7c181584 --- /dev/null +++ b/database/migrations/20250520014709_create_oauth_refresh_token.php @@ -0,0 +1,40 @@ +table('oauth_refresh_token', ['engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci', 'comment' => 'OAuth2刷新令牌表']); + $table->addColumn('client_id', 'string', ['limit' => 32, 'default' => '', 'comment' => '客户端ID']) + ->addColumn('user_id', 'integer', ['limit' => 10, 'default' => 0, 'comment' => '用户ID']) + ->addColumn('refresh_token', 'string', ['limit' => 64, 'default' => '', 'comment' => '刷新令牌']) + ->addColumn('expires', 'integer', ['limit' => 11, 'default' => 0, 'comment' => '过期时间']) + ->addColumn('scope', 'string', ['limit' => 2000, 'default' => '', 'comment' => '授权范围']) + ->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'comment' => '创建时间']) + ->create(); + } +} diff --git a/extend/oauth/OAuthStorage.php b/extend/oauth/OAuthStorage.php new file mode 100644 index 00000000..ade0f5d7 --- /dev/null +++ b/extend/oauth/OAuthStorage.php @@ -0,0 +1,167 @@ +salt = $salt; + } + } + + public function addClient($client_id, $client_secret, $redirect_uri) + { + // 实现添加客户端的逻辑 + $client = OAuthClientModel::create([ + 'client_id' => $client_id, + 'client_secret' => hash('sha1', $client_id . $client_secret . $this->salt), + 'redirect_uri' => $redirect_uri, + ]); + + return !$client->isEmpty(); + } + + public function getAuthCode($code): IOAuth2AuthCode + { + // 实现获取授权码的逻辑 + $ret = OAuthAuthCodeModel::code($code)->find(); + if (is_null($ret)) { + throw new \Exception('授权码不存在'); + } + + return new OAuth2AuthCode($ret->client_id, '', $ret->expires, $ret->scope, null, $ret->redirect_uri); + } + + public function createAuthCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null) + { + // 实现创建授权码的逻辑 + $auth = OAuthAuthCodeModel::create([ + 'code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + ]); + + return !$auth->isEmpty(); + } + + public function markAuthCodeAsUsed($code) + { + // 实现标记授权码为已使用的逻辑 + OAuthAuthCodeModel::code($code)->delete(); + } + + public function getClient($client_id): IOAuth2Client + { + // 实现获取客户端的逻辑 + $ret = OAuthClientModel::clientId($client_id)->find(); + if (is_null($ret)) { + throw new \Exception('客户端不存在'); + } + if ($ret->enabled != 1) { + throw new \Exception('客户端已禁用'); + } + + return new OAuth2Client($ret->client_id, $ret->client_secret, [$ret->redirect_uri]); + } + + public function checkClientCredentials(IOAuth2Client $client, $client_secret = null): bool + { + // 实现检查客户端凭证的逻辑 + $client = OAuthClientModel::clientId($client->getPublicId())->find(); + if (is_null($client)) { + return false; + } + + return $client->client_secret == hash('sha1', $client->client_id . $client_secret . $this->salt); + } + + public function getAccessToken($access_token): IOAuth2AccessToken + { + // 实现获取访问令牌的逻辑 + $ret = OAuthAccessTokenModel::accessToken($access_token)->find(); + if (is_null($ret)) { + throw new \Exception('访问令牌不存在'); + } + + return new OAuth2AccessToken($ret->client_id, $ret->access_token, $ret->expires, $ret->scope, null); + } + + public function createAccessToken($access_token, IOAuth2Client $client, $user_id, $expires, $scope = null) + { + // 实现创建访问令牌的逻辑 + OAuthAccessTokenModel::create([ + 'access_token' => $access_token, + 'client_id' => $client->getPublicId(), + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope, + ]); + } + + public function checkClientCredentialsGrant(IOAuth2Client $client, $client_secret): array + { + // 实现检查受限授权类型的逻辑 + return ['issue_refresh_token' => true]; + } + + public function checkRestrictedGrantType(IOAuth2Client $client, $grant_type): bool + { + // 实现检查受限授权类型的逻辑 + return $grant_type == 'client_credentials' || $grant_type == 'refresh_token'; + } + + // IOAuth2RefreshTokens 接口方法实现 + public function getRefreshToken($refresh_token): IOAuth2Token + { + // 实现获取刷新令牌的逻辑 + $ret = OAuthRefreshTokenModel::refreshToken($refresh_token)->find(); + if (is_null($ret)) { + throw new \Exception('刷新令牌不存在'); + } + + return new OAuth2Token($ret->client_id, $ret->refresh_token, $ret->expires, $ret->scope, null); + } + + public function createRefreshToken($refresh_token, IOAuth2Client $client, $user_id, $expires, $scope = null) + { + // 实现创建刷新令牌的逻辑 + OAuthRefreshTokenModel::create([ + 'refresh_token' => $refresh_token, + 'client_id' => $client->getPublicId(), + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope, + ]); + } + + public function unsetRefreshToken($refresh_token) + { + // 实现注销刷新令牌的逻辑 + OAuthRefreshTokenModel::refreshToken($refresh_token)->delete(); + } +}