add user manager and auth for backend
This commit is contained in:
@@ -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' => '退出成功',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,9 +13,14 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.1",
|
"php": ">=8.1",
|
||||||
|
"composer": "*",
|
||||||
|
"96qbhy/hyperf-auth": "^3.1",
|
||||||
|
"casbin/casbin": "^4.0",
|
||||||
"hyperf/cache": "~3.1.0",
|
"hyperf/cache": "~3.1.0",
|
||||||
"hyperf/command": "~3.1.0",
|
"hyperf/command": "~3.1.0",
|
||||||
"hyperf/config": "~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/db-connection": "~3.1.0",
|
||||||
"hyperf/engine": "^2.10",
|
"hyperf/engine": "^2.10",
|
||||||
"hyperf/framework": "~3.1.0",
|
"hyperf/framework": "~3.1.0",
|
||||||
@@ -23,8 +28,7 @@
|
|||||||
"hyperf/http-server": "~3.1.0",
|
"hyperf/http-server": "~3.1.0",
|
||||||
"hyperf/logger": "~3.1.0",
|
"hyperf/logger": "~3.1.0",
|
||||||
"hyperf/memory": "~3.1.0",
|
"hyperf/memory": "~3.1.0",
|
||||||
"hyperf/process": "~3.1.0",
|
"hyperf/process": "~3.1.0"
|
||||||
"hyperf/constants": "~3.1.0"
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"friendsofphp/php-cs-fixer": "^3.0",
|
"friendsofphp/php-cs-fixer": "^3.0",
|
||||||
|
|||||||
@@ -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 接口
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -13,15 +13,17 @@ use function Hyperf\Support\env;
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'default' => [
|
'default' => [
|
||||||
'driver' => env('DB_DRIVER', 'mysql'),
|
'driver' => env('DB_DRIVER', 'pgsql'),
|
||||||
'host' => env('DB_HOST', 'localhost'),
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
'database' => env('DB_DATABASE', 'hyperf'),
|
'database' => env('DB_DATABASE', 'dataflow'),
|
||||||
'port' => env('DB_PORT', 3306),
|
'port' => env('DB_PORT', 5416),
|
||||||
'username' => env('DB_USERNAME', 'root'),
|
'username' => env('DB_USERNAME', 'dataflow'),
|
||||||
'password' => env('DB_PASSWORD', ''),
|
'password' => env('DB_PASSWORD', 'dataflow'),
|
||||||
'charset' => env('DB_CHARSET', 'utf8'),
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
'collation' => env('DB_COLLATION', 'utf8_unicode_ci'),
|
'collation' => env('DB_COLLATION', 'utf8_unicode_ci'),
|
||||||
'prefix' => env('DB_PREFIX', ''),
|
'prefix' => env('DB_PREFIX', ''),
|
||||||
|
'schema' => env('DB_SCHEMA', 'public'),
|
||||||
|
'sslmode' => env('DB_SSL_MODE', 'prefer'),
|
||||||
'pool' => [
|
'pool' => [
|
||||||
'min_connections' => 1,
|
'min_connections' => 1,
|
||||||
'max_connections' => 10,
|
'max_connections' => 10,
|
||||||
|
|||||||
@@ -16,3 +16,20 @@ Router::addRoute(['GET', 'POST', 'HEAD'], '/', 'App\Controller\IndexController@i
|
|||||||
Router::get('/favicon.ico', function () {
|
Router::get('/favicon.ico', function () {
|
||||||
return '';
|
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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
# JWT 认证系统使用说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本项目已配置完成基于 JWT Token 的用户认证系统,支持 token 刷新功能。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **数据库**: PostgreSQL 16
|
||||||
|
- **认证库**: 96qbhy/hyperf-auth
|
||||||
|
- **权限管理**: Casbin
|
||||||
|
- **缓存**: 文件系统缓存(FilesystemCache)
|
||||||
|
|
||||||
|
## 配置信息
|
||||||
|
|
||||||
|
### 环境变量
|
||||||
|
|
||||||
|
在 `.env` 文件中已配置:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# 数据库配置
|
||||||
|
DB_DRIVER=pgsql
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=5416
|
||||||
|
DB_DATABASE=dataflow
|
||||||
|
DB_USERNAME=dataflow
|
||||||
|
DB_PASSWORD=dataflow
|
||||||
|
|
||||||
|
# JWT 配置
|
||||||
|
SIMPLE_JWT_SECRET=your-secret-key-change-this-in-production # 生产环境请修改
|
||||||
|
JWT_HEADER_NAME=Authorization
|
||||||
|
SIMPLE_JWT_TTL=7200 # Access Token 有效期:2小时
|
||||||
|
SIMPLE_JWT_REFRESH_TTL=2592000 # Refresh Token 有效期:30天
|
||||||
|
SIMPLE_JWT_PREFIX=dataflow
|
||||||
|
```
|
||||||
|
|
||||||
|
### Token 设计
|
||||||
|
|
||||||
|
- **Access Token**:
|
||||||
|
- 有效期:2 小时
|
||||||
|
- 无状态存储,通过 JWT 签名验证
|
||||||
|
- 用于日常 API 请求认证
|
||||||
|
|
||||||
|
- **Refresh Token**:
|
||||||
|
- 有效期:30 天
|
||||||
|
- 存储在数据库中,可控制撤销
|
||||||
|
- 用于刷新 Access Token
|
||||||
|
|
||||||
|
## 数据表结构
|
||||||
|
|
||||||
|
### users 表
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | bigint | 主键 |
|
||||||
|
| username | varchar(100) | 用户名(唯一) |
|
||||||
|
| password | varchar(255) | 密码(自动加密) |
|
||||||
|
| email | varchar(100) | 邮箱(唯一) |
|
||||||
|
| status | tinyint | 状态:0=禁用,1=启用 |
|
||||||
|
| ext | text | 扩展信息(JSON格式) |
|
||||||
|
| refresh_token | varchar(500) | 刷新令牌 |
|
||||||
|
| refresh_token_expires_at | timestamp | 刷新令牌过期时间 |
|
||||||
|
| created_at | timestamp | 创建时间 |
|
||||||
|
| updated_at | timestamp | 更新时间 |
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
### 1. 用户注册
|
||||||
|
|
||||||
|
**接口**: `POST /api/auth/register`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "testuser",
|
||||||
|
"password": "password123",
|
||||||
|
"email": "test@example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "注册成功",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"username": "testuser",
|
||||||
|
"email": "test@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 用户登录
|
||||||
|
|
||||||
|
**接口**: `POST /api/auth/login`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "testuser",
|
||||||
|
"password": "password123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "登录成功",
|
||||||
|
"data": {
|
||||||
|
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
||||||
|
"refresh_token": "a1b2c3d4e5f6...",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 7200,
|
||||||
|
"user": {
|
||||||
|
"id": 1,
|
||||||
|
"username": "testuser",
|
||||||
|
"email": "test@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 刷新 Token
|
||||||
|
|
||||||
|
**接口**: `POST /api/auth/refresh`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"refresh_token": "a1b2c3d4e5f6..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "Token 刷新成功",
|
||||||
|
"data": {
|
||||||
|
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
||||||
|
"refresh_token": "new_refresh_token...",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 7200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 获取用户信息(需要认证)
|
||||||
|
|
||||||
|
**接口**: `GET /api/user/me`
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
```
|
||||||
|
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "获取成功",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"username": "testuser",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"status": 1,
|
||||||
|
"ext": null,
|
||||||
|
"created_at": "2025-11-10 10:10:05"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 退出登录(需要认证)
|
||||||
|
|
||||||
|
**接口**: `POST /api/auth/logout`
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
```
|
||||||
|
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "退出成功"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 在代码中使用认证
|
||||||
|
|
||||||
|
### 在控制器中获取当前用户
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use Qbhy\HyperfAuth\AuthManager;
|
||||||
|
use Hyperf\HttpServer\Contract\ResponseInterface;
|
||||||
|
|
||||||
|
class YourController extends AbstractController
|
||||||
|
{
|
||||||
|
public function someAction(AuthManager $auth, ResponseInterface $response)
|
||||||
|
{
|
||||||
|
// 获取当前用户
|
||||||
|
$user = $auth->guard('jwt')->user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return $response->json(['code' => 401, 'message' => '未授权']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用用户信息
|
||||||
|
$userId = $user->getId();
|
||||||
|
$username = $user->username;
|
||||||
|
|
||||||
|
// 你的业务逻辑...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 保护路由
|
||||||
|
|
||||||
|
在 `config/routes.php` 中使用中间件:
|
||||||
|
|
||||||
|
```php
|
||||||
|
Router::addGroup('/api/protected', function () {
|
||||||
|
Router::get('/resource', 'App\Controller\YourController@someAction');
|
||||||
|
}, [
|
||||||
|
'middleware' => [App\Middleware\AuthMiddleware::class],
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 前端集成示例
|
||||||
|
|
||||||
|
### JavaScript/TypeScript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 登录
|
||||||
|
async function login(username: string, password: string) {
|
||||||
|
const response = await fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// 保存 token
|
||||||
|
localStorage.setItem('access_token', data.data.access_token);
|
||||||
|
localStorage.setItem('refresh_token', data.data.refresh_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求 API(自动附带 token)
|
||||||
|
async function fetchAPI(url: string, options: RequestInit = {}) {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options.headers,
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果 token 过期,自动刷新
|
||||||
|
if (response.status === 401) {
|
||||||
|
await refreshToken();
|
||||||
|
return fetchAPI(url, options); // 重试
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新 token
|
||||||
|
async function refreshToken() {
|
||||||
|
const refreshToken = localStorage.getItem('refresh_token');
|
||||||
|
|
||||||
|
const response = await fetch('/api/auth/refresh', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ refresh_token: refreshToken })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
localStorage.setItem('access_token', data.data.access_token);
|
||||||
|
localStorage.setItem('refresh_token', data.data.refresh_token);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据库迁移
|
||||||
|
|
||||||
|
已创建的迁移文件位于 `migrations/2025_11_10_021005_create_users_table.php`
|
||||||
|
|
||||||
|
执行迁移:
|
||||||
|
```bash
|
||||||
|
php bin/hyperf.php migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
回滚迁移:
|
||||||
|
```bash
|
||||||
|
php bin/hyperf.php migrate:rollback
|
||||||
|
```
|
||||||
|
|
||||||
|
## 缓存配置
|
||||||
|
|
||||||
|
### 默认配置(文件系统缓存)
|
||||||
|
|
||||||
|
当前使用文件系统缓存,JWT Token 缓存存储在系统临时目录(`sys_get_temp_dir()`)。
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 无需额外依赖
|
||||||
|
- 配置简单
|
||||||
|
- 适合单机部署
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- 不支持分布式部署
|
||||||
|
- 重启服务器会清空临时目录
|
||||||
|
|
||||||
|
### 切换到 Redis(可选)
|
||||||
|
|
||||||
|
如需分布式部署或更好的性能,可以切换到 Redis 缓存。
|
||||||
|
|
||||||
|
**步骤**:
|
||||||
|
|
||||||
|
1. 安装 Redis 并配置 `.env` 文件中的连接信息
|
||||||
|
2. 修改 `config/autoload/auth.php` 中 jwt guard 的缓存配置:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 注释掉文件系统缓存
|
||||||
|
// 'cache' => new \Doctrine\Common\Cache\FilesystemCache(sys_get_temp_dir()),
|
||||||
|
|
||||||
|
// 启用 Redis 缓存
|
||||||
|
'cache' => function () {
|
||||||
|
return make(\Qbhy\HyperfAuth\HyperfRedisCache::class);
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全建议
|
||||||
|
|
||||||
|
1. **修改 JWT Secret**: 生产环境务必修改 `.env` 中的 `SIMPLE_JWT_SECRET`
|
||||||
|
2. **HTTPS**: 生产环境必须使用 HTTPS,防止 token 被窃取
|
||||||
|
3. **Token 存储**: 前端建议使用 HttpOnly Cookie 存储 token,比 localStorage 更安全
|
||||||
|
4. **定期轮换**: 建议定期强制用户重新登录,轮换 refresh token
|
||||||
|
5. **日志监控**: 记录异常的登录尝试和 token 使用
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: Token 过期怎么办?
|
||||||
|
A: Access Token 过期后,使用 Refresh Token 调用 `/api/auth/refresh` 接口获取新的 Access Token。
|
||||||
|
|
||||||
|
### Q: 如何实现记住我功能?
|
||||||
|
A: Refresh Token 有效期为 30 天,客户端可以在 Access Token 过期时自动使用 Refresh Token 刷新。
|
||||||
|
|
||||||
|
### Q: 如何强制用户下线?
|
||||||
|
A: 可以将用户的 `status` 设置为 0(禁用),或者清空数据库中的 `refresh_token` 字段。
|
||||||
|
|
||||||
|
### Q: 支持多设备登录吗?
|
||||||
|
A: 当前实现是单设备登录(一个用户只有一个 refresh_token)。如需支持多设备,可以创建一个单独的 `user_tokens` 表。
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
1. 修改 JWT Secret 为安全的随机字符串(生产环境必须)
|
||||||
|
2. 根据业务需求扩展 User 模型的 `ext` 字段
|
||||||
|
3. 配置 Casbin 权限规则
|
||||||
|
4. 添加更多的业务接口
|
||||||
|
5. 如需分布式部署,可配置 Redis 缓存(可选)
|
||||||
Reference in New Issue
Block a user