diff --git a/docs/zh/backend/security/passport.md b/docs/zh/backend/security/passport.md index 93e5fba..78f8b4a 100644 --- a/docs/zh/backend/security/passport.md +++ b/docs/zh/backend/security/passport.md @@ -2,9 +2,419 @@ ::: tip -MineAdmin 默认实现了一套完整的 `JWT` 认证刷新方案。 -本文将详细介绍默认的 jwt 登录授权机制。以便您能快速进行接口开发 +MineAdmin 的认证流程由 [mineadmin/auth-jwt](https://github.com/mineadmin/JwtAuth) 组件加 [mineadmin/jwt](https://github.com/mineadmin/jwt) 组件接入 [lcobucci/jwt](https://github.com/lcobucci/jwt) +构建而成,本文将着重讲解如何在 MineAdmin 中使用 jwt 进行用户认证 ::: -TODO \ No newline at end of file +## 在控制器中快速获取当前用户 + +::: danger + +不建议在控制器以外注入此对象。对于 service 中操作 user、应将 user 实例传入到 service 方法中 +从而保证获取用户是在 http 请求周期内 + +::: + +使用 `App\Http\CurrentUser` 快速获取当前请求的用户对象 + +::: code-group + +```php{2,5,8} [TestController] + +#[Middleware(AccessTokenMiddleware::class)] +class TestController { + + public function __construct(private readonly CurrentUser $currentUser){}; + + public function test(){ + return $this->success('CurrentUser: '. $this->currentUser->user()->username); + } + + + +} +``` + +```php [CurrentUser] +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 + */ + public function menus(): Collection + { + // @phpstan-ignore-next-line + return $this->user()->getMenus(); + } + + /** + * 用于获取当前用户的角色列表 [ [code=>'xxx',name=>'xxxx'] ] + * @return Collection + */ + 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(); + } +} + +``` + +::: + +::: code-group + +## 为外部程序创建单独的 jwt 生成规则 + +在日常的应用开发中。业务后台与前台应用通常使用两个不同的生成规则。在 MineAdmin 中需要此项参考本章节内容 + +1. env 文件中新建一个 JWT_API_SECRET 。值为随机字符串 base64 编码后的内容 +2. 在 `config/autoload/jwt.php` 中新建一个场景 +3. 新建一个 `ApiTokenMiddleware` 中间件专门用来验证新的场景 jwt +4. 在你的前台控制器中使用 `ApiTokenMiddleware` 中间件进行用户验证 +5. 在 `PassportService` 新增一个 `loginApi` 方法 + +::: code-group + +```php[.env] +#other ... + +MINE_API_SECERT=azOVxsOWt3r0ozZNz8Ss429ht0T8z6OpeIJAIwNp6X0xqrbEY2epfIWyxtC1qSNM8eD6/LQ/SahcQi2ByXa/2A== + +``` + +```php{46-50} [jwt.php] +// config/autoload/jwt.php + [ + // 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{20-24} [ApiTokenMiddleware] +jwtFactory->get('api'); + } +} + + +``` + +```php{36-81} [TestController] +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 + ) + ); + } + +``` + +```php{48-70} [PassportService] +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 + */ + 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 + */ + 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 刷新机制](../base/lifecycle.md#双-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` \ No newline at end of file