add user manager and auth for backend

This commit is contained in:
2025-11-10 10:45:43 +08:00
parent bf57db57f1
commit 0cfecab68f
9 changed files with 1014 additions and 8 deletions
+229
View File
@@ -0,0 +1,229 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Model\User;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Qbhy\HyperfAuth\AuthManager;
use Carbon\Carbon;
class AuthController extends AbstractController
{
/**
* 用户注册
*/
public function register(RequestInterface $request, ResponseInterface $response)
{
$username = $request->input('username');
$password = $request->input('password');
$email = $request->input('email');
// 验证用户是否已存在
if (User::query()->where('username', $username)->exists()) {
return $response->json([
'code' => 400,
'message' => '用户名已存在',
]);
}
if (User::query()->where('email', $email)->exists()) {
return $response->json([
'code' => 400,
'message' => '邮箱已被注册',
]);
}
// 创建用户
$user = User::create([
'username' => $username,
'password' => $password, // 自动加密
'email' => $email,
'status' => 1,
]);
return $response->json([
'code' => 0,
'message' => '注册成功',
'data' => [
'id' => $user->id,
'username' => $user->username,
'email' => $user->email,
],
]);
}
/**
* 用户登录
*/
public function login(RequestInterface $request, ResponseInterface $response, AuthManager $auth)
{
$username = $request->input('username');
$password = $request->input('password');
// 查找用户
$user = User::query()->where('username', $username)->first();
if (!$user) {
return $response->json([
'code' => 401,
'message' => '用户名或密码错误',
]);
}
// 验证密码
if (!$user->verifyPassword($password)) {
return $response->json([
'code' => 401,
'message' => '用户名或密码错误',
]);
}
// 检查用户状态
if ($user->status !== 1) {
return $response->json([
'code' => 403,
'message' => '账号已被禁用',
]);
}
// 生成 Access Token
$token = $auth->guard('jwt')->login($user);
// 生成 Refresh Token
$refreshToken = bin2hex(random_bytes(32));
$user->refresh_token = $refreshToken;
$user->refresh_token_expires_at = Carbon::now()->addDays(30);
$user->save();
return $response->json([
'code' => 0,
'message' => '登录成功',
'data' => [
'access_token' => $token,
'refresh_token' => $refreshToken,
'token_type' => 'Bearer',
'expires_in' => 7200, // 2 小时
'user' => [
'id' => $user->id,
'username' => $user->username,
'email' => $user->email,
],
],
]);
}
/**
* 刷新 Access Token
*/
public function refresh(RequestInterface $request, ResponseInterface $response, AuthManager $auth)
{
$refreshToken = $request->input('refresh_token');
if (!$refreshToken) {
return $response->json([
'code' => 400,
'message' => '缺少 refresh_token 参数',
]);
}
// 查找用户
$user = User::query()->where('refresh_token', $refreshToken)->first();
if (!$user) {
return $response->json([
'code' => 401,
'message' => '无效的 refresh_token',
]);
}
// 验证 refresh token 是否过期
if (!$user->isRefreshTokenValid()) {
return $response->json([
'code' => 401,
'message' => 'refresh_token 已过期,请重新登录',
]);
}
// 检查用户状态
if ($user->status !== 1) {
return $response->json([
'code' => 403,
'message' => '账号已被禁用',
]);
}
// 生成新的 Access Token
$token = $auth->guard('jwt')->login($user);
// 可选:生成新的 Refresh Token(更安全)
$newRefreshToken = bin2hex(random_bytes(32));
$user->refresh_token = $newRefreshToken;
$user->refresh_token_expires_at = Carbon::now()->addDays(30);
$user->save();
return $response->json([
'code' => 0,
'message' => 'Token 刷新成功',
'data' => [
'access_token' => $token,
'refresh_token' => $newRefreshToken,
'token_type' => 'Bearer',
'expires_in' => 7200,
],
]);
}
/**
* 获取当前用户信息
*/
public function me(AuthManager $auth, ResponseInterface $response)
{
$user = $auth->guard('jwt')->user();
if (!$user) {
return $response->json([
'code' => 401,
'message' => '未授权',
]);
}
return $response->json([
'code' => 0,
'message' => '获取成功',
'data' => [
'id' => $user->id,
'username' => $user->username,
'email' => $user->email,
'status' => $user->status,
'ext' => $user->ext,
'created_at' => $user->created_at->toDateTimeString(),
],
]);
}
/**
* 退出登录
*/
public function logout(AuthManager $auth, ResponseInterface $response)
{
$user = $auth->guard('jwt')->user();
if ($user instanceof User) {
// 清除 refresh token
$user->refresh_token = null;
$user->refresh_token_expires_at = null;
$user->save();
}
// 注销当前 token
$auth->guard('jwt')->logout();
return $response->json([
'code' => 0,
'message' => '退出成功',
]);
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Middleware;
use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Qbhy\HyperfAuth\AuthManager;
use Qbhy\HyperfAuth\Exception\UnauthorizedException;
class AuthMiddleware implements MiddlewareInterface
{
public function __construct(
protected AuthManager $auth,
protected HttpResponse $response
) {
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
// 验证 token
$user = $this->auth->guard('jwt')->user();
if (!$user) {
return $this->response->json([
'code' => 401,
'message' => '未授权,请先登录',
])->withStatus(401);
}
// @attention check here!
// 检查用户状态
if (method_exists($user, '__get') && $user->status !== 1) {
return $this->response->json([
'code' => 403,
'message' => '账号已被禁用',
])->withStatus(403);
}
} catch (UnauthorizedException $e) {
return $this->response->json([
'code' => 401,
'message' => 'Token 无效或已过期',
])->withStatus(401);
} catch (\Throwable $e) {
return $this->response->json([
'code' => 500,
'message' => '认证失败: ' . $e->getMessage(),
])->withStatus(500);
}
return $handler->handle($request);
}
}
+105
View File
@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\Model;
use Hyperf\DbConnection\Model\Model;
use Qbhy\HyperfAuth\Authenticatable;
/**
* @property int $id
* @property string $username
* @property string $password
* @property string $email
* @property int $status
* @property array $ext
* @property string $refresh_token
* @property \Carbon\Carbon $refresh_token_expires_at
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class User extends Model implements Authenticatable
{
/**
* The table associated with the model.
*/
protected ?string $table = 'users';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [
'username',
'password',
'email',
'status',
'ext',
'refresh_token',
'refresh_token_expires_at',
];
/**
* The attributes that should be hidden for serialization.
*/
protected array $hidden = [
'password',
'refresh_token',
];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = [
'id' => 'integer',
'status' => 'integer',
'ext' => 'array',
'refresh_token_expires_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Get the name of the unique identifier for the user.
*/
public function getId()
{
return $this->id;
}
/**
* Retrieve a user by their unique identifier.
*/
public static function retrieveById($key): ?Authenticatable
{
return static::query()->find($key);
}
/**
* Set the user's password (auto-hash).
*/
public function setPasswordAttribute($value): void
{
$this->attributes['password'] = password_hash($value, PASSWORD_DEFAULT);
}
/**
* Verify the user's password.
*/
public function verifyPassword(string $password): bool
{
return password_verify($password, $this->password);
}
/**
* Check if refresh token is valid.
*/
public function isRefreshTokenValid(): bool
{
if (!$this->refresh_token || !$this->refresh_token_expires_at) {
return false;
}
return $this->refresh_token_expires_at->isFuture();
}
}
+6 -2
View File
@@ -13,9 +13,14 @@
"license": "Apache-2.0",
"require": {
"php": ">=8.1",
"composer": "*",
"96qbhy/hyperf-auth": "^3.1",
"casbin/casbin": "^4.0",
"hyperf/cache": "~3.1.0",
"hyperf/command": "~3.1.0",
"hyperf/config": "~3.1.0",
"hyperf/constants": "~3.1.0",
"hyperf/database-pgsql": "^3.1",
"hyperf/db-connection": "~3.1.0",
"hyperf/engine": "^2.10",
"hyperf/framework": "~3.1.0",
@@ -23,8 +28,7 @@
"hyperf/http-server": "~3.1.0",
"hyperf/logger": "~3.1.0",
"hyperf/memory": "~3.1.0",
"hyperf/process": "~3.1.0",
"hyperf/constants": "~3.1.0"
"hyperf/process": "~3.1.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.0",
+183
View File
@@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
/**
* This file is part of qbhy/hyperf-auth.
*
* @link https://github.com/qbhy/hyperf-auth
* @document https://github.com/qbhy/hyperf-auth/blob/master/README.md
* @contact qbhy0715@qq.com
* @license https://github.com/qbhy/hyperf-auth/blob/master/LICENSE
*/
use Qbhy\SimpleJwt\Encoders;
use Qbhy\SimpleJwt\EncryptAdapters as Encrypter;
use function Hyperf\Support\env;
use function Hyperf\Support\make;
return [
'default' => [
'guard' => 'jwt',
'provider' => 'users',
],
'guards' => [
'sso' => [
// 支持的设备,env配置时用英文逗号隔开
'clients' => explode(',', env('AUTH_SSO_CLIENTS', 'pc')),
// hyperf/redis 实例
'redis' => function () {
return make(\Hyperf\Redis\Redis::class);
},
// 自定义 redis key,必须包含 {uid}{uid} 会被替换成用户ID
'redis_key' => 'u:token:{uid}',
'driver' => Qbhy\HyperfAuth\Guard\SsoGuard::class,
'provider' => 'users',
/*
* 以下是 simple-jwt 配置
* 必填
* jwt 服务端身份标识
*/
'secret' => env('SSO_JWT_SECRET'),
/*
* 可选配置
* jwt 默认头部token使用的字段
*/
'header_name' => env('JWT_HEADER_NAME', 'Authorization'),
/*
* 可选配置
* jwt 生命周期,单位秒,默认一天
*/
'ttl' => (int) env('SIMPLE_JWT_TTL', 60 * 60 * 24),
/*
* 可选配置
* 允许过期多久以内的 token 进行刷新,单位秒,默认一周
*/
'refresh_ttl' => (int) env('SIMPLE_JWT_REFRESH_TTL', 60 * 60 * 24 * 7),
/*
* 可选配置
* 默认使用的加密类
*/
'default' => Encrypter\SHA1Encrypter::class,
/*
* 可选配置
* 加密类必须实现 Qbhy\SimpleJwt\Interfaces\Encrypter 接口
*/
'drivers' => [
Encrypter\PasswordHashEncrypter::alg() => Encrypter\PasswordHashEncrypter::class,
Encrypter\CryptEncrypter::alg() => Encrypter\CryptEncrypter::class,
Encrypter\SHA1Encrypter::alg() => Encrypter\SHA1Encrypter::class,
Encrypter\Md5Encrypter::alg() => Encrypter\Md5Encrypter::class,
],
/*
* 可选配置
* 编码类
*/
'encoder' => new Encoders\Base64UrlSafeEncoder(),
// 'encoder' => new Encoders\Base64Encoder(),
/*
* 可选配置
* 缓存类
*/
'cache' => new \Doctrine\Common\Cache\FilesystemCache(sys_get_temp_dir()),
// 如果需要分布式部署,请选择 redis 或者其他支持分布式的缓存驱动
// 'cache' => function () {
// return make(\Qbhy\HyperfAuth\HyperfRedisCache::class);
// },
/*
* 可选配置
* 缓存前缀
*/
'prefix' => env('SIMPLE_JWT_PREFIX', 'default'),
],
'jwt' => [
'driver' => Qbhy\HyperfAuth\Guard\JwtGuard::class,
'provider' => 'users',
/*
* 以下是 simple-jwt 配置
* 必填
* jwt 服务端身份标识
*/
'secret' => env('SIMPLE_JWT_SECRET'),
/*
* 可选配置
* jwt 默认头部token使用的字段
*/
'header_name' => env('JWT_HEADER_NAME', 'Authorization'),
/*
* 可选配置
* jwt 生命周期,单位秒,默认 2 小时
*/
'ttl' => (int) env('SIMPLE_JWT_TTL', 60 * 60 * 2),
/*
* 可选配置
* 允许过期多久以内的 token 进行刷新,单位秒,默认 30 天
*/
'refresh_ttl' => (int) env('SIMPLE_JWT_REFRESH_TTL', 60 * 60 * 24 * 30),
/*
* 可选配置
* 默认使用的加密类
*/
'default' => Encrypter\SHA1Encrypter::class,
/*
* 可选配置
* 加密类必须实现 Qbhy\SimpleJwt\Interfaces\Encrypter 接口
*/
'drivers' => [
Encrypter\PasswordHashEncrypter::alg() => Encrypter\PasswordHashEncrypter::class,
Encrypter\CryptEncrypter::alg() => Encrypter\CryptEncrypter::class,
Encrypter\SHA1Encrypter::alg() => Encrypter\SHA1Encrypter::class,
Encrypter\Md5Encrypter::alg() => Encrypter\Md5Encrypter::class,
],
/*
* 可选配置
* 编码类
*/
'encoder' => new Encoders\Base64UrlSafeEncoder(),
// 'encoder' => new Encoders\Base64Encoder(),
/*
* 可选配置
* 缓存类 - 使用文件系统缓存
*/
'cache' => new \Doctrine\Common\Cache\FilesystemCache(sys_get_temp_dir()),
// 如果需要分布式部署,请选择 redis 或者其他支持分布式的缓存驱动
// 'cache' => function () {
// return make(\Qbhy\HyperfAuth\HyperfRedisCache::class);
// },
/*
* 可选配置
* 缓存前缀
*/
'prefix' => env('SIMPLE_JWT_PREFIX', 'dataflow'),
],
'session' => [
'driver' => Qbhy\HyperfAuth\Guard\SessionGuard::class,
'provider' => 'users',
],
],
'providers' => [
'users' => [
'driver' => \Qbhy\HyperfAuth\Provider\EloquentProvider::class,
'model' => App\Model\User::class, // 需要实现 Qbhy\HyperfAuth\Authenticatable 接口
],
],
];
+8 -6
View File
@@ -13,15 +13,17 @@ use function Hyperf\Support\env;
return [
'default' => [
'driver' => env('DB_DRIVER', 'mysql'),
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE', 'hyperf'),
'port' => env('DB_PORT', 3306),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'driver' => env('DB_DRIVER', 'pgsql'),
'host' => env('DB_HOST', '127.0.0.1'),
'database' => env('DB_DATABASE', 'dataflow'),
'port' => env('DB_PORT', 5416),
'username' => env('DB_USERNAME', 'dataflow'),
'password' => env('DB_PASSWORD', 'dataflow'),
'charset' => env('DB_CHARSET', 'utf8'),
'collation' => env('DB_COLLATION', 'utf8_unicode_ci'),
'prefix' => env('DB_PREFIX', ''),
'schema' => env('DB_SCHEMA', 'public'),
'sslmode' => env('DB_SSL_MODE', 'prefer'),
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
+17
View File
@@ -16,3 +16,20 @@ Router::addRoute(['GET', 'POST', 'HEAD'], '/', 'App\Controller\IndexController@i
Router::get('/favicon.ico', function () {
return '';
});
// 认证相关路由(无需认证)
Router::addGroup('/api/auth', function () {
Router::post('/register', 'App\Controller\AuthController@register');
Router::post('/login', 'App\Controller\AuthController@login');
Router::post('/refresh', 'App\Controller\AuthController@refresh');
});
// 需要认证的路由
Router::addGroup('/api', function () {
Router::get('/user/me', 'App\Controller\AuthController@me');
Router::post('/auth/logout', 'App\Controller\AuthController@logout');
// 在这里添加其他需要认证的路由
}, [
'middleware' => [App\Middleware\AuthMiddleware::class],
]);
@@ -0,0 +1,38 @@
<?php
use Hyperf\Database\Schema\Schema;
use Hyperf\Database\Schema\Blueprint;
use Hyperf\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('username', 100)->unique()->comment('用户名');
$table->string('password')->comment('密码');
$table->string('email', 100)->unique()->comment('邮箱');
$table->tinyInteger('status')->default(1)->comment('状态:0=禁用,1=启用');
$table->text('ext')->nullable()->comment('扩展信息(JSON格式)');
$table->string('refresh_token', 500)->nullable()->comment('刷新令牌');
$table->timestamp('refresh_token_expires_at')->nullable()->comment('刷新令牌过期时间');
$table->datetimes();
$table->index('username');
$table->index('email');
$table->index('status');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
}
};