Skip to content

应用端

目前CatchAdmin 专业版已经拥有了一个管理端,但是缺少一个可以快速使用的应用端。在 3.2.0 版本开始,CatchAdmin 开始提供微应用端,也就是 app 应用端,主要用来对接 api 接口应用。包含以下功能

  • 基于 JWT 的用户认证
  • 统一的枚举管理
  • 统一的响应处理
  • 统一异常渲染处理
  • 身份认证中间件

为了开发者可以更快使用这套规范的 app 应用架构,我会在下面一一说明其使用步骤,所以在开发前,一定要看完

INFO

app 应用的路由前缀 api/app,可以通过 php artisan route:list | grep api/app 命令查看

用户认证

专业版 app 应用的用户认证是基于 JWT 来进行身份认证,这个时候有人要问了,为什么后台使用 sanctum。前台却是使用 JWT 呢。主要有两点考虑

  • santum 一次身份正常需要查询两次,JWT 只有一次
  • JWT 的 Claim 可以承载更多的信息,无需回表。sanctum 需要从数据库查询
  • sanctum 只适合单体应用,JWT 兼容单体,并支持分布式架构

基于以上 1,2 两点,一旦用户量变多,sanctum 的开销会更大。所以出于考量,app 应用端使用 JWT 来做身份认证。

安装

3.2.0 版本开始,CatchAdmin 可以开箱即用 JWT,但是如果你的版本低于它,这需要你自己安装。首先安装扩展

WARNING

一定要确认使用的版本是否是 < 3.2.0, 小于就可以使用下面的步骤,否则不需要

shell
composer require "tymon/jwt-auth"

然后发布 jwt 配置文件

shell
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

再生成 jwt 密钥

shell
php artisan jwt:secret

JWT 配置请查看配置文件 config/jwt.php

路由

app 应用目前只提供了登录登出两条路由,路由文件routes/api.php 查看

php
Route::prefix('app')->group(function (){
    // 登录
    Route::post('login', [AuthController::class, 'login']);
    // 登出
    Route::post('logout', [AuthController::class, 'logout'])->middleware('auth:app');
});

auth 配置

找到 config/auth.php, 看一下对应的 auth 配置,这里只看 app 的认证配置

php
return [
    'guards' => [
        // 前台 app 接口认证
        'app' => [
            'driver' => 'jwt',
            'provider' => 'app_users',
        ]
    ],


    'providers' => [
        // 前台用户模型
        'app_users' => [
            'driver' => 'eloquent',
            'model' => \Modules\Member\Models\Members::class // 使用会员表
        ],
    ],
];

认证代码

目前支持 密码, 小程序微信登录小程序手机号快捷登录 三种登录方式。具体实现如下,找到app/Http/Controllers/AuthController.php,使用 Laravel 自带的 Auth 门面进行身份认证,会非常简单,内容如下

php
use App\Services\Auth\UserService;

class AuthController extends Controller
{
   /**
     * 登录
     *
     * @param Request $request
     * @param UserService $service
     * @return JsonResponse
     */
    public function login(Request $request, UserService $service): JsonResponse
    {
        // 系统封装了 UserService 用于处理用户登录
        $user = $service->setAdapterType($request->get('type'))->auth($request->all());

        if (! $user) {
            throw new UnauthorizedAccessException();
        }

        return $this->success($user);
    }

    /**
     * @return JsonResponse
     */
    public function logout(): JsonResponse
    {
        // 退出之后将 token 加入黑名单
        $this->appGuard()->logout(true);

        return ApiResponse::success();
    }
}

具体登录的代码, 请查看 app/Services/Auth/UserService.php 文件

php
 /* @var Login $loginAdapter */
$loginAdapter = app($this->getLoginAdapter($this->type));

if ($res = $loginAdapter->auth($params)) {
    [$user, $token] = $res;

    $user->rememberToken($token);

    return $user->makeHidden([
        'password', 'from', 'creator_id'
    ]);
}

return false;

对应的三种登录方式代码

php
//  app/Services/Auth/PasswordLogin.php

$field = preg_match('/^1[0123456789]\d{10}$/', $params['account']) ? 'mobile' : 'username';

// Auth 认证
$token = Auth::guard('app')->attempt([
    $field => $params['account'],
    'password' => $params['password'],
]);

if ($token) {
    return [Auth::guard('app')->user(), $token];
}

return false;
php
// app/Services/Auth/WechatLogin.php
try {
    $response = $this->getMiniAppApplication()->getUtils()->codeToSession($params['code']);

    $user = $this->getAuthModel()->firstOrCreate([
        'miniapp_openid' => $response['openid'],
    ],[
        'username' => $params['username'],
        'from' => 'miniprogram',
        'miniapp_openid' => $response['openid'],
        'avatar' => $this->storeAvatar(),
        'mobile' => '',
        'created_at' => time(),
        'last_login_at' => time(),
        'updated_at' => time(),
    ]);

    if (! $user) {
        return false;
    }

    return [$user, JWTAuth::fromUser($user)];
} catch (\Throwable $e) {
    throw new UnauthorizedAccessException();
}
php
// app/Services/Auth/WechatByMobileLogin.php
try {
    $openid = $this->getMiniAppOpenId($params['code']);
    $user = $this->getAuthModel()->firstOrCreate([
        'mobile' => $this->getMiniAppUserMobile($params['phoneCode']),
    ],[
        'username' => $params['username'],
        'from' => 'miniprogram',
        'avatar' => $this->storeAvatar(),
        'miniapp_openid' => $openid,
        'created_at' => time(),
        'last_login_at' => time(),
        'updated_at' => time(),
    ]);

    if (! $user) {
        return false;
    }

    return [$user, JWTAuth::fromUser($user)];
} catch (\Throwable $e) {
    throw new UnauthorizedAccessException();
}

认证中间件

找到认证中间件app/Http/Middleware/AuthMiddleware.php

php
class AuthMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next, string $guard): Response
    {
        if (! $request->bearerToken()) {
            throw new TokenMissedException();
        }

        $user = Auth::guard($guard)->user();

        if (! $user) {
            throw new AuthenticationException();
        }

        return $next($request);
    }
}

中间件别名

为了使用上更简单一点,这里对 认证中间件 使用别名处理。找到 bootstrap/app.php 文件

php
// 找到 withMiddleware 之后,设置别名
->withMiddleware(function (Middleware $middleware) {
    //
    $middleware->alias([
        'auth' => AuthMiddleware::class // 注册 app 应用中间件别名,支持注入 guard,例如 auth:app, auth:web 等等
    ]);
})

认证中间件支持任何支持 Auth 门面的认证,只需要传入对应的门面名称$guard 即可。例如在 app 应用里的需要认证的路由是这样的

php
// auth:app,这样就是使用 `app` guard 认证
Route::post('logout', [AuthController::class, 'logout'])->middleware('auth:app');

枚举

app 应用所有的枚举都放在 App\Enums 目录中,目前有两个内置的

  • Enum 枚举接口
  • Code 项目自带的响应码枚举

WARNING

规定所有的枚举类都必须实现 Enum 接口

下面是 Code 枚举的具体实现,对于响应的数据的 code 可以根据实际需求重新修改

php
/**
 * 枚举项目的返回码
 */
enum Code:int implements Enum
{
    case SUCCESS = 10000;
    case FAILED = 10001;
    case LOGIN_FAILED = 10002;
    case AUTH_EXCEPTION = 10003;
    case TOKEN_EXPIRED = 10004;
    case TOKEN_INVALID = 10005;
    case TOKEN_BLACKLIST = 10006;
    case TOKEN_MISSED = 10007;

    /**
     * @return string
     */
    public function message(): string
    {
        return match ($this) {
            self::SUCCESS => 'success',
            self::FAILED => 'failed',
            self::LOGIN_FAILED => '登录失败',
            self::AUTH_EXCEPTION => '认证失败',
            self::TOKEN_EXPIRED => 'token 过期',
            self::TOKEN_INVALID => 'token 无效',
            self::TOKEN_BLACKLIST => 'token 黑名单',
            self::TOKEN_MISSED => 'token 丢失',
        };
    }


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

响应

app 应用提供了一个易于使用的响应类,如下,具体响应的数据结构根据实际需求更改

php
class ApiResponse
{
    /**
     * 成功响应
     */
    public static function success(mixed $data = [], string $message = 'success', Code $code = Code::SUCCESS): JsonResponse
    {
        return response()->json([
            'code' => $code->value,
            'message' => $message,
            'data' => $data,
        ]);
    }

    /**
     * 错误响应
     */
    public static function error(string $message = 'api error', int|Code $code = Code::FAILED): JsonResponse
    {
        return response()->json([
            'code' => $code instanceof Enum ? $code->value : $code,
            'message' => $message,
        ]);
    }

    /**
     * 分页响应
     */
    public static function paginate(LengthAwarePaginator $paginator, string $message = 'success', Code $code = Code::SUCCESS): JsonResponse
    {
        return response()->json([
            'code' => $code->value,
            'message' => $message,
            'data' => $paginator->items(),
            'total' => $paginator->total(),
            'limit' => $paginator->perPage(),
            'page' => $paginator->currentPage(),
        ]);
    }
}

异常

app 应用在 App\Exceptions 提供一组异常。非常易于使用和维护。目前提供的异常有

  • AuthenticationException 身份认证异常
  • FailedException 通用的失败异常
  • TokenMissedException token 修饰异常
  • UnauthorizedAccessException 登录授权异常

为了能统一处理 app 应用的业务异常,所以的自定义异常都必须继承 ApiAppException,内容如下

php
abstract class ApiAppException extends HttpException
{
    //
    public function __construct(
        string $message = '',
        Code $code = Code::FAILED
    ) {
        // 异常所有的 code 都使用枚举值,那么只需要维护 Code 枚举类就可以了
        if ($this->code instanceof Enum) {
            $code = $this->code;

            $this->message = $this->code->message();
        }

        parent::__construct(
            $this->statusCode(),
            $message ?: $this->message,
            null,
            [],
            $code->value
        );
    }

    /**
     * @return int
     */
    protected function statusCode(): int
    {
        return 500;
    }
}

例如 AuthenticationException 异常,只需要这么定义就可以了

php
use App\Enums\Code;

class AuthenticationException extends ApiAppException
{
    protected $code = Code::AUTH_EXCEPTION;
}

这样异常的 code 和 异常出错的信息,都交给枚举类来维护。是不是很 very good?

统一异常处理

当然光有异常肯定是不行的,我们还需要将异常信息返回给客户端,并且是 json 响应。找到 bootstrap/app.phpwithExceptions

php
 ->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (Throwable $exception, Request $request) {
        // 渲染 app 异常,返回错误信息
        if ($exception instanceof ApiAppException) {
            return ApiResponse::error($exception->getMessage(), $exception->getCode());
        }
        // 其他系统异常自行处理, 请根据项目实际情况进行处理
        // 如果路由前缀使用 api/app 则返回 api app 异常
        if ($request->route()->prefix('api/app')) {
            return ApiResponse::error($exception->getMessage(), $exception->getCode());
        }
    });
})

路由

app 应用也将需要身份认证和不需要的也组织好了,你只需要找到 routes/api.php,按照下面的内容填充就可以了

php
Route::prefix('app')->group(function (){
    // 不需要身份认证的路由
    Route::post('login', [AuthController::class, 'login']);

    // 需要身份认证的路由
    Route::middleware('auth:app')->group(function () {
        Route::post('logout', [AuthController::class, 'logout']);
    });
});

好了,现在你可以根据现在专业版提供的 app 应用基础快速开发你自己的应用了。enjoy it 😄