用户認證
TIP
MineAdmin 的認證流程由 mineadmin/auth-jwt 組件加 mineadmin/jwt 組件接入 lcobucci/jwt 構建而成,本文將着重講解如何在 MineAdmin 中使用 jwt 進行用户認證
在控制器中快速獲取當前用户
DANGER
不建議在控制器以外注入此對象。對於 service 中操作 user、應將 user 實例傳入到 service 方法中 從而保證獲取用户是在 http 請求週期內
使用 App\Http\CurrentUser
快速獲取當前請求的用户對象
#[Middleware(AccessTokenMiddleware::class)]
class TestController {
public function __construct(private readonly CurrentUser $currentUser){};
public function test(){
return $this->success('CurrentUser: '. $this->currentUser->user()->username);
}
}
<?php
declare(strict_types=1);
/**
* This file is part of MineAdmin.
*
* @link https://www.mineadmin.com
* @document https://doc.mineadmin.com
* @contact root@imoi.cn
* @license https://github.com/mineadmin/MineAdmin/blob/master/LICENSE
*/
namespace App\Http;
use App\Model\Enums\User\Type;
use App\Model\Permission\Menu;
use App\Model\Permission\Role;
use App\Model\Permission\User;
use App\Service\PassportService;
use App\Service\Permission\UserService;
use Hyperf\Collection\Collection;
use Lcobucci\JWT\Token\RegisteredClaims;
use Mine\Jwt\Traits\RequestScopedTokenTrait;
final class CurrentUser
{
use RequestScopedTokenTrait;
public function __construct(
private readonly PassportService $service,
private readonly UserService $userService
) {}
// 獲取當前用户 model 實例
public function user(): ?User
{
return $this->userService->getInfo($this->id());
}
// 刷新當前用户的 token、返回 [access_token=>'xxx',refresh_token=>'xxx']
public function refresh(): array
{
return $this->service->refreshToken($this->getToken());
}
// 快速獲取當前用户 id (不走 db 查詢)
public function id(): int
{
return (int) $this->getToken()->claims()->get(RegisteredClaims::ID);
}
/**
* 用於獲取當前用户的 菜單樹狀列表
* @return Collection<int,Menu>
*/
public function menus(): Collection
{
// @phpstan-ignore-next-line
return $this->user()->getMenus();
}
/**
* 用於獲取當前用户的角色列表 [ [code=>'xxx',name=>'xxxx'] ]
* @return Collection<int, Role>
*/
public function roles(): Collection
{
// @phpstan-ignore-next-line
return $this->user()->getRoles()->map(static fn (Role $role) => $role->only(['name', 'code', 'remark']));
}
// 判斷當前用户的 user_type 是否為 system 類別
public function isSystem(): bool
{
return $this->user()->user_type === Type::SYSTEM;
}
// 判斷當前用户是否具有超管權限
public function isSuperAdmin(): bool
{
return $this->user()->isSuperAdmin();
}
}
為外部程序創建單獨的 jwt 生成規則
在日常的應用開發中。業務後台與前台應用通常使用兩個不同的生成規則。在 MineAdmin 中需要此項參考本章節內容
- env 文件中新建一個 JWT_API_SECRET 。值為隨機字符串 base64 編碼後的內容
- 在
config/autoload/jwt.php
中新建一個場景 - 新建一個
ApiTokenMiddleware
中間件專門用來驗證新的場景 jwt - 在你的前台控制器中使用
ApiTokenMiddleware
中間件進行用户驗證 - 在
PassportService
新增一個loginApi
方法
#other ...
MINE_API_SECERT=azOVxsOWt3r0ozZNz8Ss429ht0T8z6OpeIJAIwNp6X0xqrbEY2epfIWyxtC1qSNM8eD6/LQ/SahcQi2ByXa/2A==
// config/autoload/jwt.php
<?php
declare(strict_types=1);
/**
* This file is part of MineAdmin.
*
* @link https://www.mineadmin.com
* @document https://doc.mineadmin.com
* @contact root@imoi.cn
* @license https://github.com/mineadmin/MineAdmin/blob/master/LICENSE
*/
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Token\RegisteredClaims;
use Mine\Jwt\Jwt;
return [
'default' => [
// jwt 配置 https://lcobucci-jwt.readthedocs.io/en/latest/
'driver' => Jwt::class,
// jwt 簽名key
'key' => InMemory::base64Encoded(env('JWT_SECRET')),
// jwt 簽名算法 可選 https://lcobucci-jwt.readthedocs.io/en/latest/supported-algorithms/
'alg' => new Sha256(),
// token過期時間,單位為秒
'ttl' => (int) env('JWT_TTL', 3600),
// 刷新token過期時間,單位為秒
'refresh_ttl' => (int) env('JWT_REFRESH_TTL', 7200),
// 黑名單模式
'blacklist' => [
// 是否開啓黑名單
'enable' => true,
// 黑名單緩存前綴
'prefix' => 'jwt_blacklist',
// 黑名單緩存驅動
'connection' => 'default',
// 黑名單緩存時間 該時間一定要設置比token過期時間要大一點,最好設置跟過期時間一樣
'ttl' => (int) env('JWT_BLACKLIST_TTL', 7201),
],
'claims' => [
// 默認的jwt claims
RegisteredClaims::ISSUER => (string) env('APP_NAME'),
],
],
// 在你想要使用不同的場景時,可以在這裏添加配置.可以填一個。其他會使用默認配置
'api' => [
'key' => InMemory::base64Encoded(env('JWT_API_SECRET')),
],
];
<?php
declare(strict_types=1);
/**
* This file is part of MineAdmin.
*
* @link https://www.mineadmin.com
* @document https://doc.mineadmin.com
* @contact root@imoi.cn
* @license https://github.com/mineadmin/MineAdmin/blob/master/LICENSE
*/
namespace App\Http\Api\Middleware;
use Mine\Jwt\JwtInterface;
use Mine\JwtAuth\Middleware\AbstractTokenMiddleware;
final class ApiTokenMiddleware extends AbstractTokenMiddleware
{
public function getJwt(): JwtInterface
{
// 指定場景為 上一步新建的場景名稱
return $this->jwtFactory->get('api');
}
}
<?php
declare(strict_types=1);
/**
* This file is part of MineAdmin.
*
* @link https://www.mineadmin.com
* @document https://doc.mineadmin.com
* @contact root@imoi.cn
* @license https://github.com/mineadmin/MineAdmin/blob/master/LICENSE
*/
namespace App\Http\Admin\Controller;
use App\Http\Admin\Request\Passport\LoginRequest;
use App\Http\Admin\Vo\PassportLoginVo;
use App\Http\Common\Controller\AbstractController;
use App\Http\Common\Middleware\AccessTokenMiddleware;
use App\Http\Common\Middleware\RefreshTokenMiddleware;
use App\Http\Common\Result;
use App\Http\CurrentUser;
use App\Model\Enums\User\Type;
use App\Schema\UserSchema;
use App\Service\PassportService;
use Hyperf\Collection\Arr;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Swagger\Annotation as OA;
use Hyperf\Swagger\Annotation\Post;
use Mine\Jwt\Traits\RequestScopedTokenTrait;
use Mine\Swagger\Attributes\ResultResponse;
#[OA\HyperfServer(name: 'http')]
final class PassportController extends AbstractController
{
use RequestScopedTokenTrait;
public function __construct(
private readonly PassportService $passportService,
private readonly CurrentUser $currentUser
) {}
#[Post(
path: '/admin/api/login',
operationId: 'ApiLogin',
summary: '系統登錄',
tags: ['api:passport']
)]
#[ResultResponse(
instance: new Result(data: new PassportLoginVo()),
title: '登錄成功',
description: '登錄成功返回對象',
example: '{"code":200,"message":"成功","data":{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MjIwOTQwNTYsIm5iZiI6MTcyMjA5NDAiwiZXhwIjoxNzIyMDk0MzU2fQ.7EKiNHb_ZeLJ1NArDpmK6sdlP7NsDecsTKLSZn_3D7k","refresh_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MjIwOTQwNTYsIm5iZiI6MTcyMjA5NDAiwiZXhwIjoxNzIyMDk0MzU2fQ.7EKiNHb_ZeLJ1NArDpmK6sdlP7NsDecsTKLSZn_3D7k","expire_at":300}}'
)]
#[OA\RequestBody(content: new OA\JsonContent(
ref: LoginRequest::class,
title: '登錄請求參數',
required: ['username', 'password'],
example: '{"username":"admin","password":"123456"}'
))]
public function loginApi(LoginRequest $request): Result
{
$username = (string) $request->input('username');
$password = (string) $request->input('password');
$ip = Arr::first(array: $request->getClientIps(), callback: static fn ($val) => $val ?: null, default: '0.0.0.0');
$browser = $request->header('User-Agent') ?: 'unknown';
// todo 用户系統的獲取
$os = $request->header('User-Agent') ?: 'unknown';
return $this->success(
$this->passportService->loginApi(
$username,
$password,
Type::User,
$ip,
$browser,
$os
)
);
}
namespace App\Service;
use App\Exception\BusinessException;
use App\Exception\JwtInBlackException;
use App\Http\Common\ResultCode;
use App\Model\Enums\User\Type;
use App\Repository\Permission\UserRepository;
use Lcobucci\JWT\Token\RegisteredClaims;
use Lcobucci\JWT\UnencryptedToken;
use Mine\Jwt\Factory;
use Mine\Jwt\JwtInterface;
use Mine\JwtAuth\Event\UserLoginEvent;
use Mine\JwtAuth\Interfaces\CheckTokenInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
final class PassportService extends IService implements CheckTokenInterface
{
/**
* @var string jwt場景
*/
private string $jwt = 'default';
public function __construct(
protected readonly UserRepository $repository,
protected readonly Factory $jwtFactory,
protected readonly EventDispatcherInterface $dispatcher
) {}
/**
* @return array<string,int|string>
*/
public function login(string $username, string $password, Type $userType = Type::SYSTEM, string $ip = '0.0.0.0', string $browser = 'unknown', string $os = 'unknown'): array
{
$user = $this->repository->findByUnameType($username, $userType);
if (! $user->verifyPassword($password)) {
$this->dispatcher->dispatch(new UserLoginEvent($user, $ip, $os, $browser, false));
throw new BusinessException(ResultCode::UNPROCESSABLE_ENTITY, trans('auth.password_error'));
}
$this->dispatcher->dispatch(new UserLoginEvent($user, $ip, $os, $browser));
$jwt = $this->getJwt();
return [
'access_token' => $jwt->builderAccessToken((string) $user->id)->toString(),
'refresh_token' => $jwt->builderRefreshToken((string) $user->id)->toString(),
'expire_at' => (int) $jwt->getConfig('ttl', 0),
];
}
/**
* @return array<string,int|string>
*/
public function loginApi(string $username, string $password, Type $userType = Type::SYSTEM, string $ip = '0.0.0.0', string $browser = 'unknown', string $os = 'unknown'): array
{
$user = $this->repository->findByUnameType($username, $userType);
if (! $user->verifyPassword($password)) {
$this->dispatcher->dispatch(new UserLoginEvent($user, $ip, $os, $browser, false));
throw new BusinessException(ResultCode::UNPROCESSABLE_ENTITY, trans('auth.password_error'));
}
$this->dispatcher->dispatch(new UserLoginEvent($user, $ip, $os, $browser));
$jwt = $this->getApiJwt();
return [
'access_token' => $jwt->builderAccessToken((string) $user->id)->toString(),
'refresh_token' => $jwt->builderRefreshToken((string) $user->id)->toString(),
'expire_at' => (int) $jwt->getConfig('ttl', 0),
];
}
public function getApiJwt(): JwtInterface{
// 填寫上一步的場景值
return $this->jwtFactory->get('api');
}
public function getJwt(): JwtInterface
{
return $this->jwtFactory->get($this->jwt);
}
jwt
TIP
查看本文檔前,需要對 jwt 的知識有一定了解。本文不再另行解釋相關基礎知識
雙 token 的區別
在 MineAdmin 中、登錄成功後會返回兩個 token。即 access_token
和 refresh_token
前者 access_token
用來作業務用户認證。後者 refresh_token
用來做無感刷新 access_token
。具體刷新流程可查看 雙 token 刷新機制
refresh_token
相比較 access_token
多了一個 sub
屬性。值為 refresh
作用標明該 token 只能用於刷新 access_token 同時該 token 只能刷新一次即失效。下次刷新必須選擇新的 refresh_token
前後者的 id
屬性則都是存儲用户的 id
access_token 的驗證由 App\Http\Common\Middleware\AccessTokenMiddleware
中間件決定 refresh_token 的驗證由 App\Http\Common\Middleware\RefreshTokenMiddleware
中間件決定
而這兩個都是繼承於 Mine\JwtAuth\Middleware\AbstractTokenMiddleware