2025-11-10 10:45:43 +08:00
|
|
|
|
# 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
|
2026-02-28 10:38:33 +08:00
|
|
|
|
DB_DATABASE=datahub
|
|
|
|
|
|
DB_USERNAME=datahub
|
|
|
|
|
|
DB_PASSWORD=datahub
|
2025-11-10 10:45:43 +08:00
|
|
|
|
|
|
|
|
|
|
# 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天
|
2026-02-28 10:38:33 +08:00
|
|
|
|
SIMPLE_JWT_PREFIX=datahub
|
2025-11-10 10:45:43 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 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 缓存(可选)
|