主题
应用端
目前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.php
的 withExceptions
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 😄