Skip to content

Openapi

openapi 模块提供了平台接口的开放能力。如果后台接口需要曝露给外部使用,但又不想给对方创建用户。那么这个模块可以很好实现这个能力

数据结构

  • openapi_users 用户表
  • openapi_user_balance 用户余额表
  • openapi_request_log 请求日志表
php
  Schema::create('openapi_users', function (Blueprint $table) {
        $table->id()->comment('ID');
        $table->string('username')->comment('用户名');
        $table->string('mobile', 20)->comment('手机号');
        $table->string('password')->comment('密码');
        $table->string('company')->nullable()->comment('公司名称');
        $table->string('description')->nullable()->comment('描述');
        $table->unsignedInteger('qps')->default(100)->comment('每分钟的 QPS');
        $table->string('app_key')->comment('app key');
        $table->string('app_secret')->comment('密钥');
        $table->creatorId();
        $table->createdAt();
        $table->updatedAt();
        $table->deletedAt();

        $table->engine = 'InnoDB';
        $table->comment('openapi 用户表');
    });

  Schema::create('openapi_user_balance', function (Blueprint $table) {
        $table->id();
        $table->uuid();
        $table->integer('user_id')->comment('用户id');
        $table->unsignedInteger('balance')->default(0)->comment('用户余额');
        $table->createdAt();
        $table->updatedAt();
        $table->deletedAt();
        $table->engine = 'InnoDB';
        $table->comment('用户余额表');
    });

  Schema::create('openapi_request_log', function (Blueprint $table) {
        $table->id();
        $table->uuid('request_id')->comment('请求id');
        $table->string('app_key')->comment('app key');
        $table->json('data')->comment('请求数据');
        $table->createdAt();
        $table->updatedAt();

        $table->engine = 'InnoDB';
        $table->comment('openapi 请求日志');
    });

创建 openapi 用户

使用管理后台的 openapi 模块来创建用户 catchadmin 专业版-openapi 模块

QPS 字段限制了访问接口频率,目前这个字段是设置每分钟的接口频率

在创建成功之后,会分配给用户一个 AppKeyAppSecret。第三方用户可以使用他来接入

接入

想要使用openapi的功能,接入验签和速率限制都非常简单。一个简单的示例,在根目录 routes/api.php 添加如下路由代码

php
Route::prefix('v1')->middleware([
    \Modules\Openapi\Middlewares\CheckSignatureMiddleware::class
])->group(function () {
    Route::get('user', function (){
       return \Modules\Openapi\Facade\OpenapiResponse::success([]);
    });
});

在浏览器打开 域名/api/v1/user 链接,会出现如下响应,就说明已经成功了 catchadmin专业版-openapi

快速验证

如果你使用 apifox 软件的话,可以添加一个前置脚本快速验证下 catchadmin专业版-openapi 脚本内容如下

javascript
const appKey = pm.environment.get('app_key')
const appSecret = pm.environment.get('app_secret')
console.log(appSecret, appKey)
function createSign(params) {
  const keys = Object.keys(params).sort()
  const signStr = keys.map((key) => `${key}=${params[key]}`).join('&')

  return CryptoJS.HmacSHA256(signStr, appSecret).toString()
}
let params = {}
params['timestamp'] = Math.floor(Date.now() / 1000)

if (pm.request.method == 'GET') {
  queryString = pm.request.url.getQueryString()
  if (queryString) {
    pm.request.url
      .getQueryString()
      .split('&')
      .forEach((item) => {
        items = item.split('=')
        params[items[0]] = items[1]
      })
  }

  const signature = createSign(params)

  pm.request.headers.add({ key: 'app-key', value: appKey })
  pm.request.headers.add({ key: 'signature', value: signature })

  let query = ''
  for (let key in params) {
    query += key + '=' + params[key] + '&'
  }

  pm.request.url.query = query
} else {
  if (pm.request.body.mode == 'urlencoded') {
    pm.request.body.urlencoded.each((item) => {
      params[item.key] = item.value
    })

    const signature = createSign(params)

    pm.request.headers.add({ key: 'app-key', value: appKey })
    pm.request.headers.add({ key: 'signature', value: signature })

    pm.request.body.urlencoded.add({
      disabled: false,
      key: 'timestamp',
      value: params.timestamp
    })
  }

  if (pm.request.body.mode == 'formdata') {
  }
}

然后给选定的环境配置 appkeyappsecret, 如下 catchadmin专业版-openapi

通过apifox发送请求,出现如下结果,就说明成功了 catchadmin专业版-openapi

中间件

  • 验签中间件 \Modules\Openapi\Middlewares\CheckSignatureMiddleware::class
  • 速率(QPS)限制中间件 \Modules\Openapi\Middlewares\RateLimiterMiddleware::class

异常

openapi 模块的所有异常都需要继承 OpenapiException,例如非法的 AppKey

php
namespace Modules\Openapi\Exceptions;

use Modules\Openapi\Enums\Code;

// 不合法的 app key
class InvalidAppKeyException extends OpenapiException
{
    protected $code = Code::INVALID_APP_KEY;
}

找到 bootstrap/app.php,可以找到对应的 openapi exception 渲染。代码如下

php
$app = Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        //
    })
    ->withExceptions(function (Exceptions $exceptions) {
        // 这里就是用来处理 openapi exception
        // 其他类型自定义异常请自行处理
        // 系统异常请自行处理
        $exceptions->render(function (OpenapiException $exception, Request $request) {
            return OpenapiResponse::error($exception->getMessage(), $exception->getCode());
        });
    })->create();

Code 枚举

所有枚举都要继承 Modules\Openapi\Enums\Enum 接口

php
namespace Modules\Openapi\Enums;

enum Code: int implements Enum
{
    case SUCCESS = 10000; // 成功的code
    case FAILED = 10001; // 失败的 code
    case APP_KEY_LOST = 10002; // app key 失效
    case SIGNATURE_LOST = 10003; // 签名失效
    case INVALID_APP_KEY = 10004; // 无效app key
    case INVALID_SIGNATURE = 10005; // 无效签名
    case INVALID_TIMESTAMP = 10006; // 无效时间
    case Balance_NOT_ENOUGH = 10007; // 余额不足
    case RATE_LIMIT = 10008; // 限流

    public function name(): string
    {
        return match ($this) {
            self::SUCCESS => 'success',
            self::FAILED => 'failed',
            self::APP_KEY_LOST => 'app key 丢失',
            self::SIGNATURE_LOST => 'signature 丢失',
            self::INVALID_APP_KEY => '无效 app key',
            self::INVALID_SIGNATURE => '无效签名',
            self::INVALID_TIMESTAMP => '无效 timestamp',
            self::Balance_NOT_ENOUGH => '余额不足',
            self::RATE_LIMIT => '请求过于频繁'
        };
    }

    // 自定义 code
    /**
     * @param mixed $value
     * @return bool
     */
    public function equal(mixed $value): bool
    {
        return $this->value == $value;
    }
}