用户认证
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