feat: 开放API授权相关

This commit is contained in:
2025-05-20 14:40:39 +08:00
parent 970ff14c22
commit 7ffbc6d173
16 changed files with 577 additions and 1 deletions

2
app/openapi/common.php Normal file
View File

@@ -0,0 +1,2 @@
<?php
// 这是系统自动生成的公共文件

View File

@@ -0,0 +1,37 @@
<?php
declare (strict_types = 1);
namespace app\openapi\controller\v1;
use OAuth2\OAuth2;
use OAuth2\OAuth2ServerException;
use oauth\OAuthStorage;
use Symfony\Component\HttpFoundation\Request;
class Authorize
{
/**
* 获取/刷新token
*/
public function token()
{
try {
$post = request()->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());
}
}
}

5
app/openapi/event.php Normal file
View File

@@ -0,0 +1,5 @@
<?php
// 这是系统自动生成的event定义文件
return [
];

View File

@@ -0,0 +1,5 @@
<?php
// 这是系统自动生成的middleware定义文件
return [
];

View File

@@ -0,0 +1,31 @@
<?php
declare (strict_types = 1);
namespace app\openapi\middleware;
use OAuth2\OAuth2;
use OAuth2\OAuth2ServerException;
use oauth\OAuthStorage;
class Auth
{
/**
* 处理请求
*
* @param \think\Request $request
* @param \Closure $next
* @return Response
*/
public function handle($request, \Closure $next)
{
try {
$oauth = new OAuth2(new OAuthStorage);
$token = $oauth->getBearerToken();
$oauth->verifyAccessToken($token);
} catch (OAuth2ServerException $e) {
return error('Unauthorized', $e->sendHttpResponse(), 401);
}
return $next($request);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use think\Model;
/**
* @mixin \think\Model
*/
class OAuthAccessTokenModel extends Model
{
// 表名
protected $name = 'oauth_access_token';
// 字段信息
protected $schema = [
'id' => '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);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use think\Model;
/**
* @mixin \think\Model
*/
class OAuthAuthCodeModel extends Model
{
// 表名
protected $name = 'oauth_auth_code';
// 字段信息
protected $schema = [
'id' => '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);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use think\Model;
/**
* @mixin \think\Model
*/
class OAuthClientModel extends Model
{
// 表名
protected $name = 'oauth_client';
// 字段信息
protected $schema = [
'id' => '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);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use think\Model;
/**
* @mixin \think\Model
*/
class OAuthRefreshTokenModel extends Model
{
// 表名
protected $name = 'oauth_refresh_token';
// 字段信息
protected $schema = [
'id' => '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);
}
}

35
app/openapi/route/v1.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
use think\facade\Route;
Route::group('v1', function() {
// 接口授权
Route::post('authorize', 'v1.Authorize/token');
// 接口授权后才能访问
Route::group(function(){
// 获取产品列表
Route::get('products', 'v1.Product/list');
Route::group('product', function() {
// 获取产品信息
Route::get(':id', 'v1.Product/detail');
// 获取产品分类
Route::get('categories', 'v1.Product/categories');
});
// 获取新闻动态
Route::get('news', 'v1.News/list');
Route::group('news', function() {
// 获取新闻详情
Route::get(':id', 'v1.News/detail');
});
})
->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);
},
]);

View File

@@ -32,7 +32,8 @@
"topthink/think-throttle": "^2.0", "topthink/think-throttle": "^2.0",
"intervention/image": "^3.10", "intervention/image": "^3.10",
"topthink/think-cors": "^1.0", "topthink/think-cors": "^1.0",
"phpoffice/phpspreadsheet": "^3.8" "phpoffice/phpspreadsheet": "^3.8",
"friendsofsymfony/oauth2-php": "^1.3"
}, },
"require-dev": { "require-dev": {
"symfony/var-dumper": ">=4.2", "symfony/var-dumper": ">=4.2",

View File

@@ -0,0 +1,42 @@
<?php
use think\migration\Migrator;
use think\migration\db\Column;
class CreateOauthClient extends Migrator
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* renameColumn
* addIndex
* addForeignKey
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->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();
}
}

View File

@@ -0,0 +1,41 @@
<?php
use think\migration\Migrator;
use think\migration\db\Column;
class CreateOauthAuthCode extends Migrator
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* renameColumn
* addIndex
* addForeignKey
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->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();
}
}

View File

@@ -0,0 +1,40 @@
<?php
use think\migration\Migrator;
use think\migration\db\Column;
class CreateOauthAccessToken extends Migrator
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* renameColumn
* addIndex
* addForeignKey
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->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();
}
}

View File

@@ -0,0 +1,40 @@
<?php
use think\migration\Migrator;
use think\migration\db\Column;
class CreateOauthRefreshToken extends Migrator
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* renameColumn
* addIndex
* addForeignKey
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->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();
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace oauth;
use app\openapi\model\OAuthAccessTokenModel;
use app\openapi\model\OAuthAuthCodeModel;
use app\openapi\model\OAuthClientModel;
use app\openapi\model\OAuthRefreshTokenModel;
use OAuth2\IOAuth2GrantClient;
use OAuth2\IOAuth2GrantCode;
use OAuth2\IOAuth2RefreshTokens;
use OAuth2\Model\IOAuth2AccessToken;
use OAuth2\Model\IOAuth2AuthCode;
use OAuth2\Model\IOAuth2Client;
use OAuth2\Model\IOAuth2Token;
use OAuth2\Model\OAuth2AccessToken;
use OAuth2\Model\OAuth2AuthCode;
use OAuth2\Model\OAuth2Client;
use OAuth2\Model\OAuth2Token;
/**
* OAuth2存储实现
*/
class OAuthStorage implements IOAuth2GrantCode, IOAuth2RefreshTokens, IOAuth2GrantClient
{
private $salt = '';
public function __construct($salt = null)
{
if (!is_null($salt)) {
$this->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();
}
}