From ff3e0105ec4db22f5e5847f61c00d24af37c498a Mon Sep 17 00:00:00 2001 From: ctexthuang Date: Fri, 12 Sep 2025 18:12:30 +0800 Subject: [PATCH] feat : jwt --- app/Constants/ResultCode.php | 6 ++ app/Controller/Admin/AdminUserController.php | 32 ++++++++ app/Exception/Handler/JwtExceptionHandler.php | 57 +++++++++++++ app/Interface/CheckTokenInterface.php | 10 +++ app/Lib/Jwt/AbstractJwt.php | 11 ++- app/Lib/Jwt/RequestScopedTokenTrait.php | 14 ++++ .../Token/AbstractTokenMiddleware.php | 80 +++++++++++++++++++ app/Middleware/Token/AdminTokenMiddleware.php | 29 +++++++ app/Service/Admin/AdminUser/UserService.php | 34 ++++++++ app/Service/Admin/Login/LoginService.php | 19 ++--- app/Service/BaseTokenService.php | 54 +++++++++++++ config/autoload/dependencies.php | 1 + config/autoload/jwt.php | 24 ++---- request/admin.http | 29 ++++++- 14 files changed, 362 insertions(+), 38 deletions(-) create mode 100644 app/Controller/Admin/AdminUserController.php create mode 100644 app/Exception/Handler/JwtExceptionHandler.php create mode 100644 app/Interface/CheckTokenInterface.php create mode 100644 app/Lib/Jwt/RequestScopedTokenTrait.php create mode 100644 app/Middleware/Token/AbstractTokenMiddleware.php create mode 100644 app/Middleware/Token/AdminTokenMiddleware.php create mode 100644 app/Service/Admin/AdminUser/UserService.php create mode 100644 app/Service/BaseTokenService.php diff --git a/app/Constants/ResultCode.php b/app/Constants/ResultCode.php index 3295850..dcb75b9 100644 --- a/app/Constants/ResultCode.php +++ b/app/Constants/ResultCode.php @@ -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; } diff --git a/app/Controller/Admin/AdminUserController.php b/app/Controller/Admin/AdminUserController.php new file mode 100644 index 0000000..bb06e71 --- /dev/null +++ b/app/Controller/Admin/AdminUserController.php @@ -0,0 +1,32 @@ +handle(); + } + + #[RequestMapping(path: "refresh", methods: "POST")] + public function refresh() + { + return (new UserService)->refresh(); + } +} diff --git a/app/Exception/Handler/JwtExceptionHandler.php b/app/Exception/Handler/JwtExceptionHandler.php new file mode 100644 index 0000000..13bd7ca --- /dev/null +++ b/app/Exception/Handler/JwtExceptionHandler.php @@ -0,0 +1,57 @@ +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; + } +} \ No newline at end of file diff --git a/app/Interface/CheckTokenInterface.php b/app/Interface/CheckTokenInterface.php new file mode 100644 index 0000000..96eaf25 --- /dev/null +++ b/app/Interface/CheckTokenInterface.php @@ -0,0 +1,10 @@ +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, diff --git a/app/Lib/Jwt/RequestScopedTokenTrait.php b/app/Lib/Jwt/RequestScopedTokenTrait.php new file mode 100644 index 0000000..bd2acda --- /dev/null +++ b/app/Lib/Jwt/RequestScopedTokenTrait.php @@ -0,0 +1,14 @@ +getAttribute('token'); + } +} \ No newline at end of file diff --git a/app/Middleware/Token/AbstractTokenMiddleware.php b/app/Middleware/Token/AbstractTokenMiddleware.php new file mode 100644 index 0000000..7c6661b --- /dev/null +++ b/app/Middleware/Token/AbstractTokenMiddleware.php @@ -0,0 +1,80 @@ +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 ''; + } +} diff --git a/app/Middleware/Token/AdminTokenMiddleware.php b/app/Middleware/Token/AdminTokenMiddleware.php new file mode 100644 index 0000000..156189f --- /dev/null +++ b/app/Middleware/Token/AdminTokenMiddleware.php @@ -0,0 +1,29 @@ +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错误'); + } +} diff --git a/app/Service/Admin/AdminUser/UserService.php b/app/Service/Admin/AdminUser/UserService.php new file mode 100644 index 0000000..07f68bc --- /dev/null +++ b/app/Service/Admin/AdminUser/UserService.php @@ -0,0 +1,34 @@ +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(); + } +} \ No newline at end of file diff --git a/app/Service/Admin/Login/LoginService.php b/app/Service/Admin/Login/LoginService.php index d249cdb..5a49010 100644 --- a/app/Service/Admin/Login/LoginService.php +++ b/app/Service/Admin/Login/LoginService.php @@ -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); + ]); } } \ No newline at end of file diff --git a/app/Service/BaseTokenService.php b/app/Service/BaseTokenService.php new file mode 100644 index 0000000..1b16f08 --- /dev/null +++ b/app/Service/BaseTokenService.php @@ -0,0 +1,54 @@ +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已过期'); + } +} \ No newline at end of file diff --git a/config/autoload/dependencies.php b/config/autoload/dependencies.php index bfa90c1..17ec871 100644 --- a/config/autoload/dependencies.php +++ b/config/autoload/dependencies.php @@ -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, ]; diff --git a/config/autoload/jwt.php b/config/autoload/jwt.php index ce82c2f..6f06825 100644 --- a/config/autoload/jwt.php +++ b/config/autoload/jwt.php @@ -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', ], ], // 在你想要使用不同的场景时,可以在这里添加配置.可以填一个。其他会使用默认配置 diff --git a/request/admin.http b/request/admin.http index 5ba7418..527345f 100644 --- a/request/admin.http +++ b/request/admin.http @@ -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}} \ No newline at end of file