# 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 缓存(可选)