From a60c6ea29e1648b36fe21ebffaf169e3e966410b Mon Sep 17 00:00:00 2001 From: ctexthuang Date: Sun, 27 Oct 2024 19:34:20 +0800 Subject: [PATCH] feat: jwt --- app/Cache/Redis/Admin/AdminRedisKey.php | 16 + app/Cache/Redis/Admin/UserCache.php | 47 ++ app/Cache/Redis/RedisCache.php | 493 ++++++++++++++++++ app/Constants/AdminCode.php | 5 + app/Controller/Admin/AuthController.php | 7 +- app/Controller/Admin/LoginController.php | 13 +- app/Lib/Crypto/CryptoFactory.php | 5 + app/Lib/Crypto/JwtCrypto.php | 17 +- app/Middleware/Admin/JwtAuthMiddleware.php | 56 ++ app/Service/Admin/User/LoginService.php | 20 +- .../ServiceTrait/Admin/GetUserInfoTrait.php | 8 + config/autoload/system.php | 2 + sync/http/admin/auth.http | 16 +- 13 files changed, 697 insertions(+), 8 deletions(-) create mode 100644 app/Cache/Redis/Admin/AdminRedisKey.php create mode 100644 app/Cache/Redis/Admin/UserCache.php create mode 100644 app/Cache/Redis/RedisCache.php create mode 100644 app/Middleware/Admin/JwtAuthMiddleware.php create mode 100644 app/Service/ServiceTrait/Admin/GetUserInfoTrait.php diff --git a/app/Cache/Redis/Admin/AdminRedisKey.php b/app/Cache/Redis/Admin/AdminRedisKey.php new file mode 100644 index 0000000..c2338f9 --- /dev/null +++ b/app/Cache/Redis/Admin/AdminRedisKey.php @@ -0,0 +1,16 @@ +redis->get(AdminRedisKey::adminUserToken($userId),'system') ?? false; + } + + /** + * @param $userId + * @param string $token + * @param int $ttl + * @return bool + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws \RedisException + */ + public function setAdminToken($userId, string $token, int $ttl): bool + { + $key = AdminRedisKey::adminUserToken($userId); + $this->redis->delete($key,'system'); + return $this->redis->setEx($key, $token, $ttl, 'system'); + } +} \ No newline at end of file diff --git a/app/Cache/Redis/RedisCache.php b/app/Cache/Redis/RedisCache.php new file mode 100644 index 0000000..ebe7fc8 --- /dev/null +++ b/app/Cache/Redis/RedisCache.php @@ -0,0 +1,493 @@ +get(RedisFactory::class)->get($poolName); + } + + + // +-------------------------------------------------------------------------------------------------------------------------------------------- + // | atom 原子操作 + // +-------------------------------------------------------------------------------------------------------------------------------------------- + /** + * @param string $script + * @param array $array + * @param int $num + * @param string $poolName + * @return mixed + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function eval(string $script, array $array, int $num = 1, string $poolName = 'default'): mixed + { + return $this->getRedis($poolName)->eval($script, $array, $num); + } + + + /** + * @param string $key + * @param int $ttl + * @param string $poolName + * @return int + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function addLock(string $key, int $ttl = 5, string $poolName = 'lock'): int + { + $script = <<getRedis($poolName)->eval($script, [$key, $ttl], 1); + } + + /** + * @param string $key + * @param string $poolName + * @return int|bool + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface|RedisException + */ + public function delLock(string $key, string $poolName = 'lock'): int|bool + { + return $this->delete($key, $poolName); + } + + + // +-------------------------------------------------------------------------------------------------------------------------------------------- + // | key + // +-------------------------------------------------------------------------------------------------------------------------------------------- + /** + * 判断 key 是否存在 + * @param string $key + * @param string $poolName + * @return int|bool + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function exists(string $key, string $poolName = 'default'): int|bool + { + return $this->getRedis($poolName)->exists($key); + } + + /** + * 删除 + * @param string $key + * @param string $poolName + * @return int|bool + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function delete(string $key, string $poolName = 'default'): int|bool + { + return $this->getRedis($poolName)->del($key); + } + + /** + * @param string $key + * @param int $ex + * @param string $poolName + * @return bool + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function expire(string $key, int $ex, string $poolName = 'default'): bool + { + return $this->getRedis($poolName)->expire($key, $ex); + } + + /** + * 返回过期时间 -1=key存在未设置过期时间 -2=key不存在 >0 = 过期时间(秒) + * @param string $key + * @param string $poolName + * @return bool|int|Redis + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function ttl(string $key, string $poolName = 'default') + { + return $this->getRedis($poolName)->ttl($key); + } + + + // +-------------------------------------------------------------------------------------------------------------------------------------------- + // | string + // +-------------------------------------------------------------------------------------------------------------------------------------------- + /** + * 设置一个key + * @param string $key + * @param string $value + * @param string $poolName + * @return bool + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function set(string $key, string $value, string $poolName = 'default'): bool + { + return $this->getRedis($poolName)->set($key, $value); + } + + /** + * 获取key + * @param string $key + * @param string $poolName + * @return false|mixed|Redis|string + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function get(string $key, string $poolName = 'default'): mixed + { + return $this->getRedis($poolName)->get($key); + } + + /** + * 添加一个有过期值的key (ps:如果 key 已经存在, setEx 命令将会替换旧的值。) + * @param string $key + * @param string $value + * @param int $ttl + * @param string $poolName + * @return Redis|bool + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function setEx(string $key, string $value, int $ttl, string $poolName = 'default'): Redis|bool + { + return $this->getRedis($poolName)->setex($key, $ttl, $value); + + } + + + /** + * 将 key 中储存的数字值增一。 + * 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。 + * @param $key + * @param string $poolName + * @return false|int|Redis + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function incr($key, string $poolName = 'default') + { + return $this->getRedis($poolName)->incr($key); + } + + // +-------------------------------------------------------------------------------------------------------------------------------------------- + // | set + // +-------------------------------------------------------------------------------------------------------------------------------------------- + /** + * 判断 value 元素是否是集合 key 的成员 + * 如果 member 元素是集合的成员,返回 1 。 + * 如果 member 元素不是集合的成员,或 key 不存在,返回 0 。 + * @param $key + * @param $value + * @param string $poolName + * @return bool|Redis + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function sIsMember($key, $value, string $poolName = 'default') + { + return $this->getRedis($poolName)->sIsMember($key, $value); + } + + + /** + * 添加集合成员 + * @param $key + * @param $value + * @param string $poolName + * @return bool|int|Redis + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function sAdd($key, $value, string $poolName = 'default') + { + return $this->getRedis($poolName)->sAdd($key, $value); + } + + /** + * 获取集合所有内容 + * @param $key + * @param string $poolName + * @return array|false|Redis + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function sMembers($key, string $poolName = 'default') + { + return $this->getRedis($poolName)->sMembers($key); + } + + /** + * 删除集合成员 + * @param $key + * @param $value + * @param string $poolName + * @return false|int|Redis + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function sRem($key, $value, string $poolName = 'default') + { + return $this->getRedis($poolName)->sRem($key, $value); + } + + // +-------------------------------------------------------------------------------------------------------------------------------------------- + // | hash + // +-------------------------------------------------------------------------------------------------------------------------------------------- + /** + * 设置多个key-value 的 hash值 + * @param $key + * @param $hashKeys + * @param null $expire + * @param string $poolName + * @return bool|Redis + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function hMset($key, $hashKeys, $expire = null, string $poolName = 'default') + { + $result = $this->getRedis($poolName)->hMset($key, $hashKeys); + if ($expire) { + $this->getRedis($poolName)->expire($key, $expire); + } + return $result; + } + + /** + * 返回列表 key 的集合数据 + * @param $key + * @param string $poolName + * @return false|array|Redis + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function hGetAll($key, string $poolName = 'default') + { + return $this->getRedis($poolName)->hGetAll($key); + } + + /** + * 设置单个key-value 的 hash值 + * @param $key + * @param $hashKey + * @param $hashValue + * @param int $expire + * @param string $poolName + * @return bool|int|Redis + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function hSet($key, $hashKey, $hashValue, int $expire = 0, string $poolName = 'default') + { + $result = $this->getRedis($poolName)->hSet($key, $hashKey, $hashValue); + if ($expire) { + $this->getRedis($poolName)->expire($key, $expire); + } + return $result; + } + + /** + * 获取单个字段的 hash 值 + * @param $key + * @param $hashKey + * @param string $poolName + * @return false|mixed|Redis|string + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function hGet($key, $hashKey, string $poolName = 'default') + { + return $this->getRedis($poolName)->hGet($key, $hashKey); + } + + /** + * 删除hash某个值 + * @param $key + * @param $hashKey + * @param string $poolName + * @return bool|int|Redis + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function hDel($key, $hashKey, string $poolName = 'default') + { + return $this->getRedis($poolName)->hDel($key, $hashKey); + } + + /** + * 查找hash某个值 + * @param $key + * @param $hashKey + * @param string $poolName + * @return bool|Redis + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function hExists($key, $hashKey, string $poolName = 'default') + { + return $this->getRedis($poolName)->hExists($key, $hashKey); + } + + + /** + * 单个字段的 hash 值原子增加 + * @param $key + * @param $hashKey + * @param $hashValue + * @param string $poolName + * @return false|int|Redis + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function HIncrBy($key, $hashKey, $hashValue, string $poolName = 'default') + { + return $this->getRedis($poolName)->hincrby($key, $hashKey, $hashValue); + } + + + // +-------------------------------------------------------------------------------------------------------------------------------------------- + // | list + // +-------------------------------------------------------------------------------------------------------------------------------------------- + /** + * 从左边入 + * @param string $key + * @param string $data + * @param string $poolName + * @return false|int|Redis + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function lPush(string $key, string $data, string $poolName = 'default') + { + return $this->getRedis($poolName)->lPush($key, $data); + } + + /** + * 从右边出 + * @param $key + * @param string $poolName + * @return array|bool|mixed|Redis|string + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function rPop($key, string $poolName = 'default') + { + return $this->getRedis($poolName)->rPop($key); + } + + /** + * 批量加入数据 + * @param $data + * @param string $poolName + * @return RedisProxy + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function lPushBatch($data, string $poolName = 'default') + { + $result = $this->getRedis($poolName); + call_user_func_array([$result, 'lPush'], $data); + return $result; + } + + /** + * 返回列表 key 的长度 + * @param $key + * @param string $poolName + * @return bool|int|Redis + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function lLen($key, string $poolName = 'default') + { + return $this->getRedis($poolName)->lLen($key); + } + + // +-------------------------------------------------------------------------------------------------------------------------------------------- + // | sorted set + // +-------------------------------------------------------------------------------------------------------------------------------------------- + /** + * 返回有序集 key 中,成员 member 的 score 值。 + * 如果 member 元素不是有序集 key 的成员,或 key 不存在,返回 nil 。 + * @param $key + * @param $value + * @param string $poolName + * @return bool|float|Redis + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function zScore($key, $value, string $poolName = 'default') + { + return $this->getRedis($poolName)->zScore($key, $value); + } + + /** + * 加入有序集合 + * @param $key + * @param $score + * @param string $poolName + * @param ...$value + * @return false|int|Redis + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ + public function zAdd($key, $score, string $poolName = 'default', ...$value) + { + return $this->getRedis($poolName)->zAdd($key, $score, ...$value); + } + + +} \ No newline at end of file diff --git a/app/Constants/AdminCode.php b/app/Constants/AdminCode.php index 9e8d8a9..805c486 100644 --- a/app/Constants/AdminCode.php +++ b/app/Constants/AdminCode.php @@ -11,4 +11,9 @@ class AdminCode extends ReturnCode * @Message("登录失败") */ public const int LOGIN_ERROR = 10001; + + /** + * @Message("登录token已失效") + */ + public const int LOGIN_TOKEN_ERROR = 10002; } \ No newline at end of file diff --git a/app/Controller/Admin/AuthController.php b/app/Controller/Admin/AuthController.php index 61cfdb6..b5871b8 100644 --- a/app/Controller/Admin/AuthController.php +++ b/app/Controller/Admin/AuthController.php @@ -5,16 +5,19 @@ declare(strict_types=1); namespace App\Controller\Admin; use App\Controller\AbstractController; +use App\Middleware\Admin\JwtAuthMiddleware; use App\Request\Admin\AuthRequest; use App\Service\Admin\User\RoleMenuService; use App\Service\Admin\User\RoleService; use Hyperf\HttpServer\Annotation\Controller; +use Hyperf\HttpServer\Annotation\Middlewares; use Hyperf\HttpServer\Annotation\RequestMapping; -use Hyperf\HttpServer\Contract\RequestInterface; -use Hyperf\HttpServer\Contract\ResponseInterface; use Hyperf\Validation\Annotation\Scene; #[Controller(prefix: "admin/auth")] +#[Middlewares([ + JwtAuthMiddleware::class, +])] class AuthController extends AbstractController { public function menu_add(AuthRequest $request) diff --git a/app/Controller/Admin/LoginController.php b/app/Controller/Admin/LoginController.php index adbdb95..6fa37d5 100644 --- a/app/Controller/Admin/LoginController.php +++ b/app/Controller/Admin/LoginController.php @@ -10,13 +10,24 @@ use App\Service\Admin\User\LoginService; use Hyperf\HttpServer\Annotation\Controller; use Hyperf\HttpServer\Annotation\RequestMapping; use Hyperf\Validation\Annotation\Scene; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; +use RedisException; #[Controller(prefix: "admin/login")] class LoginController extends AbstractController { + /** + * 登录 + * @param LoginRequest $request + * @return array + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RedisException + */ #[RequestMapping(path: "user", methods: "POST")] #[Scene(scene: "login")] - public function login(LoginRequest $request) + public function login(LoginRequest $request): array { $service = new LoginService(); return $service->handle(); diff --git a/app/Lib/Crypto/CryptoFactory.php b/app/Lib/Crypto/CryptoFactory.php index 23f0df3..29b2d7c 100644 --- a/app/Lib/Crypto/CryptoFactory.php +++ b/app/Lib/Crypto/CryptoFactory.php @@ -42,6 +42,11 @@ class CryptoFactory $jwtCrypto = new JwtCrypto(); $this->cryptoInterface = $jwtCrypto; break; + case 'admin-jwt': + $jwtCrypto = new JwtCrypto(); + $this->cryptoInterface = $jwtCrypto; + $this->cryptoInterface->type = $type; + break; case 'admin-password': $adminCrypto = new AdminPasswordCrypto(); $this->cryptoInterface = $adminCrypto; diff --git a/app/Lib/Crypto/JwtCrypto.php b/app/Lib/Crypto/JwtCrypto.php index e5abdda..f0d8ad5 100644 --- a/app/Lib/Crypto/JwtCrypto.php +++ b/app/Lib/Crypto/JwtCrypto.php @@ -23,6 +23,12 @@ class JwtCrypto implements CryptoInterface */ public string $data = ''; + /** + * 登录类型 + * @var string + */ + public string $type = ''; + /** * 加密 key * @var string @@ -41,7 +47,6 @@ class JwtCrypto implements CryptoInterface public function __construct() { $this->key = config('system.jwt_key'); - $this->expire = (int)config('system.jwt_expire'); } /** @@ -50,6 +55,16 @@ class JwtCrypto implements CryptoInterface */ public function encrypt(): string { + switch ($this->type) { + case 'admin-jwt': + $this->expire = (int)config('system.admin_jwt_expire'); + break; + default: + case 'jwt': + $this->expire = (int)config('system.jwt_expire'); + break; + } + try { $time = time(); $payload = [ diff --git a/app/Middleware/Admin/JwtAuthMiddleware.php b/app/Middleware/Admin/JwtAuthMiddleware.php new file mode 100644 index 0000000..c33e10b --- /dev/null +++ b/app/Middleware/Admin/JwtAuthMiddleware.php @@ -0,0 +1,56 @@ +getHeaderLine('Authorization'); + + if (empty($authorization)) { + return $this->response->json( + $this->apiReturn->error(AdminCode::getMessage(AdminCode::LOGIN_ERROR), AdminCode::LOGIN_ERROR) + ); + } + + $authorization = str_replace("Bearer ", "", $authorization); + $userJwt = $this->cryptoFactory->cryptoClass('admin-jwt', $authorization)->decrypt(); + if (empty($userJwt)) { + return $this->response->json( + $this->apiReturn->error(AdminCode::getMessage(AdminCode::LOGIN_TOKEN_ERROR), AdminCode::LOGIN_TOKEN_ERROR) + ); + } + + //单点登录 + if ($this->userCache->getAdminToken($userJwt['data']->id) != $authorization) { + return $this->response->json( + $this->apiReturn->error(AdminCode::getMessage(AdminCode::LOGIN_TOKEN_ERROR), AdminCode::LOGIN_TOKEN_ERROR) + ); + } + + Context::set('admin_id',$userJwt['data']->id); + Context::set('role_id',$userJwt['data']->role); + + return $handler->handle($request); + } +} \ No newline at end of file diff --git a/app/Service/Admin/User/LoginService.php b/app/Service/Admin/User/LoginService.php index d497acc..b782418 100644 --- a/app/Service/Admin/User/LoginService.php +++ b/app/Service/Admin/User/LoginService.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace App\Service\Admin\User; +use App\Cache\Redis\Admin\UserCache; use App\Constants\Admin\UserCode; use App\Constants\AdminCode; use App\Exception\AdminException; @@ -19,6 +20,9 @@ use App\Model\AdminUser; use App\Service\Admin\BaseService; use Exception; use Hyperf\Di\Annotation\Inject; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; +use function Hyperf\Config\config; class LoginService extends BaseService { @@ -37,10 +41,19 @@ class LoginService extends BaseService #[Inject] protected CryptoFactory $cryptoFactory; + /** + * 注入用户缓存 + * @var UserCache $userCache + */ + #[Inject] + protected UserCache $userCache; + /** * 后台登录 * @return array - * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws \RedisException */ public function handle(): array { @@ -60,11 +73,14 @@ class LoginService extends BaseService if (!$userInfo->save()) throw new AdminException('登录失败'); //生成 token - $token = $this->cryptoFactory->cryptoClass('jwt',json_encode([ + $token = $this->cryptoFactory->cryptoClass('admin-jwt',json_encode([ 'id' => $userInfo->id, 'role' => $userInfo->role_id, ]))->encrypt(); + //单点登录 + $this->userCache->setAdminToken($userInfo->id, $token, (int)config('system.admin_jwt_expire')); + return $this->return->success('success',[ 'token' => $token, 'info' => [ diff --git a/app/Service/ServiceTrait/Admin/GetUserInfoTrait.php b/app/Service/ServiceTrait/Admin/GetUserInfoTrait.php new file mode 100644 index 0000000..553b71d --- /dev/null +++ b/app/Service/ServiceTrait/Admin/GetUserInfoTrait.php @@ -0,0 +1,8 @@ + env('JWT_KEY','hhl@shenzhen'), // jwt 过期时间 'jwt_expire' => env('JWT_EXPIRE',86400 * 30), + // admin jwt 过期时间 + 'admin_jwt_expire' => env('ADMIN_JWT_EXPIRE',86400 * 30), ]; \ No newline at end of file diff --git a/sync/http/admin/auth.http b/sync/http/admin/auth.http index c065e3a..073e751 100644 --- a/sync/http/admin/auth.http +++ b/sync/http/admin/auth.http @@ -1,5 +1,17 @@ -### 角色详情 -GET {{host}}/admin/auth/role?role_id=1 +### 登录 +POST {{host}}/admin/login/user Content-Type: application/x-www-form-urlencoded +account=13632877014&password=123456 + +> {% + client.global.set("admin_token", response.body.data.token); +%} + +### 角色详情 +GET {{host}}/admin/auth/role?role_id=1 +Content-Type: application/json +Authorization: Bearer {{admin_token}} + + ### \ No newline at end of file