From 0cfecab68f9bbe892de53b2aa0cc54ed4073da7a Mon Sep 17 00:00:00 2001 From: Nick Zeng Date: Mon, 10 Nov 2025 10:45:43 +0800 Subject: [PATCH] add user manager and auth for backend --- backend/app/Controller/AuthController.php | 229 +++++++++++ backend/app/Middleware/AuthMiddleware.php | 58 +++ backend/app/Model/User.php | 105 +++++ backend/composer.json | 8 +- backend/config/autoload/auth.php | 183 +++++++++ backend/config/autoload/databases.php | 14 +- backend/config/routes.php | 17 + .../2025_11_10_021005_create_users_table.php | 38 ++ docs/AUTH_USAGE.md | 370 ++++++++++++++++++ 9 files changed, 1014 insertions(+), 8 deletions(-) create mode 100644 backend/app/Controller/AuthController.php create mode 100644 backend/app/Middleware/AuthMiddleware.php create mode 100644 backend/app/Model/User.php create mode 100644 backend/config/autoload/auth.php create mode 100644 backend/migrations/2025_11_10_021005_create_users_table.php create mode 100644 docs/AUTH_USAGE.md diff --git a/backend/app/Controller/AuthController.php b/backend/app/Controller/AuthController.php new file mode 100644 index 0000000..a2b3167 --- /dev/null +++ b/backend/app/Controller/AuthController.php @@ -0,0 +1,229 @@ +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' => '退出成功', + ]); + } +} diff --git a/backend/app/Middleware/AuthMiddleware.php b/backend/app/Middleware/AuthMiddleware.php new file mode 100644 index 0000000..0fb6c16 --- /dev/null +++ b/backend/app/Middleware/AuthMiddleware.php @@ -0,0 +1,58 @@ +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); + } +} diff --git a/backend/app/Model/User.php b/backend/app/Model/User.php new file mode 100644 index 0000000..db167b7 --- /dev/null +++ b/backend/app/Model/User.php @@ -0,0 +1,105 @@ + '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(); + } +} diff --git a/backend/composer.json b/backend/composer.json index 713c6b7..a89816f 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -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", diff --git a/backend/config/autoload/auth.php b/backend/config/autoload/auth.php new file mode 100644 index 0000000..912a7cd --- /dev/null +++ b/backend/config/autoload/auth.php @@ -0,0 +1,183 @@ + [ + '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 接口 + ], + ], +]; diff --git a/backend/config/autoload/databases.php b/backend/config/autoload/databases.php index 79c013e..87780d1 100644 --- a/backend/config/autoload/databases.php +++ b/backend/config/autoload/databases.php @@ -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, diff --git a/backend/config/routes.php b/backend/config/routes.php index 46d04e5..44e9e4d 100644 --- a/backend/config/routes.php +++ b/backend/config/routes.php @@ -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], +]); diff --git a/backend/migrations/2025_11_10_021005_create_users_table.php b/backend/migrations/2025_11_10_021005_create_users_table.php new file mode 100644 index 0000000..2f2009b --- /dev/null +++ b/backend/migrations/2025_11_10_021005_create_users_table.php @@ -0,0 +1,38 @@ +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'); + } +}; diff --git a/docs/AUTH_USAGE.md b/docs/AUTH_USAGE.md new file mode 100644 index 0000000..e0054fe --- /dev/null +++ b/docs/AUTH_USAGE.md @@ -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 +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 缓存(可选)