feat: admin login

This commit is contained in:
2024-10-27 00:34:45 +08:00
parent 3a39ff3790
commit d76e37a81d
18 changed files with 698 additions and 3 deletions

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Constants\Admin;
use Hyperf\Constants\AbstractConstants;
use Hyperf\Constants\Annotation\Constants;
#[Constants]
class UserCode extends AbstractConstants
{
/**
* 禁用
* @Message("该用户已被禁用")
*/
const DISABLE = 2;
/**
* 启用
*/
const ENABLE = 1;
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exception;
use Hyperf\Server\Exception\ServerException;
class AdminException extends ServerException
{
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Exception\Handler;
use App\Exception\AdminException;
use App\Lib\AdminReturn;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Psr\Http\Message\ResponseInterface;
use Throwable;
class AdminExceptionHandler extends ExceptionHandler
{
public function __construct(protected AdminReturn $return)
{
}
/**
* admin控制器异常处理
* @param Throwable $throwable
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
{
if ($throwable instanceof AdminException) {
$result = $this->return->error($throwable->getMessage(),[],$throwable->getCode());
// 阻止异常冒泡
$this->stopPropagation();
return $response->withHeader("Content-Type", "application/json")
->withStatus(200)
->withBody(new SwooleStream(json_encode($result, JSON_UNESCAPED_UNICODE)));
}
// 交给下一个异常处理器
return $response;
}
public function isValid(Throwable $throwable): bool
{
return true;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Exception\Handler;
use App\Lib\AdminReturn;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpServer\Request;
use Psr\Http\Message\ResponseInterface;
use Throwable;
use Hyperf\Validation\ValidationException;
use Hyperf\HttpMessage\Stream\SwooleStream;
class ValidationDataExceptionHandler extends ExceptionHandler
{
public function __construct(protected AdminReturn $adminReturn)
{
}
/**
* 验证器异常处理
* @param Throwable $throwable
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
{
if ($throwable instanceof ValidationException) {
// 格式化输出
$request = new Request();
$url = $request->path();
$urlArr = explode('/',$url);
if ($urlArr[0] == 'admin') {
$result = $this->adminReturn->error($throwable->validator->errors()->first());
}else{
//todo api
$result = $this->adminReturn->error($throwable->validator->errors()->first());
}
// 阻止异常冒泡
$this->stopPropagation();
if (!is_array($result)){
return $response->withHeader("Content-Type", "application/json")
->withStatus(200)
->withBody(new SwooleStream($result));
}
return $response->withHeader("Content-Type", "application/json")
->withStatus(200)
->withBody(new SwooleStream(json_encode($result, JSON_UNESCAPED_UNICODE)));
}
// 交给下一个异常处理器
return $response;
}
public function isValid(Throwable $throwable): bool
{
return true;
}
}

60
app/Extend/SystemUtil.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
namespace App\Extend;
use Hyperf\Context\Context;
use Psr\Http\Message\ServerRequestInterface;
use function Hyperf\Support\env;
class SystemUtil
{
/**
* prod 1=生产环境 0=开发环境
* @return bool
*/
static function checkProEnv()
{
return Env('APP_ENV') == 'prod';
}
/**
* 获取客户端 ip
* @return mixed|string
*/
static function getClientIp()
{
$request = Context::get(ServerRequestInterface::class);
$ip_addr = $request->getHeaderLine('x-forwarded-for');
if (self::verifyIp($ip_addr)) {
return $ip_addr;
}
$ip_addr = $request->getHeaderLine('remote-host');
if (self::verifyIp($ip_addr)) {
return $ip_addr;
}
$ip_addr = $request->getHeaderLine('x-real-ip');
if (self::verifyIp($ip_addr)) {
return $ip_addr;
}
$ip_addr = $request->getServerParams()['remote_addr'] ?? '0.0.0.0';
if (self::verifyIp($ip_addr)) {
return $ip_addr;
}
return '0.0.0.0';
}
/**
* 验证ip
* @param $realIp
* @return mixed
*/
static function verifyIp($realIp): mixed
{
return filter_var($realIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
}
}

View File

@@ -15,7 +15,7 @@ class AdminReturn
* @param array $debug
* @return array
*/
public static function success(string $msg = 'success', array $data = [], int $code = ReturnCode::SUCCESS, array $debug = []): array
public function success(string $msg = 'success', array $data = [], int $code = ReturnCode::SUCCESS, array $debug = []): array
{
$res = [
'code' => $code,
@@ -34,7 +34,7 @@ class AdminReturn
* @param array $debug
* @return array
*/
public static function error(string $msg = 'error', array $data = [], int $code = ReturnCode::ERROR, array $debug = []): array
public function error(string $msg = 'error', int $code = ReturnCode::ERROR, array $data = [], array $debug = []): array
{
$res = [
'code' => $code,

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Lib\Crypto;
use Exception;
class AdminPasswordCrypto implements CryptoInterface
{
/**
* 明文密码
* @var string
*/
public string $data = '';
/**
* 加密盐
* @var string
*/
public string $salt = '';
/**
* admin password 加密
* @return string
*/
public function encrypt(): string
{
try {
return hash("sha256", $this->salt . hash("sha256", $this->salt . $this->data));
} catch (Exception) {
return '';
}
}
/**
* 解密 不需要解密
* @return string
*/
public function decrypt(): string
{
return '';
}
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* This lib file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Lib\Crypto;
use Exception;
use function Hyperf\Config\config;
/**
* 接口加密类
*/
class ApiCrypto implements CryptoInterface
{
/**
* 加密数据
* @var string
*/
public string $data = '';
/**
* 加密key
* @var string
*/
private string $key;
/**
* 构造方法 配置写入
*/
public function __construct()
{
$this->key = config('system.api_return_key');
}
/**
* api加密接口
* @return string
*/
public function encrypt(): string
{
try {
//设置偏移量
$iv = substr(md5($this->data), 0, 16);
//使用 openssl 加密数据
$encrypted = openssl_encrypt($this->data,'AES-128-CBC',$this->key,OPENSSL_RAW_DATA,$iv);
return $iv.'|'.base64_encode($encrypted);
} catch (Exception) {
return '';
}
}
/**
* api解密接口
* @return string
*/
public function decrypt(): string
{
try {
$array = explode('|',$this->data);
//获取偏移量
$iv = $array[0];
//获取加密数据
$encrypted = base64_decode($array[1]);
//使用 openssl 解密数据 并返回
return openssl_decrypt($encrypted, 'AES-128-CBC',$this->key,OPENSSL_RAW_DATA,$iv);
} catch (Exception) {
return '';
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Lib\Crypto;
use Exception;
/**
* 加密工厂
*/
class CryptoFactory
{
/**
* 加密类主体
* @var CryptoInterface
*/
protected CryptoInterface $cryptoInterface;
/**
* 加密工厂
* @param string $type
* @param string $dataStr
* @param string $key
* @return ApiCrypto|CryptoInterface|JwtCrypto
* @throws Exception
*/
public function cryptoClass(string $type, string $dataStr, string $key = ''): JwtCrypto|CryptoInterface|ApiCrypto
{
switch ($type) {
case 'api':
$apiCrypto = new ApiCrypto();
$this->cryptoInterface = $apiCrypto;
break;
case 'jwt':
$jwtCrypto = new JwtCrypto();
$this->cryptoInterface = $jwtCrypto;
break;
case 'admin-password':
$adminCrypto = new AdminPasswordCrypto();
$this->cryptoInterface = $adminCrypto;
$this->cryptoInterface->salt = $key;
break;
default:
throw new Exception('The encryption algorithm does not exist');
}
$this->cryptoInterface->data = $dataStr;
return $this->cryptoInterface;
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* This lib file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Lib\Crypto;
/**
* 加密类接口
*/
interface CryptoInterface
{
public function encrypt();
public function decrypt();
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* This lib file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Lib\Crypto;
use Exception;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use function Hyperf\Config\config;
class JwtCrypto implements CryptoInterface
{
/**
* 加密数据
* @var string
*/
public string $data = '';
/**
* 加密 key
* @var string
*/
private string $key;
/**
* 加密过期时间
* @var int
*/
private int $expire;
/**
* 构造函数 获取配置
*/
public function __construct()
{
$this->key = config('system.jwt_key');
$this->expire = (int)config('system.jwt_expire');
}
/**
* jwt 加密
* @return string
*/
public function encrypt(): string
{
try {
$time = time();
$payload = [
'iat' => $time, //签发时间
'nbf' => $time, //(Not Before)某个时间点后才能访问比如设置time+30表示当前时间30秒后才能使用
'exp' => $time + $this->expire,
'data' => json_decode($this->data,true),
];
return JWT::encode($payload, $this->key,'HS256');
} catch (Exception) {
return '';
}
}
/**
* jwt 解密
* @return array
*/
public function decrypt(): array
{
try {
return (array)JWT::decode($this->data, new Key($this->key, 'HS256'));
} catch (Exception) {
return [];
}
}
}

56
app/Model/AdminUser.php Normal file
View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Model;
use Hyperf\Database\Model\Builder;
use Hyperf\DbConnection\Model\Model;
/**
* @property int $id
* @property string $username
* @property string $password
* @property string $salt
* @property int $avatar
* @property string $chinese_name
* @property string $mobile
* @property int $status
* @property string $last_login_ip
* @property string $last_login_time
* @property int $is_del
* @property int $role_id
* @property string $create_time
*/
class AdminUser extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'admin_user';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
protected array $guarded = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'avatar' => 'integer', 'status' => 'integer', 'is_del' => 'integer', 'role_id' => 'integer'];
const CREATED_AT = 'create_time';
const UPDATED_AT = null;
/**
* @param $account
* @return Builder|\Hyperf\Database\Model\Model|null
*/
public function getAdminInfoByAccount($account): \Hyperf\Database\Model\Model|Builder|null
{
return $this->where('username', $account)->where('is_del',1)->first();
}
}

View File

@@ -10,10 +10,27 @@ declare(strict_types=1);
namespace App\Service\Admin;
use App\Lib\AdminReturn;
use Hyperf\Context\Context;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
abstract class BaseService
{
/**
* 请求对象
* @var RequestInterface
*/
#[Inject]
protected RequestInterface $request;
/**
* 通用返回对象
* @var AdminReturn $return
*/
#[Inject]
protected AdminReturn $return;
/**
* 管理员id
* @var int $adminId

View File

@@ -10,13 +10,68 @@ declare(strict_types=1);
namespace App\Service\Admin\User;
use App\Constants\Admin\UserCode;
use App\Constants\AdminCode;
use App\Exception\AdminException;
use App\Extend\SystemUtil;
use App\Lib\AdminReturn;
use App\Lib\Crypto\CryptoFactory;
use App\Model\AdminUser;
use App\Service\Admin\BaseService;
use App\Service\Common\AppMakeService;
use Exception;
use Hyperf\Di\Annotation\Inject;
class LoginService extends BaseService
{
/**
* 注入管理员模型
* @var AdminUser $adminUserModel
*/
#[Inject]
protected AdminUser $adminUserModel;
/**
* 注入加密工厂
* @var CryptoFactory $cryptoFactory
*/
#[Inject]
protected CryptoFactory $cryptoFactory;
/**
* 后台登录
* @return array
* @throws Exception
*/
public function handle(): array
{
return AdminReturn::success();
$userInfo = $this->adminUserModel->getAdminInfoByAccount($this->request->input('account'));
if (!$userInfo) throw new AdminException('账号不存在');
if ($userInfo->status == UserCode::DISABLE) throw new AdminException(UserCode::getMessage($userInfo->status),AdminCode::LOGIN_ERROR);
if ($this->cryptoFactory->cryptoClass('admin-password',$this->request->input('password'),$userInfo->salt) != $userInfo->password) throw new AdminException('密码错误!');
$userInfo->last_login_time = date('Y-m-d H:i:s');
$userInfo->last_login_ip = SystemUtil::getClientIp();
$userInfo->save();
if (!$userInfo->save()) throw new AdminException('登录失败');
$token = $this->cryptoFactory->cryptoClass('jwt',json_encode([
'id' => $userInfo->id,
'role' => $userInfo->role_id,
]))->encrypt();
return $this->return->success('success',[
'token' => $token,
'info' => [
'admin_id' => $userInfo->id,
'avatar' => $userInfo->avatar,
'name' => $userInfo->chinese_name,
'role_id' => $userInfo->role_id,
]
]);
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Common;
use Exception;
use Hyperf\Context\Context;
class AppMakeService
{
/**
* 生成类对象
* @param $className
* @return mixed
* @throws Exception
*/
public static function make($className): mixed
{
if (!class_exists($className)) throw new Exception('The instantiated class does not exist');
$array = Context::get('obj');
if (!isset($array[$className])) {
$array = $array ?? [];
$array[$className] = new $className;
Context::set('obj', $array);
}
return $array[$className];
}
/**
* 获取对象类
* @return mixed
*/
public static function getObj(): mixed
{
return Context::get('obj');
}
}

View File

@@ -13,6 +13,8 @@ return [
'handler' => [
'http' => [
Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class,
App\Exception\Handler\AdminExceptionHandler::class,
App\Exception\Handler\ValidationDataExceptionHandler::class,
App\Exception\Handler\AppExceptionHandler::class,
],
],

View File

@@ -0,0 +1,20 @@
<?php
/**
* This config file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
use function Hyperf\Support\env;
return [
// api 返回加密/解密 key
'api_return_key' => env('API_RETURN_KEY','hhl@shenzhen'),
// jwt 加密 key
'jwt_key' => env('JWT_KEY','hhl@shenzhen'),
// jwt 过期时间
'jwt_expire' => env('JWT_EXPIRE',86400 * 30),
];

25
sync/database/admin.sql Normal file
View File

@@ -0,0 +1,25 @@
/*
* Server Type : MySQL
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- 后台用户表
DROP TABLE IF EXISTS `app_admin_user`;
CREATE TABLE `app_admin_user` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户密码',
`salt` varchar(6) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '盐值',
`avatar` int NOT NULL DEFAULT 0 COMMENT '用户头像',
`chinese_name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '中文名',
`mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '手机号',
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '用户状态 1 正常 2 禁用',
`last_login_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0' COMMENT '最后登录IP',
`last_login_time` datetime NOT NULL COMMENT '最后登录时间',
`is_del` tinyint(1) NOT NULL DEFAULT '1' COMMENT '用户状态 1 正常 2 删除 涉及到后台操作日志表',
`role_id` tinyint(1) NOT NULL DEFAULT '0' COMMENT '角色',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='后台用户表';