first commit

This commit is contained in:
2026-02-13 15:57:29 +08:00
commit aacda0b66a
53 changed files with 10029 additions and 0 deletions

185
docs/api/api-overview.md Normal file
View File

@@ -0,0 +1,185 @@
# API 接口概览
本文档提供所有 API 接口的快速参考。
## 基础信息
### Base URL
```
开发环境: http://localhost:3000
生产环境: https://api.yourdomain.com
```
### 认证方式
本 API 使用 JWTJSON Web Token进行认证
- **Access Token**:有效期 15 分钟,用于 API 请求认证
- **Refresh Token**:有效期 7 天,用于获取新的 Access Token
### 认证 Header 格式
```http
Authorization: Bearer <access_token>
```
### 响应格式
所有接口返回统一的 JSON 格式:
**成功响应**
```json
{
"code": 200,
"message": "Success",
"data": { }
}
```
**错误响应**
```json
{
"code": 400,
"message": "错误信息",
"data": null
}
```
### 通用错误码
| 错误码 | 说明 |
|-------|------|
| 200 | 成功 |
| 400 | 请求参数错误 |
| 401 | 未授权Token 无效或过期) |
| 404 | 资源不存在 |
| 500 | 服务器内部错误 |
## 接口列表
### 公开接口(无需认证)
| 方法 | 路径 | 说明 | 详细文档 |
|------|------|------|----------|
| GET | `/health` | 健康检查 | [查看详情](endpoints/public.md#get-health) |
| GET | `/info` | 服务器信息 | [查看详情](endpoints/public.md#get-info) |
| POST | `/auth/register` | 用户注册 | [查看详情](endpoints/public.md#post-authregister) |
| POST | `/auth/login` | 用户登录 | [查看详情](endpoints/public.md#post-authlogin) |
| POST | `/auth/refresh` | 刷新 Token | [查看详情](endpoints/public.md#post-authrefresh) |
### 需要认证的接口
| 方法 | 路径 | 说明 | 详细文档 |
|------|------|------|----------|
| POST | `/auth/delete` | 删除账号 | [查看详情](endpoints/protected.md#post-authdelete) |
| POST | `/auth/delete-refresh-token` | 删除 Refresh Token | [查看详情](endpoints/protected.md#post-authdelete-refresh-token) |
## 认证流程简述
### 1. 注册/登录
用户注册或登录成功后,会返回 Access Token 和 Refresh Token
```json
{
"code": 200,
"message": "Success",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}
}
```
### 2. 使用 Access Token
将 Access Token 添加到请求头:
```http
GET /auth/delete
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
```
### 3. 刷新 Token
当 Access Token 过期时,使用 Refresh Token 获取新的 Token
```bash
POST /auth/refresh
Content-Type: application/json
{
"refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}
```
> 查看 [认证机制详解](authentication.md) 了解完整流程
## 快速示例
### 注册用户
```bash
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "password123"
}'
```
### 用户登录
```bash
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "password123"
}'
```
### 访问受保护接口
```bash
curl -X POST http://localhost:3000/auth/delete \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your_access_token>" \
-d '{
"user_id": "1234567890",
"password": "password123"
}'
```
### 健康检查
```bash
curl http://localhost:3000/health
```
## 详细文档
- [公开接口详细文档](endpoints/public.md) - 所有公开接口的详细说明
- [受保护接口详细文档](endpoints/protected.md) - 需要认证的接口详细说明
- [认证机制详解](authentication.md) - JWT 认证流程和安全最佳实践
- [前端集成示例](examples/frontend-integration.md) - JavaScript/TypeScript/React/Vue 集成代码示例
## 相关文档
- [快速开始指南](../development/getting-started.md) - 安装和运行项目
- [环境变量配置](../deployment/environment-variables.md) - 配置 API 服务器
- [前端集成指南](examples/frontend-integration.md) - 前端开发集成示例
## 获取帮助
如果您在使用 API 时遇到问题:
1. 检查请求格式是否正确
2. 确认 Token 是否有效(未过期)
3. 查看日志输出获取详细错误信息
4. 参考 [认证机制详解](authentication.md) 了解认证流程
---
**提示**:建议使用 Postman、Insomnia 或类似工具测试 API 接口。

608
docs/api/authentication.md Normal file
View File

@@ -0,0 +1,608 @@
# 认证机制详解
本文档详细说明 Web Rust Template 的 JWT 认证机制、安全特性和最佳实践。
## 目录
- [认证架构概述](#认证架构概述)
- [双 Token 机制](#双-token-机制)
- [认证流程](#认证流程)
- [Token 管理](#token-管理)
- [安全特性](#安全特性)
- [最佳实践](#最佳实践)
---
## 认证架构概述
本系统采用 **JWT (JSON Web Token)** 进行用户认证,使用 **双 Token 机制**
1. **Access Token**:短期有效,用于 API 请求认证
2. **Refresh Token**:长期有效,用于获取新的 Access Token
### 架构特点
-**无状态认证**:服务器不存储会话信息,易于扩展
-**安全性**Token 泄露影响可控,自动过期
-**用户体验**Refresh Token 可减少用户登录次数
-**可撤销性**:通过 Redis 存储 Refresh Token支持主动撤销
---
## 双 Token 机制
### Access Token
**用途**:访问需要认证的 API 接口
**特点**
- 有效期15 分钟(可配置)
- 包含用户 ID 和 Token 类型信息
- 不存储在服务器端(无状态)
- 每次请求都通过 HTTP Header 传递
**格式**
```http
Authorization: Bearer <access_token>
```
### Refresh Token
**用途**:获取新的 Access Token
**特点**
- 有效期7 天(可配置)
- 存储在 Redis 中
- 支持撤销和主动登出
- 每次刷新会生成新的 Refresh Token
**存储位置**
- 前端localStorage 或 sessionStorage
- 后端RedisKey`auth:refresh_token:<user_id>`
---
## 认证流程
### 1. 用户注册流程
```mermaid
sequenceDiagram
participant User as 用户
participant Frontend as 前端应用
participant API as API 服务器
participant DB as 数据库
participant Redis as Redis
User->>Frontend: 输入邮箱和密码
Frontend->>API: POST /auth/register
API->>API: 验证邮箱格式
API->>API: 生成用户 ID
API->>API: 哈希密码Argon2
API->>DB: 创建用户记录
DB-->>API: 用户创建成功
API->>API: 生成 Access Token (15min)
API->>API: 生成 Refresh Token (7days)
API->>Redis: 存储 Refresh Token
Redis-->>API: 存储成功
API-->>Frontend: 返回 Access Token + Refresh Token
Frontend->>Frontend: 存储 Token 到 localStorage
Frontend-->>User: 注册成功,自动登录
```
**关键点**
- 密码使用 Argon2 算法哈希,不可逆
- Refresh Token 存储在 Redis支持撤销
- 注册成功后自动登录,返回 Token
### 2. 用户登录流程
```mermaid
sequenceDiagram
participant User as 用户
participant Frontend as 前端应用
participant API as API 服务器
participant DB as 数据库
participant Redis as Redis
User->>Frontend: 输入邮箱和密码
Frontend->>API: POST /auth/login
API->>DB: 查询用户记录
DB-->>API: 返回用户信息
API->>API: 验证密码Argon2
API->>API: 生成 Access Token (15min)
API->>API: 生成 Refresh Token (7days)
API->>Redis: 存储/更新 Refresh Token
Redis-->>API: 存储成功
API-->>Frontend: 返回 Access Token + Refresh Token
Frontend->>Frontend: 存储 Token 到 localStorage
Frontend-->>User: 登录成功
```
**安全特性**
- 登录失败不返回具体错误信息(防止账号枚举)
- 密码错误会记录日志用于风控
- Refresh Token 每次登录都会更新
### 3. 访问受保护接口流程
```mermaid
sequenceDiagram
participant Frontend as 前端应用
participant API as API 服务器
participant Redis as Redis
Frontend->>API: GET /protected<br/>Authorization: Bearer <access_token>
API->>API: 验证 JWT 签名
API->>API: 检查 Token 类型
API->>API: 检查 Token 过期时间
alt Token 有效
API-->>Frontend: 200 OK 返回数据
else Token 无效或过期
API-->>Frontend: 401 Unauthorized
Frontend->>API: POST /auth/refresh
API->>Redis: 获取 Refresh Token
Redis-->>API: 返回 Refresh Token
API->>API: 验证 Refresh Token
API->>API: 生成新的 Token 对
API->>Redis: 更新 Refresh Token
API-->>Frontend: 返回新的 Token
Frontend->>API: 重试原请求
API-->>Frontend: 200 OK 返回数据
end
```
**关键点**
- 所有受保护接口都需要在 Header 中携带 Access Token
- Token 过期时前端自动刷新并重试请求
- 刷新成功后旧 Refresh Token 立即失效
### 4. Token 刷新流程
```mermaid
sequenceDiagram
participant Frontend as 前端应用
participant API as API 服务器
participant Redis as Redis
Frontend->>API: POST /auth/refresh<br/>{"refresh_token": "..."}
API->>API: 验证 Refresh Token 签名
API->>API: 检查 Token 类型(必须是 Refresh Token
API->>API: 检查 Token 过期时间
API->>Redis: 检查 Refresh Token 是否存在
alt Token 有效
API->>API: 生成新的 Access Token (15min)
API->>API: 生成新的 Refresh Token (7days)
API->>Redis: 删除旧的 Refresh Token
API->>Redis: 存储新的 Refresh Token
API-->>Frontend: 返回新的 Token 对
else Token 无效或过期
API-->>Frontend: 401 Unauthorized
Frontend->>Frontend: 清除 Token
Frontend->>Frontend: 跳转到登录页
end
```
**Token 轮换**
- 每次刷新都会生成新的 Refresh Token
- 旧的 Refresh Token 立即失效
- 防止 Token 重放攻击
### 5. 用户登出流程
```mermaid
sequenceDiagram
participant User as 用户
participant Frontend as 前端应用
participant API as API 服务器
participant Redis as Redis
User->>Frontend: 点击登出按钮
Frontend->>API: POST /auth/delete-refresh-token<br/>Authorization: Bearer <access_token>
API->>API: 验证 Access Token
API->>API: 从 Token 中提取 user_id
API->>Redis: 删除 Refresh Token
Redis-->>API: 删除成功
API-->>Frontend: 200 OK
Frontend->>Frontend: 清除本地 Token
Frontend->>Frontend: 跳转到登录页
Frontend-->>User: 登出成功
```
---
## Token 管理
### Token 生成
```rust
// src/utils/jwt.rs
// 生成 Access Token
pub fn generate_access_token(
user_id: &str,
expiration_minutes: u64,
jwt_secret: &str,
) -> Result<String> {
let expiration = Utc::now()
.checked_add_signed(Duration::minutes(expiration_minutes as i64))
.expect("invalid expiration timestamp")
.timestamp() as usize;
let claims = Claims {
sub: user_id.to_string(),
exp: expiration,
token_type: TokenType::Access,
};
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(jwt_secret.as_ref()),
)?;
Ok(token)
}
// 生成 Refresh Token
pub fn generate_refresh_token(
user_id: &str,
expiration_days: i64,
jwt_secret: &str,
) -> Result<String> {
let expiration = Utc::now()
.checked_add_signed(Duration::days(expiration_days))
.expect("invalid expiration timestamp")
.timestamp() as usize;
let claims = Claims {
sub: user_id.to_string(),
exp: expiration,
token_type: TokenType::Refresh,
};
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(jwt_secret.as_ref()),
)?;
Ok(token)
}
```
### Token 验证
```rust
// src/infra/middleware/auth.rs
pub async fn auth_middleware(
State(state): State<AppState>,
mut request: Request,
next: Next,
) -> Result<Response, ErrorResponse> {
// 1. 提取 Authorization header
let auth_header = request
.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok())
.ok_or_else(|| ErrorResponse::new("缺少 Authorization header".to_string()))?;
// 2. 验证 Bearer 格式
if !auth_header.starts_with("Bearer ") {
return Err(ErrorResponse::new("Authorization header 格式错误".to_string()));
}
let token = &auth_header[7..]; // 跳过 "Bearer "
// 3. 验证 JWT 签名和过期时间
let claims = decode_token(token, &state.config.auth.jwt_secret)?;
// 4. 检查 Token 类型(必须是 Access Token
if claims.token_type != TokenType::Access {
return Err(ErrorResponse::new("Token 类型错误".to_string()));
}
// 5. 将 user_id 添加到请求扩展中
let user_id = claims.sub;
request.extensions_mut().insert(user_id);
// 6. 继续处理请求
Ok(next.run(request).await)
}
```
### Refresh Token 存储
```rust
// src/services/auth_service.rs
async fn save_refresh_token(&self, user_id: &str, refresh_token: &str, expiration_days: i64) -> Result<()> {
let key = RedisKey::new(BusinessType::Auth)
.add_identifier("refresh_token")
.add_identifier(user_id);
let expiration_seconds = expiration_days * 24 * 3600;
self.redis_client
.set_ex(&key.build(), refresh_token, expiration_seconds as u64)
.await
.map_err(|e| anyhow::anyhow!("Redis 保存失败: {}", e))?;
Ok(())
}
```
**Redis Key 设计**
```
auth:refresh_token:<user_id>
```
**过期时间**7 天(与 Refresh Token 有效期一致)
---
## 安全特性
### 1. 密码安全
**Argon2 哈希**
- 使用 Argon2 算法(内存 hard抗 GPU/ASIC 破解)
- 自动生成随机盐值
- 哈希结果不可逆
```rust
// src/services/auth_service.rs
pub fn hash_password(&self, password: &str) -> Result<String> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| anyhow::anyhow!("密码哈希失败: {}", e))?
.to_string();
Ok(password_hash)
}
```
### 2. JWT 安全
**签名算法**HS256 (HMAC-SHA256)
**Claims 结构**
```rust
pub struct Claims {
pub sub: String, // 用户 ID
pub exp: usize, // 过期时间Unix 时间戳)
pub token_type: TokenType, // Token 类型Access/Refresh
}
```
**安全措施**
- 使用强密钥(至少 32 位随机字符串)
- Token 包含过期时间
- Token 类型区分(防止混用)
- 签名验证防止篡改
### 3. Refresh Token 安全
**存储安全**
- 存储在 Redis 中,支持快速撤销
- 每次刷新生成新 Token旧 Token 失效
- 支持主动登出,删除 Refresh Token
**使用限制**
- Refresh Token 只能使用一次
- 过期后无法续期
- 需要重新登录
### 4. 防护措施
**防重放攻击**
- Refresh Token 单次使用
- 刷新后立即失效
**防 Token 泄露**
- Access Token 短期有效15 分钟)
- 只通过 HTTPS 传输
- 不在 URL 中传递
**防暴力破解**
- 限制登录频率(可选实现)
- 记录失败尝试(日志)
- 密码哈希使用 Argon2
---
## 最佳实践
### 前端集成
#### 1. Token 存储
**推荐方案**
```typescript
// 存储 Token
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', refresh_token);
// 读取 Token
const accessToken = localStorage.getItem('access_token');
const refreshToken = localStorage.getItem('refresh_token');
// 清除 Token
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
```
#### 2. 请求拦截器
```typescript
// 添加 Token 到请求头
api.interceptors.request.use((config) => {
const accessToken = localStorage.getItem('access_token');
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
});
```
#### 3. 响应拦截器(自动刷新 Token
```typescript
// 处理 401 错误并自动刷新
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = localStorage.getItem('refresh_token');
const response = await axios.post('/auth/refresh', {
refresh_token: refreshToken,
});
const { access_token, refresh_token } = response.data.data;
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', refresh_token);
// 重试原请求
originalRequest.headers.Authorization = `Bearer ${access_token}`;
return axios(originalRequest);
} catch (refreshError) {
// 刷新失败,跳转登录页
localStorage.clear();
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
```
### 后端开发
#### 1. 密码强度要求
```rust
// 验证密码强度
fn validate_password(password: &str) -> Result<()> {
if password.len() < 8 {
return Err(anyhow!("密码长度至少 8 位"));
}
if password.len() > 100 {
return Err(anyhow!("密码长度最多 100 位"));
}
// 可添加更多规则(如必须包含大小写、数字等)
Ok(())
}
```
#### 2. JWT 密钥管理
**开发环境**
使用 `config/` 目录下的配置文件:
```bash
# 方式1使用默认配置推荐
# JWT 密钥已在 config/default.toml 中配置
# 方式2创建本地配置文件
cp config/default.toml config/local.toml
# 编辑 config/local.toml修改 jwt_secret
nano config/local.toml
# 运行
cargo run -- -c config/local.toml
```
**生产环境**
```bash
# 使用强随机密钥
AUTH_JWT_SECRET=$(openssl rand -base64 32)
```
#### 3. Token 过期时间配置
```bash
# Access Token15 分钟(推荐)
AUTH_ACCESS_TOKEN_EXPIRATION_MINUTES=15
# Refresh Token7 天(推荐)
AUTH_REFRESH_TOKEN_EXPIRATION_DAYS=7
```
**建议**
- Access Token5-30 分钟(权衡安全性和用户体验)
- Refresh Token7-30 天(根据应用安全要求)
### 生产部署
#### 1. HTTPS 强制
```nginx
server {
listen 80;
server_name api.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name api.yourdomain.com;
# SSL 配置...
}
```
#### 2. CORS 配置
开发环境可以允许所有来源:
```rust
CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any)
```
生产环境应该限制允许的来源:
```rust
CorsLayer::new()
.allow_origin("https://yourdomain.com".parse::<HeaderValue>().unwrap())
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
.allow_headers([HeaderName::from_static("content-type"), HeaderName::from_static("authorization")])
```
#### 3. 速率限制
防止暴力破解和 DDoS 攻击(需要额外实现):
```rust
// 使用 governor 库实现速率限制
use governor::{Quota, RateLimiter};
let limiter = RateLimiter::direct(Quota::per_second(5));
// 每秒最多 5 个请求
```
---
## 相关文档
- [公开接口文档](endpoints/public.md) - 注册、登录、刷新 Token 接口
- [受保护接口文档](endpoints/protected.md) - 需要认证的接口
- [前端集成示例](examples/frontend-integration.md) - 完整的前端集成代码
- [环境变量配置](../deployment/environment-variables.md) - 认证相关配置说明
---
**提示**:生产环境部署前务必修改 JWT 密钥为强随机字符串!

View File

@@ -0,0 +1,368 @@
# 公开接口文档
本文档详细说明所有无需认证即可访问的 API 接口。
## 目录
- [GET /health - 健康检查](#get-health)
- [GET /info - 服务器信息](#get-info)
- [POST /auth/register - 用户注册](#post-authregister)
- [POST /auth/login - 用户登录](#post-authlogin)
- [POST /auth/refresh - 刷新 Token](#post-authrefresh)
---
## GET /health
健康检查端点,用于检查服务是否正常运行。
### 请求
```http
GET /health
```
**请求参数**:无
**请求头**:无特殊要求
### 响应
**成功响应 (200)**
```json
{
"status": "ok"
}
```
或服务不可用时:
```json
{
"status": "unavailable"
}
```
### 示例
```bash
curl http://localhost:3000/health
```
### 错误码
| 错误码 | 说明 |
|-------|------|
| 500 | 服务器内部错误 |
---
## GET /info
获取服务器基本信息,包括应用名称、版本、状态等。
### 请求
```http
GET /info
```
**请求参数**:无
**请求头**:无特殊要求
### 响应
**成功响应 (200)**
```json
{
"name": "web-rust-template",
"version": "0.1.0",
"status": "running",
"timestamp": 1704112800
}
```
### 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| name | string | 应用名称 |
| version | string | 应用版本 |
| status | string | 运行状态 |
| timestamp | number | 当前时间戳Unix 时间戳) |
### 示例
```bash
curl http://localhost:3000/info
```
### 错误码
| 错误码 | 说明 |
|-------|------|
| 500 | 服务器内部错误 |
---
## POST /auth/register
创建新用户账户。注册成功后自动登录,返回 Access Token 和 Refresh Token。
### 请求
```http
POST /auth/register
Content-Type: application/json
```
**请求参数**
```json
{
"email": "user@example.com",
"password": "password123"
}
```
### 字段说明
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| email | string | 是 | 用户邮箱,必须是有效的邮箱格式 |
| password | string | 是 | 用户密码,建议长度至少 8 位 |
### 响应
**成功响应 (200)**
```json
{
"code": 200,
"message": "Success",
"data": {
"email": "user@example.com",
"created_at": "2026-02-13T12:00:00.000Z",
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
```
### 响应字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| email | string | 用户邮箱 |
| created_at | string | 账号创建时间ISO 8601 格式) |
| access_token | string | Access Token有效期 15 分钟 |
| refresh_token | string | Refresh Token有效期 7 天 |
### 示例
```bash
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "password123"
}'
```
### 错误码
| 错误码 | 说明 |
|-------|------|
| 400 | 请求参数错误(邮箱格式错误、密码长度不够) |
| 409 | 邮箱已被注册 |
| 500 | 服务器内部错误 |
### 注意事项
- 邮箱地址将作为用户的唯一标识符
- 密码会使用 Argon2 算法进行哈希存储
- 注册成功后会自动生成 Access Token 和 Refresh Token
- Refresh Token 会存储在 Redis 中,用于后续刷新 Token
---
## POST /auth/login
用户登录。验证成功后返回 Access Token 和 Refresh Token。
### 请求
```http
POST /auth/login
Content-Type: application/json
```
**请求参数**
```json
{
"email": "user@example.com",
"password": "password123"
}
```
### 字段说明
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| email | string | 是 | 用户邮箱 |
| password | string | 是 | 用户密码 |
### 响应
**成功响应 (200)**
```json
{
"code": 200,
"message": "Success",
"data": {
"id": "1234567890",
"email": "user@example.com",
"created_at": "2026-02-13T12:00:00.000Z",
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
```
### 响应字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| id | string | 用户 ID10 位数字) |
| email | string | 用户邮箱 |
| created_at | string | 账号创建时间ISO 8601 格式) |
| access_token | string | Access Token有效期 15 分钟 |
| refresh_token | string | Refresh Token有效期 7 天 |
### 示例
```bash
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "password123"
}'
```
### 错误码
| 错误码 | 说明 |
|-------|------|
| 400 | 请求参数错误 |
| 401 | 邮箱或密码错误 |
| 500 | 服务器内部错误 |
### 注意事项
- 登录失败不会返回具体的错误信息(如"邮箱不存在"或"密码错误"),统一返回"邮箱或密码错误"
- 密码错误次数过多可能会被临时限制(取决于具体实现)
- 登录成功后会生成新的 Token 对,旧的 Token 会失效
---
## POST /auth/refresh
使用 Refresh Token 获取新的 Access Token 和 Refresh Token。
### 请求
```http
POST /auth/refresh
Content-Type: application/json
```
**请求参数**
```json
{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
### 字段说明
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| refresh_token | string | 是 | Refresh Token |
### 响应
**成功响应 (200)**
```json
{
"code": 200,
"message": "Success",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
```
### 响应字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| access_token | string | 新的 Access Token有效期 15 分钟 |
| refresh_token | string | 新的 Refresh Token有效期 7 天 |
### 示例
```bash
curl -X POST http://localhost:3000/auth/refresh \
-H "Content-Type: application/json" \
-d '{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}'
```
### 错误码
| 错误码 | 说明 |
|-------|------|
| 400 | 请求参数错误 |
| 401 | Refresh Token 无效或已过期 |
| 500 | 服务器内部错误 |
### 注意事项
- 每次刷新会生成新的 Refresh Token旧的 Refresh Token 会立即失效
- Refresh Token 只能使用一次,重复使用会返回错误
- Refresh Token 有效期为 7 天,过期后需要重新登录
- Refresh Token 存储在 Redis 中,服务器重启不会丢失(如果 Redis 持久化配置正确)
### Token 刷新策略建议
**前端实现建议**
1. 在每次 API 请求失败401 错误)时尝试刷新 Token
2. 刷新成功后重试原请求
3. 刷新失败则跳转到登录页
4. 不要在 Token 即将过期时主动刷新,而是在使用时检查有效性
查看 [前端集成示例](../examples/frontend-integration.md) 了解完整的实现代码。
---
## 相关文档
- [受保护接口文档](protected.md) - 需要认证的接口说明
- [认证机制详解](../authentication.md) - 完整的认证流程说明
- [API 概览](../api-overview.md) - 所有接口快速索引
- [前端集成示例](../examples/frontend-integration.md) - 前端集成代码示例
---
**提示**:建议使用 Postman、Insomnia 或类似工具测试 API 接口。

View File

@@ -0,0 +1,768 @@
# 前端集成示例
本文档提供完整的前端集成代码示例,包括 JavaScript/TypeScript、React 和 Vue。
## 目录
- [TypeScript 基础示例](#typescript-基础示例)
- [React 集成示例](#react-集成示例)
- [Vue 集成示例](#vue-集成示例)
- [Token 存储建议](#token-存储建议)
- [错误处理](#错误处理)
---
## TypeScript 基础示例
### 认证客户端类
以下是一个完整的 TypeScript 认证客户端实现包含注册、登录、Token 刷新等功能:
```typescript
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
interface RegisterData {
email: string;
password: string;
}
interface LoginData {
email: string;
password: string;
}
interface RegisterResponse {
email: string;
created_at: string;
access_token: string;
refresh_token: string;
}
interface LoginResponse {
id: string;
email: string;
created_at: string;
access_token: string;
refresh_token: string;
}
interface RefreshResponse {
access_token: string;
refresh_token: string;
}
class AuthClient {
private baseURL: string;
private accessToken: string | null = null;
private refreshToken: string | null = null;
constructor(baseURL: string = 'http://localhost:3000') {
this.baseURL = baseURL;
// 从 localStorage 加载 Token
this.accessToken = localStorage.getItem('access_token');
this.refreshToken = localStorage.getItem('refresh_token');
}
/**
* 用户注册
*/
async register(email: string, password: string): Promise<RegisterResponse> {
const response = await fetch(`${this.baseURL}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const result: ApiResponse<RegisterResponse> = await response.json();
if (result.code === 200) {
this.saveTokens(result.data.access_token, result.data.refresh_token);
return result.data;
}
throw new Error(result.message);
}
/**
* 用户登录
*/
async login(email: string, password: string): Promise<LoginResponse> {
const response = await fetch(`${this.baseURL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const result: ApiResponse<LoginResponse> = await response.json();
if (result.code === 200) {
this.saveTokens(result.data.access_token, result.data.refresh_token);
return result.data;
}
throw new Error(result.message);
}
/**
* 刷新 Token
*/
async refreshTokens(): Promise<void> {
if (!this.refreshToken) {
throw new Error('No refresh token available');
}
const response = await fetch(`${this.baseURL}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: this.refreshToken }),
});
const result: ApiResponse<RefreshResponse> = await response.json();
if (result.code === 200) {
this.saveTokens(result.data.access_token, result.data.refresh_token);
} else {
this.clearTokens();
throw new Error(result.message);
}
}
/**
* 发起需要认证的请求
*/
async authenticatedFetch(url: string, options?: RequestInit): Promise<Response> {
if (!this.accessToken) {
throw new Error('No access token available');
}
let response = await fetch(url, {
...options,
headers: {
...options?.headers,
'Authorization': `Bearer ${this.accessToken}`,
},
});
// Token 过期,尝试刷新
if (response.status === 401) {
try {
await this.refreshTokens();
// 重试原请求
response = await fetch(url, {
...options,
headers: {
...options?.headers,
'Authorization': `Bearer ${this.accessToken}`,
},
});
} catch (error) {
// 刷新失败,清除 Token 并抛出错误
this.clearTokens();
throw error;
}
}
return response;
}
/**
* 保存 Token 到 localStorage
*/
private saveTokens(accessToken: string, refreshToken: string): void {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken);
}
/**
* 清除 Token
*/
private clearTokens(): void {
this.accessToken = null;
this.refreshToken = null;
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
}
/**
* 登出
*/
logout(): void {
this.clearTokens();
}
/**
* 检查是否已登录
*/
isAuthenticated(): boolean {
return this.accessToken !== null;
}
}
// 使用示例
const authClient = new AuthClient();
// 注册
try {
const result = await authClient.register('user@example.com', 'password123');
console.log('注册成功:', result);
} catch (error) {
console.error('注册失败:', error);
}
// 登录
try {
const result = await authClient.login('user@example.com', 'password123');
console.log('登录成功:', result);
} catch (error) {
console.error('登录失败:', error);
}
// 访问受保护接口
try {
const response = await authClient.authenticatedFetch(
'http://localhost:3000/auth/delete',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: '1234567890', password: 'password123' }),
}
);
const data = await response.json();
console.log('请求成功:', data);
} catch (error) {
console.error('请求失败:', error);
}
// 登出
authClient.logout();
```
---
## React 集成示例
### AuthContext Provider
```typescript
// AuthContext.tsx
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
interface User {
id: string;
email: string;
created_at: string;
}
interface AuthContextType {
user: User | null;
accessToken: string | null;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
register: (email: string, password: string) => Promise<void>;
logout: () => void;
loading: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 从 localStorage 加载 Token
const storedAccessToken = localStorage.getItem('access_token');
const storedUser = localStorage.getItem('user');
if (storedAccessToken && storedUser) {
setAccessToken(storedAccessToken);
setUser(JSON.parse(storedUser));
}
setLoading(false);
}, []);
const login = async (email: string, password: string) => {
const response = await fetch('http://localhost:3000/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const result = await response.json();
if (result.code === 200) {
const userData: User = {
id: result.data.id,
email: result.data.email,
created_at: result.data.created_at,
};
setUser(userData);
setAccessToken(result.data.access_token);
localStorage.setItem('access_token', result.data.access_token);
localStorage.setItem('refresh_token', result.data.refresh_token);
localStorage.setItem('user', JSON.stringify(userData));
} else {
throw new Error(result.message);
}
};
const register = async (email: string, password: string) => {
const response = await fetch('http://localhost:3000/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const result = await response.json();
if (result.code === 200) {
const userData: User = {
id: result.data.id || '',
email: result.data.email,
created_at: result.data.created_at,
};
setUser(userData);
setAccessToken(result.data.access_token);
localStorage.setItem('access_token', result.data.access_token);
localStorage.setItem('refresh_token', result.data.refresh_token);
localStorage.setItem('user', JSON.stringify(userData));
} else {
throw new Error(result.message);
}
};
const logout = () => {
setUser(null);
setAccessToken(null);
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user');
};
return (
<AuthContext.Provider
value={{
user,
accessToken,
isAuthenticated: !!accessToken,
login,
register,
logout,
loading,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
```
### API Hook带 Token 刷新)
```typescript
// useApi.ts
import { useCallback } from 'react';
import { useAuth } from './AuthContext';
export function useApi() {
const { accessToken, setAccessToken, logout } = useAuth();
const fetchWithAuth = useCallback(
async (url: string, options?: RequestInit): Promise<Response> => {
if (!accessToken) {
throw new Error('Not authenticated');
}
let response = await fetch(url, {
...options,
headers: {
...options?.headers,
'Authorization': `Bearer ${accessToken}`,
},
});
// Token 过期,尝试刷新
if (response.status === 401) {
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken) {
const refreshResponse = await fetch('http://localhost:3000/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken }),
});
const refreshResult = await refreshResponse.json();
if (refreshResult.code === 200) {
setAccessToken(refreshResult.data.access_token);
localStorage.setItem('access_token', refreshResult.data.access_token);
localStorage.setItem('refresh_token', refreshResult.data.refresh_token);
// 重试原请求
response = await fetch(url, {
...options,
headers: {
...options?.headers,
'Authorization': `Bearer ${refreshResult.data.access_token}`,
},
});
} else {
// 刷新失败,登出
logout();
throw new Error('Session expired');
}
} else {
logout();
throw new Error('Session expired');
}
}
return response;
},
[accessToken, setAccessToken, logout]
);
return { fetchWithAuth };
}
```
### 登录组件示例
```typescript
// Login.tsx
import React, { useState } from 'react';
import { useAuth } from './AuthContext';
export function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const { login } = useAuth();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
await login(email, password);
// 登录成功,路由跳转
} catch (err) {
setError(err instanceof Error ? err.message : '登录失败');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<h2></h2>
{error && <div style={{ color: 'red' }}>{error}</div>}
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="邮箱"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="密码"
required
/>
<button type="submit" disabled={loading}>
{loading ? '登录中...' : '登录'}
</button>
</form>
);
}
```
---
## Vue 集成示例
### Auth Composable
```typescript
// composables/useAuth.ts
import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
interface User {
id: string;
email: string;
created_at: string;
}
export function useAuth() {
const user = ref<User | null>(null);
const accessToken = ref<string | null>(null);
const router = useRouter();
const isAuthenticated = computed(() => !!accessToken.value);
// 初始化:从 localStorage 加载
const init = () => {
const storedAccessToken = localStorage.getItem('access_token');
const storedUser = localStorage.getItem('user');
if (storedAccessToken && storedUser) {
accessToken.value = storedAccessToken;
user.value = JSON.parse(storedUser);
}
};
const login = async (email: string, password: string) => {
const response = await fetch('http://localhost:3000/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const result = await response.json();
if (result.code === 200) {
const userData: User = {
id: result.data.id,
email: result.data.email,
created_at: result.data.created_at,
};
user.value = userData;
accessToken.value = result.data.access_token;
localStorage.setItem('access_token', result.data.access_token);
localStorage.setItem('refresh_token', result.data.refresh_token);
localStorage.setItem('user', JSON.stringify(userData));
} else {
throw new Error(result.message);
}
};
const register = async (email: string, password: string) => {
const response = await fetch('http://localhost:3000/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const result = await response.json();
if (result.code === 200) {
const userData: User = {
id: result.data.id || '',
email: result.data.email,
created_at: result.data.created_at,
};
user.value = userData;
accessToken.value = result.data.access_token;
localStorage.setItem('access_token', result.data.access_token);
localStorage.setItem('refresh_token', result.data.refresh_token);
localStorage.setItem('user', JSON.stringify(userData));
} else {
throw new Error(result.message);
}
};
const logout = () => {
user.value = null;
accessToken.value = null;
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user');
router.push('/login');
};
return {
user,
accessToken,
isAuthenticated,
login,
register,
logout,
init,
};
}
```
### Axios 拦截器示例
```typescript
// api/axios.ts
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:3000',
});
// 请求拦截器:添加 Authorization header
api.interceptors.request.use((config) => {
const accessToken = localStorage.getItem('access_token');
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
});
// 响应拦截器:处理 401 错误并刷新 Token
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken) {
try {
const response = await axios.post('/auth/refresh', {
refresh_token: refreshToken,
});
if (response.data.code === 200) {
const { access_token, refresh_token } = response.data.data;
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', refresh_token);
// 重试原请求
originalRequest.headers.Authorization = `Bearer ${access_token}`;
return axios(originalRequest);
}
} catch (refreshError) {
// 刷新失败,清除 Token
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
window.location.href = '/login';
return Promise.reject(refreshError);
}
} else {
// 没有 Refresh Token跳转到登录页
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
export default api;
```
---
## Token 存储建议
### localStorage vs sessionStorage vs Cookie
| 存储方式 | 优点 | 缺点 | 推荐场景 |
|---------|------|------|----------|
| localStorage | 数据持久化,刷新页面不丢失 | 容易受到 XSS 攻击 | Access Token、Refresh Token |
| sessionStorage | 关闭标签页自动清除 | 刷新页面会丢失 | 不推荐 |
| Cookie | 可设置 HttpOnly 防止 XSS | 容易受到 CSRF 攻击 | 服务器渲染场景 |
### 推荐方案
**前端应用SPA**
- Access TokenlocalStorage
- Refresh TokenlocalStorage
- 添加适当的 XSS 防护(内容安全策略、输入验证)
**安全性要求高的场景**
- Access Token内存React Context/Vue Reactive
- Refresh TokenHttpOnly Cookie需要后端支持
---
## 错误处理
### 通用错误处理
```typescript
async function handleApiCall<T>(
apiCall: () => Promise<T>,
onError?: (error: Error) => void
): Promise<T | null> {
try {
return await apiCall();
} catch (error) {
if (onError) {
onError(error as Error);
} else {
console.error('API 调用失败:', error);
}
return null;
}
}
// 使用示例
const result = await handleApiCall(
() => authClient.login('user@example.com', 'password123'),
(error) => {
alert(`登录失败: ${error.message}`);
}
);
```
### 网络错误重试
```typescript
async function fetchWithRetry(
url: string,
options?: RequestInit,
maxRetries: number = 3
): Promise<Response> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fetch(url, options);
} catch (error) {
if (i === maxRetries - 1) {
throw error;
}
// 等待后重试
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
throw new Error('Max retries reached');
}
```
---
## 相关文档
- [公开接口文档](../endpoints/public.md) - API 接口详细说明
- [认证机制详解](../authentication.md) - JWT 认证流程
- [受保护接口文档](../endpoints/protected.md) - 需要认证的接口
---
**提示**:以上示例代码仅供参考,实际使用时请根据项目需求调整。