feat : jwt

This commit is contained in:
2025-09-12 18:12:30 +08:00
parent a80c237bbb
commit ff3e0105ec
14 changed files with 362 additions and 38 deletions

View File

@@ -19,4 +19,10 @@ class ResultCode extends AbstractConstants
#[Message("failed")]
final public const int ERROR = 1;
#[Message("token已过期")]
final public const int JWT_EXPIRED = 10001;
#[Message("token错误")]
final public const int JWT_ERROR = 10002;
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Annotation\ResponseFormat;
use App\Middleware\Token\AdminTokenMiddleware;
use App\Service\Admin\AdminUser\UserService;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\Validation\Annotation\Scene;
#[Controller(prefix: "admin/adminUser")]
#[ResponseFormat('admin')]
#[Middleware(AdminTokenMiddleware::class)]
class AdminUserController
{
#[RequestMapping(path: "getInfo", methods: "GET")]
public function getInfo()
{
return (new UserService)->handle();
}
#[RequestMapping(path: "refresh", methods: "POST")]
public function refresh()
{
return (new UserService)->refresh();
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Exception\Handler;
use App\Constants\ResultCode;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Http\Message\ResponseInterface;
use Throwable;
use Lcobucci\JWT\Exception as JWTException;
class JwtExceptionHandler extends BaseErrExceptionHandler
{
/**
* @param Throwable $throwable
* @param ResponseInterface $response
* @return ResponseInterface
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
{
if ($throwable instanceof JWTException) {
$throwable = $this->modifyException($throwable);
// 传递给基类处理
return $this->handlerResponse($throwable, $response);
}
return $response;
}
protected function modifyException(Throwable $e): Throwable
{
// 根据不同的异常类型设置不同的code和message
switch ($e->getMessage()) {
case 'The token is expired':
$e->code = ResultCode::JWT_EXPIRED;
$e->message = 'token已过期';
break;
default:
$e->code = ResultCode::JWT_ERROR;
$e->message = 'token错误';
}
return $e;
}
/**
* @param Throwable $throwable
* @return bool
*/
public function isValid(Throwable $throwable): bool
{
return $throwable instanceof JWTException;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Interface;
use Lcobucci\JWT\UnencryptedToken;
interface CheckTokenInterface
{
public function checkJwt(UnencryptedToken $token): void;
}

View File

@@ -11,6 +11,7 @@ use Lcobucci\JWT\Builder;
use Lcobucci\JWT\JwtFacade;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Token\RegisteredClaims;
use Lcobucci\JWT\UnencryptedToken;
use Lcobucci\JWT\Validation\Constraint;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
@@ -45,7 +46,8 @@ abstract class AbstractJwt implements JwtInterface
$this->getSigner(),
$this->getSigningKey(),
function (Builder $builder, \DateTimeImmutable $immutable) use ($sub, $callable) {
$builder = $builder->identifiedBy($sub);
$builder = $builder->identifiedBy($sub)
->issuedBy($this->getConfig('claims.'.RegisteredClaims::ISSUER,''));
if ($callable !== null) {
$builder = $callable($builder);
}
@@ -65,8 +67,10 @@ abstract class AbstractJwt implements JwtInterface
$this->getSigner(),
$this->getSigningKey(),
function (Builder $builder, \DateTimeImmutable $immutable) use ($sub, $callable) {
$builder = $builder->identifiedBy($sub);
$builder = $builder->expiresAt($this->getRefreshExpireAt($immutable));
$builder = $builder->identifiedBy($sub)
->issuedBy($this->getConfig('claims.'.RegisteredClaims::ISSUER,''))
->expiresAt($this->getRefreshExpireAt($immutable));
if ($callable !== null) {
$builder = $callable($builder);
}
@@ -81,6 +85,7 @@ abstract class AbstractJwt implements JwtInterface
*/
public function parserAccessToken(string $accessToken): UnencryptedToken
{
echo 1;
return $this->getJwtFacade()
->parse(
$accessToken,

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Lib\Jwt;
use Hyperf\Context\RequestContext;
use Lcobucci\JWT\UnencryptedToken;
trait RequestScopedTokenTrait
{
public function getToken(): ?UnencryptedToken
{
return RequestContext::get()->getAttribute('token');
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Middleware\Token;
use App\Interface\CheckTokenInterface;
use App\Interface\JwtInterface;
use App\Lib\Jwt\JwtFactory;
use Hyperf\Collection\Arr;
use Hyperf\Stringable\Str;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\UnencryptedToken;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Swow\Psr7\Message\ServerRequestPlusInterface;
use function Hyperf\Support\value;
abstract class AbstractTokenMiddleware
{
public function __construct(
protected ContainerInterface $container,
protected readonly JwtFactory $jwtFactory,
protected readonly CheckTokenInterface $checkToken,
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$token = $this->parserToken($request);
$this->checkToken->checkJwt($token);
$this->checkIssuer($token);
return $handler->handle(
value(
static function (ServerRequestPlusInterface $request, UnencryptedToken $token) {
return $request->setAttribute('token', $token);
},
$request,
$token
)
);
}
/**
* @param UnencryptedToken $token
* @return void
*/
abstract public function checkIssuer(UnencryptedToken $token): void;
abstract public function getJwt(): JwtInterface;
/**
* @param ServerRequestInterface $request
* @return Token
*/
protected function parserToken(ServerRequestInterface $request): Token
{
return $this->getJwt()->parserAccessToken($this->getToken($request));
}
/**
* @param ServerRequestInterface $request
* @return string
*/
protected function getToken(ServerRequestInterface $request): string
{
if ($request->hasHeader('Authorization')) {
return Str::replace('Bearer ', '', $request->getHeaderLine('Authorization'));
}
if ($request->hasHeader('token')) {
return $request->getHeaderLine('token');
}
if (Arr::has($request->getQueryParams(), 'token')) {
return $request->getQueryParams()['token'];
}
return '';
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Middleware\Token;
use App\Exception\ErrException;
use App\Interface\JwtInterface;
use Lcobucci\JWT\Token\RegisteredClaims;
use Lcobucci\JWT\UnencryptedToken;
use function Hyperf\Support\env;
final class AdminTokenMiddleware extends AbstractTokenMiddleware
{
public function getJwt(): JwtInterface
{
return $this->jwtFactory->get('admin');
}
/**
* @param UnencryptedToken $token
* @return void
*/
public function checkIssuer(UnencryptedToken $token): void
{
$audience = $token->claims()->get(RegisteredClaims::ISSUER);
if ($audience === env('APP_NAME') .'_admin') throw new ErrException('token错误');
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Admin\AdminUser;
use App\Lib\Jwt\RequestScopedTokenTrait;
use App\Service\Admin\BaseAdminService;
use Lcobucci\JWT\Token\RegisteredClaims;
class UserService extends BaseAdminService
{
use RequestScopedTokenTrait;
public function handle(): array
{
var_dump($this->getToken()->claims()->all());
var_dump($this->getToken()->claims()->get(RegisteredClaims::ID));
var_dump($this->getToken()->claims()->get(RegisteredClaims::AUDIENCE));
return $this->adminReturn->success();
}
public function refresh(): array
{
return $this->adminReturn->success();
}
}

View File

@@ -16,6 +16,7 @@ use App\Interface\JwtInterface;
use App\Lib\Jwt\JwtFactory;
use App\Repository\AdminUserRepository;
use App\Service\Admin\BaseAdminService;
use App\Service\BaseTokenService;
use Hyperf\Di\Annotation\Inject;
class LoginService extends BaseAdminService
@@ -32,10 +33,10 @@ class LoginService extends BaseAdminService
protected AdminUserRepository $userRepository;
/**
* @var JwtFactory
* @var BaseTokenService
*/
#[Inject]
protected JwtFactory $jwtFactory;
protected BaseTokenService $tokenService;
/**
* @return array
@@ -53,20 +54,12 @@ class LoginService extends BaseAdminService
if ($adminInfo->status == AdminUserStatusCode::DISABLE) throw new ErrException('用户已禁用');
$jwtHandle = $this->getJwt();
$jwtHandle = $this->tokenService->getJwt();
return [
return $this->adminReturn->success('success',[
'access_token' => $jwtHandle->builderAccessToken((string) $adminInfo->id)->toString(),
'refresh_token' => $jwtHandle->builderRefreshToken((string) $adminInfo->id)->toString(),
'expire_at' => (int) $jwtHandle->getConfig('ttl', 0),
];
}
/**
* @return JwtInterface
*/
private function getJwt(): JwtInterface
{
return $this->jwtFactory->get($this->jwt);
]);
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service;
use App\Exception\ErrException;
use App\Interface\CheckTokenInterface;
use App\Lib\Jwt\JwtFactory;
use Hyperf\Di\Annotation\Inject;
use Lcobucci\JWT\UnencryptedToken;
use App\Interface\JwtInterface;
final class BaseTokenService implements CheckTokenInterface
{
private string $jwt = 'admin';
/**
* @var JwtFactory
*/
#[Inject]
protected JwtFactory $jwtFactory;
/**
* @return JwtInterface
*/
public function getJwt(): JwtInterface
{
return $this->jwtFactory->get($this->jwt);
}
/**
* @return JwtInterface
*/
public function getApiJwt(): JwtInterface
{
return $this->jwtFactory->get('api');
}
/**
* @param UnencryptedToken $token
* @return void
*/
public function checkJwt(UnencryptedToken $token): void
{
$this->getJwt()->hasBlackList($token) && throw new ErrException('token已过期');
}
}

View File

@@ -11,4 +11,5 @@ declare(strict_types=1);
*/
return [
Hyperf\Database\Schema\Blueprint::class => App\Common\Macros\BlueprintMacros::class,
App\Interface\CheckTokenInterface::class => App\Service\BaseTokenService::class,
];

View File

@@ -31,34 +31,20 @@ return [
],
'claims' => [
// 默认的jwt claims
RegisteredClaims::ISSUER => (string) env('APP_NAME'),
RegisteredClaims::ISSUER => (string) env('APP_NAME') . '_default',
RegisteredClaims::AUDIENCE => 'default', // 明确标识受众
],
],
'admin' => [
// 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('ADMIN_JWT_TTL', 3600),
'ttl' => (int) env('ADMIN_JWT_TTL', 1),
// 刷新token过期时间单位为秒
'refresh_ttl' => (int) env('ADMIN_JWT_REFRESH_TTL', 7200),
// 黑名单模式
'blacklist' => [
// 是否开启黑名单
'enable' => true,
// 黑名单缓存前缀
'prefix' => 'jwt_blacklist',
// 黑名单缓存驱动
'connection' => 'default',
// 黑名单缓存时间 该时间一定要设置比token过期时间要大一点最好设置跟过期时间一样
'ttl' => (int) env('ADMIN_JWT_BLACKLIST_TTL', 7201),
],
'refresh_ttl' => (int) env('ADMIN_JWT_REFRESH_TTL', 10),
'claims' => [
// 默认的jwt claims
RegisteredClaims::ISSUER => 'ADMIN'. env('APP_NAME'),
RegisteredClaims::ISSUER => (string) env('APP_NAME') .'_admin',
],
],
// 在你想要使用不同的场景时,可以在这里添加配置.可以填一个。其他会使用默认配置

View File

@@ -4,7 +4,30 @@ Content-Type: application/x-www-form-urlencoded
username=admin&password=admin
#> {%
# client.global.set("admin_token", response.body.data.token);
#%}
> {%
client.global.set("admin_token", response.body.data.access_token);
client.global.set("refresh_token", response.body.data.refresh_token);
%}
### 登录
GET {{host}}/admin/adminUser/getInfo
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {{admin_token}}
### 登录
POST {{host}}/admin/passport/login
Content-Type: application/x-www-form-urlencoded
username=admin&password=123456
> {%
client.global.set("admin_token", response.body.data.access_token);
%}
### 登录
GET {{host}}/admin/passport/getInfo
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {{admin_token}}