# 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=datahub
DB_USERNAME=datahub
DB_PASSWORD=datahub

# 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=datahub
```

### 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 缓存（可选）
