first commit
This commit is contained in:
116
docs/README.md
Normal file
116
docs/README.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Web Rust Template 文档中心
|
||||
|
||||
欢迎使用 Web Rust Template 文档!本模板项目提供了生产级的 Rust Web 服务器基础架构,采用 DDD 分层设计,包含完整的认证、数据库、缓存等功能。
|
||||
|
||||
## 快速导航
|
||||
|
||||
### 按角色查找文档
|
||||
|
||||
#### 前端开发者
|
||||
- [API 接口概览](api/api-overview.md) - 快速了解所有可用的 API 接口
|
||||
- [公开接口文档](api/endpoints/public.md) - 注册、登录等公开接口的详细说明
|
||||
- [前端集成示例](api/examples/frontend-integration.md) - JavaScript/TypeScript/React/Vue 集成代码示例
|
||||
- [认证机制详解](api/authentication.md) - JWT 认证流程和最佳实践
|
||||
|
||||
#### 后端开发者
|
||||
- [快速开始指南](development/getting-started.md) - 安装、配置和运行项目
|
||||
- [项目结构详解](development/project-structure.md) - DDD 分层架构说明
|
||||
- [DDD 架构规范](development/ddd-architecture.md) - 各层设计原则和开发规范
|
||||
- [代码风格规范](development/code-style.md) - Rust 代码风格和命名规范
|
||||
- [Git 提交规范](development/git-workflow.md) - 提交信息规范和分支策略
|
||||
- [测试规范](development/testing.md) - 单元测试和集成测试指南
|
||||
|
||||
#### 运维人员
|
||||
- [环境变量配置](deployment/environment-variables.md) - 完整的环境变量列表和说明
|
||||
- [配置文件详解](deployment/configuration.md) - 多环境配置文件组织
|
||||
- [生产环境部署指南](deployment/production-guide.md) - 安全配置和部署最佳实践
|
||||
|
||||
## 文档结构
|
||||
|
||||
```
|
||||
docs/
|
||||
├── README.md # 本文档
|
||||
├── api/ # API 接口文档
|
||||
│ ├── api-overview.md # API 概览和快速参考
|
||||
│ ├── authentication.md # 认证机制详解
|
||||
│ ├── endpoints/
|
||||
│ │ ├── public.md # 公开接口
|
||||
│ │ └── protected.md # 需要认证的接口
|
||||
│ └── examples/
|
||||
│ └── frontend-integration.md # 前端集成代码示例
|
||||
├── development/ # 开发指南
|
||||
│ ├── getting-started.md # 快速开始
|
||||
│ ├── project-structure.md # 项目结构详解
|
||||
│ ├── ddd-architecture.md # DDD 分层架构规范
|
||||
│ ├── code-style.md # 代码风格和命名规范
|
||||
│ ├── git-workflow.md # Git 提交规范
|
||||
│ └── testing.md # 测试规范
|
||||
└── deployment/ # 部署文档
|
||||
├── environment-variables.md # 环境变量配置说明
|
||||
├── configuration.md # 配置文件详解
|
||||
└── production-guide.md # 生产环境部署指南
|
||||
```
|
||||
|
||||
## 核心概念
|
||||
|
||||
### DDD 分层架构
|
||||
|
||||
本项目采用领域驱动设计(DDD)分层架构:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Interface Layer (handlers) │ HTTP 处理器层
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
┌──────────────▼──────────────────────┐
|
||||
│ Application Layer (services) │ 业务逻辑层
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
┌───────┴────────┐
|
||||
│ │
|
||||
┌──────▼──────┐ ┌─────▼──────────┐
|
||||
│ Domain │ │ Infrastructure│
|
||||
│ Layer │ │ Layer │
|
||||
└─────────────┘ └────────────────┘
|
||||
```
|
||||
|
||||
### 双 Token 认证机制
|
||||
|
||||
- **Access Token**:15 分钟有效期,用于 API 请求认证
|
||||
- **Refresh Token**:7 天有效期,存储在 Redis,用于获取新的 Access Token
|
||||
- **Token 轮换**:每次刷新会生成新的 Refresh Token,旧 Token 自动失效
|
||||
|
||||
### 多数据库支持
|
||||
|
||||
支持 MySQL、PostgreSQL、SQLite 三种数据库,通过简单的环境变量配置即可切换。
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 组件 | 技术 | 版本 |
|
||||
|------|------|------|
|
||||
| Web 框架 | Axum | 0.7 |
|
||||
| 异步运行时 | Tokio | 1.x |
|
||||
| 数据库 ORM | SeaORM | 1.1 |
|
||||
| 认证 | JWT | 9.x |
|
||||
| 密码哈希 | Argon2 | 0.5 |
|
||||
| 缓存 | Redis | 0.27 |
|
||||
| 日志 | tracing | 0.1 |
|
||||
|
||||
## 快速链接
|
||||
|
||||
- [项目 README](../README.md) - 返回项目主页
|
||||
- [API 接口文档](api/api-overview.md) - 完整的 API 接口说明
|
||||
- [快速开始指南](development/getting-started.md) - 安装和配置指南
|
||||
- [开发规范](development/ddd-architecture.md) - DDD 架构和代码规范
|
||||
- [部署文档](deployment/configuration.md) - 配置和部署指南
|
||||
|
||||
## 获取帮助
|
||||
|
||||
如果您在阅读文档时有任何疑问,请:
|
||||
1. 查看相关主题的详细文档
|
||||
2. 检查 [常见问题](deployment/production-guide.md#常见问题)
|
||||
3. 提交 Issue 到项目仓库
|
||||
|
||||
---
|
||||
|
||||
**提示**:建议按照"快速开始指南"→"API 接口文档"→"开发规范"的顺序阅读文档。
|
||||
185
docs/api/api-overview.md
Normal file
185
docs/api/api-overview.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# API 接口概览
|
||||
|
||||
本文档提供所有 API 接口的快速参考。
|
||||
|
||||
## 基础信息
|
||||
|
||||
### Base URL
|
||||
|
||||
```
|
||||
开发环境: http://localhost:3000
|
||||
生产环境: https://api.yourdomain.com
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
本 API 使用 JWT(JSON 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
608
docs/api/authentication.md
Normal 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
|
||||
- 后端:Redis(Key:`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 Token:15 分钟(推荐)
|
||||
AUTH_ACCESS_TOKEN_EXPIRATION_MINUTES=15
|
||||
|
||||
# Refresh Token:7 天(推荐)
|
||||
AUTH_REFRESH_TOKEN_EXPIRATION_DAYS=7
|
||||
```
|
||||
|
||||
**建议**:
|
||||
- Access Token:5-30 分钟(权衡安全性和用户体验)
|
||||
- Refresh Token:7-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 密钥为强随机字符串!
|
||||
368
docs/api/endpoints/public.md
Normal file
368
docs/api/endpoints/public.md
Normal 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 | 用户 ID(10 位数字) |
|
||||
| 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 接口。
|
||||
768
docs/api/examples/frontend-integration.md
Normal file
768
docs/api/examples/frontend-integration.md
Normal 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 Token:localStorage
|
||||
- Refresh Token:localStorage
|
||||
- 添加适当的 XSS 防护(内容安全策略、输入验证)
|
||||
|
||||
**安全性要求高的场景**:
|
||||
- Access Token:内存(React Context/Vue Reactive)
|
||||
- Refresh Token:HttpOnly 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) - 需要认证的接口
|
||||
|
||||
---
|
||||
|
||||
**提示**:以上示例代码仅供参考,实际使用时请根据项目需求调整。
|
||||
536
docs/deployment/environment-variables.md
Normal file
536
docs/deployment/environment-variables.md
Normal file
@@ -0,0 +1,536 @@
|
||||
# 环境变量配置说明
|
||||
|
||||
本文档提供所有可配置的环境变量说明。
|
||||
|
||||
## 目录
|
||||
|
||||
- [配置优先级](#配置优先级)
|
||||
- [服务器配置](#服务器配置)
|
||||
- [数据库配置](#数据库配置)
|
||||
- [认证配置](#认证配置)
|
||||
- [Redis 配置](#redis-配置)
|
||||
- [配置示例](#配置示例)
|
||||
|
||||
---
|
||||
|
||||
## 配置优先级
|
||||
|
||||
配置的加载优先级从高到低为:
|
||||
|
||||
1. **环境变量**(最高优先级)
|
||||
2. **配置文件**(config/ 目录)
|
||||
3. **默认值**(代码中硬编码)
|
||||
|
||||
这意味着:
|
||||
- 环境变量会覆盖配置文件中的设置
|
||||
- 配置文件会覆盖代码中的默认值
|
||||
|
||||
---
|
||||
|
||||
## 服务器配置
|
||||
|
||||
### SERVER_HOST
|
||||
|
||||
服务器监听地址。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 字符串 |
|
||||
| 默认值 | `0.0.0.0` |
|
||||
| 说明 | `0.0.0.0` 表示监听所有网络接口 |
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
SERVER_HOST=127.0.0.1 # 仅本地访问
|
||||
SERVER_HOST=0.0.0.0 # 允许外部访问
|
||||
```
|
||||
|
||||
### SERVER_PORT
|
||||
|
||||
服务器监听端口。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 整数 |
|
||||
| 默认值 | `3000` |
|
||||
| 说明 | 1-65535 之间的有效端口 |
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
SERVER_PORT=3000 # 开发环境
|
||||
SERVER_PORT=8080 # 生产环境
|
||||
SERVER_PORT=80 # HTTP 标准端口
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据库配置
|
||||
|
||||
### DATABASE_TYPE
|
||||
|
||||
数据库类型,支持 MySQL、PostgreSQL、SQLite。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 字符串 |
|
||||
| 默认值 | `sqlite` |
|
||||
| 可选值 | `mysql`、`postgresql`、`sqlite` |
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
DATABASE_TYPE=sqlite # SQLite 数据库
|
||||
DATABASE_TYPE=mysql # MySQL 数据库
|
||||
DATABASE_TYPE=postgresql # PostgreSQL 数据库
|
||||
```
|
||||
|
||||
### MySQL 配置
|
||||
|
||||
当 `DATABASE_TYPE=mysql` 时使用。
|
||||
|
||||
#### DATABASE_HOST
|
||||
|
||||
MySQL 服务器地址。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 字符串 |
|
||||
| 默认值 | `localhost` |
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_HOST=192.168.1.100
|
||||
DATABASE_HOST=mysql.example.com
|
||||
```
|
||||
|
||||
#### DATABASE_PORT
|
||||
|
||||
MySQL 服务器端口。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 整数 |
|
||||
| 默认值 | `3306` |
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
DATABASE_PORT=3306
|
||||
```
|
||||
|
||||
#### DATABASE_USER
|
||||
|
||||
MySQL 用户名。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 字符串 |
|
||||
| 默认值 | - |
|
||||
| 必填 | 是 |
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
DATABASE_USER=root
|
||||
DATABASE_USER=webapp
|
||||
```
|
||||
|
||||
#### DATABASE_PASSWORD
|
||||
|
||||
MySQL 密码。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 字符串 |
|
||||
| 默认值 | - |
|
||||
| 必填 | 是 |
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
DATABASE_PASSWORD=your-password
|
||||
```
|
||||
|
||||
#### DATABASE_DATABASE
|
||||
|
||||
MySQL 数据库名称。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 字符串 |
|
||||
| 默认值 | - |
|
||||
| 必填 | 是 |
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
DATABASE_DATABASE=web_template
|
||||
```
|
||||
|
||||
### PostgreSQL 配置
|
||||
|
||||
当 `DATABASE_TYPE=postgresql` 时使用,配置项与 MySQL 相同。
|
||||
|
||||
| 环境变量 | 说明 | 默认值 |
|
||||
|---------|------|--------|
|
||||
| DATABASE_HOST | PostgreSQL 服务器地址 | localhost |
|
||||
| DATABASE_PORT | PostgreSQL 服务器端口 | 5432 |
|
||||
| DATABASE_USER | PostgreSQL 用户名 | - |
|
||||
| DATABASE_PASSWORD | PostgreSQL 密码 | - |
|
||||
| DATABASE_DATABASE | PostgreSQL 数据库名称 | - |
|
||||
|
||||
### SQLite 配置
|
||||
|
||||
当 `DATABASE_TYPE=sqlite` 时使用。
|
||||
|
||||
#### DATABASE_PATH
|
||||
|
||||
SQLite 数据库文件路径。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 字符串 |
|
||||
| 默认值 | - |
|
||||
| 必填 | 是 |
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
DATABASE_PATH=data/app.db
|
||||
DATABASE_PATH=/var/data/webapp.db
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- 目录必须存在,程序不会自动创建目录
|
||||
- 文件不存在时会自动创建
|
||||
|
||||
### DATABASE_MAX_CONNECTIONS
|
||||
|
||||
数据库连接池最大连接数。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 整数 |
|
||||
| 默认值 | `10` |
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
DATABASE_MAX_CONNECTIONS=10 # 开发环境
|
||||
DATABASE_MAX_CONNECTIONS=100 # 生产环境
|
||||
```
|
||||
|
||||
**建议**:
|
||||
- 开发环境:5-10
|
||||
- 生产环境:根据应用负载调整(通常是 CPU 核心数的 2-4 倍)
|
||||
|
||||
---
|
||||
|
||||
## 认证配置
|
||||
|
||||
### AUTH_JWT_SECRET
|
||||
|
||||
JWT 签名密钥。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 字符串 |
|
||||
| 默认值 | - |
|
||||
| 必填 | 是 |
|
||||
|
||||
**安全建议**:
|
||||
- 生产环境使用至少 32 位的随机字符串
|
||||
- 定期更换密钥
|
||||
- 不要在代码中硬编码
|
||||
|
||||
**生成强密钥**:
|
||||
```bash
|
||||
# 使用 OpenSSL
|
||||
openssl rand -base64 32
|
||||
|
||||
# 使用 Python
|
||||
python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
|
||||
# 使用 Node.js
|
||||
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
# 开发环境(不安全)
|
||||
AUTH_JWT_SECRET=dev-secret-key
|
||||
|
||||
# 生产环境(安全)
|
||||
AUTH_JWT_SECRET=Kx7Yn2Zp9qR8wF4tL6mN3vB5xC8zD1sE9aH2jK7
|
||||
```
|
||||
|
||||
### AUTH_ACCESS_TOKEN_EXPIRATION_MINUTES
|
||||
|
||||
Access Token 过期时间(分钟)。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 整数 |
|
||||
| 默认值 | `15` |
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
AUTH_ACCESS_TOKEN_EXPIRATION_MINUTES=15 # 15 分钟(推荐)
|
||||
AUTH_ACCESS_TOKEN_EXPIRATION_MINUTES=30 # 30 分钟
|
||||
AUTH_ACCESS_TOKEN_EXPIRATION_MINUTES=60 # 1 小时
|
||||
```
|
||||
|
||||
**建议**:
|
||||
- 安全性要求高:5-15 分钟
|
||||
- 用户体验优先:30-60 分钟
|
||||
- 权衡安全性和用户体验
|
||||
|
||||
### AUTH_REFRESH_TOKEN_EXPIRATION_DAYS
|
||||
|
||||
Refresh Token 过期时间(天)。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 整数 |
|
||||
| 默认值 | `7` |
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
AUTH_REFRESH_TOKEN_EXPIRATION_DAYS=7 # 7 天(推荐)
|
||||
AUTH_REFRESH_TOKEN_EXPIRATION_DAYS=30 # 30 天
|
||||
AUTH_REFRESH_TOKEN_EXPIRATION_DAYS=90 # 90 天
|
||||
```
|
||||
|
||||
**建议**:
|
||||
- Web 应用:7-30 天
|
||||
- 移动应用:30-90 天
|
||||
- 安全性要求高的应用:7 天或更短
|
||||
|
||||
---
|
||||
|
||||
## Redis 配置
|
||||
|
||||
### REDIS_HOST
|
||||
|
||||
Redis 服务器地址。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 字符串 |
|
||||
| 默认值 | `localhost` |
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
REDIS_HOST=localhost
|
||||
REDIS_HOST=192.168.1.100
|
||||
REDIS_HOST=redis.example.com
|
||||
```
|
||||
|
||||
### REDIS_PORT
|
||||
|
||||
Redis 服务器端口。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 整数 |
|
||||
| 默认值 | `6379` |
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
REDIS_PORT=6379
|
||||
```
|
||||
|
||||
### REDIS_PASSWORD
|
||||
|
||||
Redis 密码(如果设置了密码)。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 字符串 |
|
||||
| 默认值 | - |
|
||||
| 必填 | 否 |
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
REDIS_PASSWORD=your-redis-password
|
||||
```
|
||||
|
||||
### REDIS_DB
|
||||
|
||||
Redis 数据库编号。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 整数 |
|
||||
| 默认值 | `0` |
|
||||
| 范围 | 0-15 |
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
REDIS_DB=0 # 默认数据库
|
||||
REDIS_DB=1 # 数据库 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置示例
|
||||
|
||||
### 开发环境(SQLite)
|
||||
|
||||
**重要**:本项目不支持 .env 文件。开发环境请使用 `config/` 目录下的 toml 配置文件。
|
||||
|
||||
**方式一:使用默认配置(最简单)**
|
||||
|
||||
无需任何配置,直接运行即可:
|
||||
|
||||
```bash
|
||||
cargo run
|
||||
```
|
||||
|
||||
**方式二:修改配置文件**
|
||||
|
||||
如果需要修改配置,编辑 `config/default.toml` 或创建 `config/local.toml`:
|
||||
|
||||
```bash
|
||||
# 复制默认配置
|
||||
cp config/default.toml config/local.toml
|
||||
|
||||
# 编辑配置文件
|
||||
nano config/local.toml # 或使用其他编辑器
|
||||
|
||||
# 运行
|
||||
cargo run -- -c config/local.toml
|
||||
```
|
||||
|
||||
### 开发环境(MySQL)
|
||||
|
||||
**重要**:本项目不支持 .env 文件。开发环境请使用 `config/` 目录下的 toml 配置文件。
|
||||
|
||||
复制并修改 MySQL 配置文件:
|
||||
|
||||
```bash
|
||||
# 复制 MySQL 配置模板
|
||||
cp config/development.mysql.toml config/local.toml
|
||||
|
||||
# 编辑 config/local.toml,修改数据库连接信息
|
||||
nano config/local.toml
|
||||
|
||||
# 运行
|
||||
cargo run -- -c config/local.toml
|
||||
```
|
||||
|
||||
或使用环境变量(适用于 Docker/Kubernetes):
|
||||
|
||||
```bash
|
||||
DATABASE_TYPE=mysql \
|
||||
DATABASE_HOST=localhost \
|
||||
DATABASE_PORT=3306 \
|
||||
DATABASE_USER=root \
|
||||
DATABASE_PASSWORD=root \
|
||||
DATABASE_DATABASE=web_template_dev \
|
||||
cargo run
|
||||
```
|
||||
|
||||
### 生产环境
|
||||
|
||||
**重要**:本项目不支持 .env 文件。生产环境请使用 `config/` 目录下的 toml 配置文件或环境变量。
|
||||
|
||||
**方式一:使用配置文件**
|
||||
|
||||
修改 `config/production.toml` 中的配置:
|
||||
|
||||
```bash
|
||||
# 编辑生产环境配置文件
|
||||
nano config/production.toml
|
||||
|
||||
# 运行
|
||||
cargo run -- -e production -c config/production.toml
|
||||
```
|
||||
|
||||
**方式二:使用环境变量(Docker/Kubernetes 推荐)**
|
||||
|
||||
```bash
|
||||
DATABASE_TYPE=mysql \
|
||||
DATABASE_HOST=mysql.production.example.com \
|
||||
DATABASE_PORT=3306 \
|
||||
DATABASE_USER=webapp \
|
||||
DATABASE_PASSWORD=strong-password-here \
|
||||
DATABASE_DATABASE=web_template_prod \
|
||||
DATABASE_MAX_CONNECTIONS=100 \
|
||||
AUTH_JWT_SECRET=Kx7Yn2Zp9qR8wF4tL6mN3vB5xC8zD1sE9aH2jK7 \
|
||||
REDIS_HOST=redis.production.example.com \
|
||||
REDIS_PORT=6379 \
|
||||
REDIS_PASSWORD=strong-redis-password \
|
||||
REDIS_DB=0 \
|
||||
cargo run -- -e production
|
||||
```
|
||||
|
||||
### Docker Compose 配置
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
image: web-rust-template:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- SERVER_HOST=0.0.0.0
|
||||
- SERVER_PORT=3000
|
||||
- DATABASE_TYPE=postgresql
|
||||
- DATABASE_HOST=db
|
||||
- DATABASE_PORT=5432
|
||||
- DATABASE_USER=webapp
|
||||
- DATABASE_PASSWORD=password
|
||||
- DATABASE_DATABASE=web_template
|
||||
- DATABASE_MAX_CONNECTIONS=10
|
||||
- AUTH_JWT_SECRET=${JWT_SECRET}
|
||||
- AUTH_ACCESS_TOKEN_EXPIRATION_MINUTES=15
|
||||
- AUTH_REFRESH_TOKEN_EXPIRATION_DAYS=7
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_DB=0
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
db:
|
||||
image: postgres:15
|
||||
environment:
|
||||
- POSTGRES_USER=webapp
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=web_template
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安全检查清单
|
||||
|
||||
生产环境部署前检查:
|
||||
|
||||
- [ ] JWT 密钥使用强随机字符串(至少 32 位)
|
||||
- [ ] 数据库密码使用强密码
|
||||
- [ ] Redis 设置密码(如果可从外部访问)
|
||||
- [ ] 服务器监听地址根据需求配置(0.0.0.0 或 127.0.0.1)
|
||||
- [ ] 数据库连接数根据负载调整
|
||||
- [ ] Token 过期时间根据安全要求配置
|
||||
- [ ] 环境变量文件不提交到版本控制
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [配置文件详解](configuration.md) - 配置文件组织说明
|
||||
- [快速开始指南](../development/getting-started.md) - 安装和配置指南
|
||||
- [生产环境部署](production-guide.md) - 生产部署最佳实践
|
||||
|
||||
---
|
||||
|
||||
**提示**:使用 `.env.example` 作为模板,不要提交包含敏感信息的 `.env` 文件到版本控制。
|
||||
459
docs/development/getting-started.md
Normal file
459
docs/development/getting-started.md
Normal file
@@ -0,0 +1,459 @@
|
||||
# 快速开始指南
|
||||
|
||||
本文档将指导你完成 Web Rust Template 项目的安装、配置和运行。
|
||||
|
||||
## 目录
|
||||
|
||||
- [环境要求](#环境要求)
|
||||
- [安装步骤](#安装步骤)
|
||||
- [配置说明](#配置说明)
|
||||
- [运行项目](#运行项目)
|
||||
- [验证安装](#验证安装)
|
||||
- [常见问题](#常见问题)
|
||||
|
||||
---
|
||||
|
||||
## 环境要求
|
||||
|
||||
### 必需环境
|
||||
|
||||
- **Rust**:1.70 或更高版本
|
||||
- 安装方法:访问 [rustup.rs](https://rustup.rs/) 或使用 `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
|
||||
- **Git**:用于克隆项目
|
||||
|
||||
### 数据库(任选其一)
|
||||
|
||||
- **SQLite**:默认选项,无需额外安装
|
||||
- **MySQL**:5.7 或更高版本
|
||||
- **PostgreSQL**:12 或更高版本
|
||||
|
||||
### 可选环境
|
||||
|
||||
- **Redis**:用于存储 Refresh Token(推荐)
|
||||
- Windows:下载 [Redis for Windows](https://github.com/microsoftarchive/redis/releases)
|
||||
- macOS:`brew install redis`
|
||||
- Linux:`sudo apt-get install redis-server`
|
||||
|
||||
### 检查环境
|
||||
|
||||
```bash
|
||||
# 检查 Rust 版本
|
||||
rustc --version
|
||||
|
||||
# 检查 Cargo 版本
|
||||
cargo --version
|
||||
|
||||
# 检查 Git 版本
|
||||
git --version
|
||||
|
||||
# 检查 MySQL(如果使用)
|
||||
mysql --version
|
||||
|
||||
# 检查 PostgreSQL(如果使用)
|
||||
psql --version
|
||||
|
||||
# 检查 Redis(如果使用)
|
||||
redis-cli --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安装步骤
|
||||
|
||||
### 1. 克隆项目
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd web-rust-template
|
||||
```
|
||||
|
||||
### 2. 安装依赖
|
||||
|
||||
使用 Cargo 构建项目(会自动下载依赖):
|
||||
|
||||
```bash
|
||||
cargo build
|
||||
```
|
||||
|
||||
### 3. 配置项目
|
||||
|
||||
#### 方式一:使用默认配置(SQLite,最简单)
|
||||
|
||||
**无需任何配置!** 直接运行即可:
|
||||
|
||||
```bash
|
||||
cargo run
|
||||
```
|
||||
|
||||
默认配置:
|
||||
- 数据库:SQLite(自动创建 `db.sqlite3`)
|
||||
- 服务器:`127.0.0.1:3000`
|
||||
- Redis:`localhost:6379`
|
||||
|
||||
#### 方式二:使用 MySQL/PostgreSQL
|
||||
|
||||
**步骤 1**:复制对应的配置文件
|
||||
|
||||
```bash
|
||||
# 使用 MySQL
|
||||
cp config/development.mysql.toml config/local.toml
|
||||
|
||||
# 或使用 PostgreSQL
|
||||
cp config/development.postgresql.toml config/local.toml
|
||||
```
|
||||
|
||||
**步骤 2**:修改配置文件
|
||||
|
||||
编辑 `config/local.toml`,修改数据库连接信息:
|
||||
|
||||
```toml
|
||||
[database]
|
||||
# MySQL 配置
|
||||
host = "localhost"
|
||||
port = 3306
|
||||
user = "root"
|
||||
password = "your-password"
|
||||
database = "web_template_dev"
|
||||
|
||||
# 或 PostgreSQL 配置
|
||||
# host = "localhost"
|
||||
# port = 5432
|
||||
# user = "postgres"
|
||||
# password = "your-password"
|
||||
# database = "web_template_dev"
|
||||
```
|
||||
|
||||
**步骤 3**:运行项目
|
||||
|
||||
```bash
|
||||
# 使用指定配置文件运行
|
||||
cargo run -- -c config/local.toml
|
||||
```
|
||||
|
||||
#### 方式三:通过环境变量覆盖(适用于 Docker/Kubernetes)
|
||||
|
||||
```bash
|
||||
# 使用环境变量
|
||||
DATABASE_TYPE=postgresql \
|
||||
DATABASE_HOST=localhost \
|
||||
DATABASE_PORT=5432 \
|
||||
DATABASE_USER=postgres \
|
||||
DATABASE_PASSWORD=password \
|
||||
DATABASE_DATABASE=web_template_dev \
|
||||
cargo run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 数据库配置
|
||||
|
||||
#### SQLite(默认,推荐用于开发)
|
||||
|
||||
**优点**:无需额外安装,文件存储,易于测试
|
||||
|
||||
**缺点**:不支持高并发写入
|
||||
|
||||
**适用场景**:开发环境、小型应用
|
||||
|
||||
**使用方法**:无需配置,直接运行
|
||||
|
||||
#### MySQL
|
||||
|
||||
**优点**:成熟稳定,支持高并发
|
||||
|
||||
**缺点**:需要额外安装和配置
|
||||
|
||||
**适用场景**:生产环境、大型应用
|
||||
|
||||
**配置方法**:
|
||||
|
||||
**选项 1**:修改配置文件
|
||||
|
||||
```bash
|
||||
# 复制 MySQL 配置模板
|
||||
cp config/development.mysql.toml config/local.toml
|
||||
|
||||
# 编辑 config/local.toml,修改数据库连接信息
|
||||
```
|
||||
|
||||
**选项 2**:使用环境变量
|
||||
|
||||
```bash
|
||||
DATABASE_TYPE=mysql \
|
||||
DATABASE_HOST=localhost \
|
||||
DATABASE_PORT=3306 \
|
||||
DATABASE_USER=root \
|
||||
DATABASE_PASSWORD=your-password \
|
||||
DATABASE_DATABASE=web_template_dev \
|
||||
cargo run
|
||||
```
|
||||
|
||||
#### PostgreSQL
|
||||
|
||||
**优点**:功能强大,支持高级特性
|
||||
|
||||
**缺点**:资源占用较大
|
||||
|
||||
**适用场景**:需要高级数据库功能的应用
|
||||
|
||||
**配置方法**:与 MySQL 类似,使用 `config/development.postgresql.toml` 或环境变量
|
||||
|
||||
### 认证配置
|
||||
|
||||
**开发环境**:使用默认配置即可(JWT 密钥已在配置文件中)
|
||||
|
||||
**生产环境**:必须修改配置文件中的 JWT 密钥
|
||||
|
||||
```toml
|
||||
[auth]
|
||||
# 生产环境必须使用强密钥
|
||||
jwt_secret = "Kx7Yn2Zp9qR8wF4tL6mN3vB5xC8zD1sE9aH2jK7"
|
||||
```
|
||||
|
||||
生成强密钥:
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
### Redis 配置
|
||||
|
||||
**开发环境**:默认连接 `localhost:6379`,无需配置
|
||||
|
||||
**生产环境**:修改配置文件或设置环境变量
|
||||
|
||||
```bash
|
||||
REDIS_HOST=your-redis-host \
|
||||
REDIS_PORT=6379 \
|
||||
REDIS_PASSWORD=your-password \
|
||||
cargo run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 运行项目
|
||||
|
||||
### 开发模式
|
||||
|
||||
**使用默认配置(SQLite)**:
|
||||
```bash
|
||||
cargo run
|
||||
```
|
||||
|
||||
**使用指定配置文件**:
|
||||
```bash
|
||||
cargo run -- -c config/development.mysql.toml
|
||||
```
|
||||
|
||||
**使用环境变量**:
|
||||
```bash
|
||||
DATABASE_TYPE=mysql DATABASE_HOST=localhost cargo run
|
||||
```
|
||||
|
||||
### 指定环境
|
||||
|
||||
```bash
|
||||
# 开发环境
|
||||
cargo run -- -e development
|
||||
|
||||
# 生产环境
|
||||
cargo run -- -e production
|
||||
```
|
||||
|
||||
### 后台运行(生产环境)
|
||||
|
||||
```bash
|
||||
# 使用 nohup
|
||||
nohup cargo run -- -e production > app.log 2>&1 &
|
||||
|
||||
# 使用 screen
|
||||
screen -S web-rust-template
|
||||
cargo run -- -e production
|
||||
# 按 Ctrl+A 然后 D 分离会话
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验证安装
|
||||
|
||||
### 1. 健康检查
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/health
|
||||
```
|
||||
|
||||
预期响应:
|
||||
```json
|
||||
{
|
||||
"status": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 服务器信息
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/info
|
||||
```
|
||||
|
||||
预期响应:
|
||||
```json
|
||||
{
|
||||
"name": "web-rust-template",
|
||||
"version": "0.1.0",
|
||||
"status": "running",
|
||||
"timestamp": 1704112800
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 用户注册
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "test@example.com",
|
||||
"password": "password123"
|
||||
}'
|
||||
```
|
||||
|
||||
预期响应:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "Success",
|
||||
"data": {
|
||||
"email": "test@example.com",
|
||||
"created_at": "2026-02-13T12:00:00.000Z",
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"refresh_token": "eyJhbGciOiJIUzI1NiIs..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 用户登录
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "test@example.com",
|
||||
"password": "password123"
|
||||
}'
|
||||
```
|
||||
|
||||
预期响应:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "Success",
|
||||
"data": {
|
||||
"id": "1234567890",
|
||||
"email": "test@example.com",
|
||||
"created_at": "2026-02-13T12:00:00.000Z",
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"refresh_token": "eyJhbGciOiJIUzI1NiIs..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 端口被占用
|
||||
|
||||
**错误信息**:`Os { code: 10048, kind: AddrInUse }` 或 `Address already in use`
|
||||
|
||||
**解决方案**:
|
||||
|
||||
**选项 1**:修改配置文件中的端口
|
||||
|
||||
```toml
|
||||
[server]
|
||||
port = 3001
|
||||
```
|
||||
|
||||
**选项 2**:通过环境变量覆盖
|
||||
|
||||
```bash
|
||||
SERVER_PORT=3001 cargo run
|
||||
```
|
||||
|
||||
**选项 3**:停止占用端口的进程
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
netstat -ano | findstr :3000
|
||||
taskkill /PID <pid> /F
|
||||
|
||||
# macOS/Linux
|
||||
lsof -ti:3000 | xargs kill -9
|
||||
```
|
||||
|
||||
### 2. 数据库连接失败
|
||||
|
||||
**错误信息**:`Database connection failed`
|
||||
|
||||
**解决方案**:
|
||||
|
||||
- 检查数据库服务是否启动
|
||||
- 检查配置文件中的数据库配置是否正确
|
||||
- 确认数据库用户权限
|
||||
- SQLite:检查是否有写入权限
|
||||
|
||||
### 3. Redis 连接失败
|
||||
|
||||
**错误信息**:`Redis 连接失败`
|
||||
|
||||
**解决方案**:
|
||||
|
||||
- 检查 Redis 服务是否启动:`redis-cli ping`
|
||||
- 检查配置文件中的 Redis 配置是否正确
|
||||
- 如果不需要 Redis 功能,可以暂时禁用(需要修改代码)
|
||||
|
||||
### 4. 编译错误
|
||||
|
||||
**错误信息**:`error: linking with link.exe failed`
|
||||
|
||||
**解决方案**:
|
||||
|
||||
- Windows 用户需要安装 [C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/)
|
||||
- 或使用 `cargo install cargo-vcpkg` 安装依赖
|
||||
|
||||
### 5. 权限错误
|
||||
|
||||
**错误信息**:`Permission denied`
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```bash
|
||||
# Linux/macOS
|
||||
chmod +x target/debug/web-rust-template
|
||||
|
||||
# 或使用 sudo 运行(不推荐生产环境)
|
||||
sudo cargo run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
安装成功后,你可以:
|
||||
|
||||
1. 阅读 [API 接口文档](../api/api-overview.md) 了解所有可用的 API
|
||||
2. 查看 [项目结构详解](project-structure.md) 了解代码组织
|
||||
3. 学习 [DDD 架构规范](ddd-architecture.md) 了解设计原则
|
||||
4. 参考 [前端集成示例](../api/examples/frontend-integration.md) 集成前端应用
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [配置文件详解](../deployment/configuration.md) - 配置文件组织说明
|
||||
- [环境变量配置](../deployment/environment-variables.md) - 完整的环境变量列表
|
||||
- [API 接口文档](../api/api-overview.md) - 完整的 API 接口说明
|
||||
|
||||
---
|
||||
|
||||
**提示**:遇到问题?查看 [常见问题](#常见问题) 或提交 Issue 到项目仓库。
|
||||
615
docs/development/project-structure.md
Normal file
615
docs/development/project-structure.md
Normal file
@@ -0,0 +1,615 @@
|
||||
# 项目结构详解
|
||||
|
||||
本文档详细说明 Web Rust Template 的项目结构、DDD 分层架构和各层职责。
|
||||
|
||||
## 目录
|
||||
|
||||
- [DDD 分层架构](#ddd-分层架构)
|
||||
- [项目目录结构](#项目目录结构)
|
||||
- [各层职责说明](#各层职责说明)
|
||||
- [数据流转](#数据流转)
|
||||
- [核心组件](#核心组件)
|
||||
|
||||
---
|
||||
|
||||
## DDD 分层架构
|
||||
|
||||
本系统采用**领域驱动设计(DDD)**的分层架构,将代码划分为不同的职责层次。
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Interface Layer (handlers) │ HTTP 处理器层
|
||||
│ 路由定义、请求处理、响应封装 │
|
||||
└──────────────┬───────────────────────┘
|
||||
│
|
||||
┌──────────────▼───────────────────────┐
|
||||
│ Application Layer (services) │ 业务逻辑层
|
||||
│ 业务逻辑、Token 生成、认证 │
|
||||
└──────────────┬───────────────────────┘
|
||||
│
|
||||
┌───────┴────────┐
|
||||
│ │
|
||||
┌──────▼──────┐ ┌─────▼──────────┐
|
||||
│ Domain │ │ Infrastructure│
|
||||
│ Layer │ │ Layer │
|
||||
│ │ │ │
|
||||
│ - DTO │ │ - Middleware │
|
||||
│ - Entities │ │ - Redis │
|
||||
│ - VO │ │ - Repositories│
|
||||
└─────────────┘ └────────────────┘
|
||||
```
|
||||
|
||||
### 分层优势
|
||||
|
||||
| 优势 | 说明 |
|
||||
|------|------|
|
||||
| 职责清晰 | 每层只关注自己的职责,降低耦合 |
|
||||
| 易于测试 | 每层可独立测试,Mock 依赖 |
|
||||
| 易于维护 | 修改某层不影响其他层 |
|
||||
| 易于扩展 | 添加新功能只需扩展相应层 |
|
||||
|
||||
---
|
||||
|
||||
## 项目目录结构
|
||||
|
||||
```
|
||||
web-rust-template/
|
||||
├── src/ # 源代码目录
|
||||
│ ├── main.rs # 应用入口
|
||||
│ ├── cli.rs # 命令行参数解析
|
||||
│ ├── config.rs # 配置模块导出
|
||||
│ ├── db.rs # 数据库连接池
|
||||
│ ├── error.rs # 错误处理
|
||||
│ │
|
||||
│ ├── config/ # 配置模块
|
||||
│ │ ├── app.rs # 主配置结构
|
||||
│ │ ├── auth.rs # 认证配置
|
||||
│ │ ├── database.rs # 数据库配置
|
||||
│ │ ├── redis.rs # Redis 配置
|
||||
│ │ └── server.rs # 服务器配置
|
||||
│ │
|
||||
│ ├── domain/ # 领域层(DDD)
|
||||
│ │ ├── dto/ # 数据传输对象(Data Transfer Object)
|
||||
│ │ │ └── auth.rs # 认证相关 DTO
|
||||
│ │ ├── entities/ # 实体(数据库模型)
|
||||
│ │ │ └── users.rs # 用户实体
|
||||
│ │ └── vo/ # 视图对象(View Object)
|
||||
│ │ └── auth.rs # 认证相关 VO
|
||||
│ │
|
||||
│ ├── handlers/ # HTTP 处理器层(接口层)
|
||||
│ │ ├── auth.rs # 认证接口
|
||||
│ │ └── health.rs # 健康检查接口
|
||||
│ │
|
||||
│ ├── infra/ # 基础设施层
|
||||
│ │ ├── middleware/ # 中间件
|
||||
│ │ │ ├── auth.rs # JWT 认证中间件
|
||||
│ │ │ └── logging.rs # 日志中间件
|
||||
│ │ └── redis/ # Redis 客户端封装
|
||||
│ │ ├── redis_client.rs
|
||||
│ │ └── redis_key.rs
|
||||
│ │
|
||||
│ ├── repositories/ # 数据访问层
|
||||
│ │ └── user_repository.rs # 用户数据访问
|
||||
│ │
|
||||
│ ├── services/ # 业务逻辑层
|
||||
│ │ └── auth_service.rs # 认证业务逻辑
|
||||
│ │
|
||||
│ └── utils/ # 工具函数
|
||||
│ └── jwt.rs # JWT 工具类
|
||||
│
|
||||
├── config/ # 配置文件目录
|
||||
│ ├── default.toml # 默认配置
|
||||
│ ├── development.sqlite.toml # SQLite 开发环境配置
|
||||
│ ├── development.mysql.toml # MySQL 开发环境配置
|
||||
│ ├── development.postgresql.toml # PostgreSQL 开发环境配置
|
||||
│ └── production.toml # 生产环境配置
|
||||
│
|
||||
├── sql/ # SQL 脚本
|
||||
│ └── init.sql # 数据库初始化脚本
|
||||
│
|
||||
├── tests/ # 测试目录
|
||||
│ └── integration_test.rs # 集成测试
|
||||
│
|
||||
├── docs/ # 文档目录
|
||||
│ ├── README.md
|
||||
│ ├── api/
|
||||
│ ├── development/
|
||||
│ └── deployment/
|
||||
│
|
||||
├── .env.example # 环境变量参考(仅用于 Docker/Kubernetes 等部署场景)
|
||||
├── .gitignore # Git 忽略文件
|
||||
├── Cargo.toml # 项目依赖定义
|
||||
├── README.md # 项目说明
|
||||
└── rust-toolchain.toml # Rust 工具链配置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 各层职责说明
|
||||
|
||||
### 1. 接口层(handlers/)
|
||||
|
||||
**职责**:处理 HTTP 请求和响应
|
||||
|
||||
**位置**:`src/handlers/`
|
||||
|
||||
**关键文件**:
|
||||
- `auth.rs`:认证相关接口(注册、登录、刷新 Token、删除账号)
|
||||
- `health.rs`:健康检查和服务器信息接口
|
||||
|
||||
**示例**:
|
||||
```rust
|
||||
// src/handlers/auth.rs
|
||||
|
||||
pub async fn register(
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<RegisterRequest>,
|
||||
) -> Result<Json<ApiResponse<RegisterResult>>, ErrorResponse> {
|
||||
// 1. 记录日志
|
||||
log_info(&request_id, "注册请求参数", &payload);
|
||||
|
||||
// 2. 调用服务层处理业务逻辑
|
||||
let user_repo = UserRepository::new(state.pool.clone());
|
||||
let service = AuthService::new(user_repo, state.redis_client.clone(), state.config.auth.clone());
|
||||
|
||||
// 3. 调用业务逻辑
|
||||
match service.register(payload).await {
|
||||
Ok((user_model, access_token, refresh_token)) => {
|
||||
let data = RegisterResult::from((user_model, access_token, refresh_token));
|
||||
let response = ApiResponse::success(data);
|
||||
log_info(&request_id, "注册成功", &response);
|
||||
Ok(Json(response))
|
||||
}
|
||||
Err(e) => {
|
||||
log_info(&request_id, "注册失败", &e.to_string());
|
||||
Err(ErrorResponse::new(e.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**职责边界**:
|
||||
- ✅ 接收 HTTP 请求
|
||||
- ✅ 提取请求参数
|
||||
- ✅ 调用服务层处理业务逻辑
|
||||
- ✅ 封装响应数据
|
||||
- ❌ 不包含业务逻辑
|
||||
- ❌ 不直接访问数据库
|
||||
|
||||
### 2. 业务逻辑层(services/)
|
||||
|
||||
**职责**:实现核心业务逻辑
|
||||
|
||||
**位置**:`src/services/`
|
||||
|
||||
**关键文件**:
|
||||
- `auth_service.rs`:认证业务逻辑(注册、登录、Token 刷新、密码哈希)
|
||||
|
||||
**示例**:
|
||||
```rust
|
||||
// src/services/auth_service.rs
|
||||
|
||||
pub struct AuthService {
|
||||
user_repo: UserRepository,
|
||||
redis_client: RedisClient,
|
||||
auth_config: AuthConfig,
|
||||
}
|
||||
|
||||
impl AuthService {
|
||||
/// 用户注册
|
||||
pub async fn register(&self, payload: RegisterRequest) -> Result<(Model, String, String)> {
|
||||
// 1. 验证邮箱格式
|
||||
if !payload.email.contains('@') {
|
||||
return Err(anyhow!("邮箱格式错误"));
|
||||
}
|
||||
|
||||
// 2. 生成唯一用户 ID
|
||||
let user_id = self.generate_unique_user_id().await?;
|
||||
|
||||
// 3. 哈希密码
|
||||
let password_hash = self.hash_password(&payload.password)?;
|
||||
|
||||
// 4. 创建用户实体
|
||||
let user_model = users::Model {
|
||||
id: user_id,
|
||||
email: payload.email.clone(),
|
||||
password_hash,
|
||||
created_at: chrono::Utc::now().naive_utc(),
|
||||
updated_at: chrono::Utc::now().naive_utc(),
|
||||
};
|
||||
|
||||
// 5. 保存到数据库
|
||||
let created_user = self.user_repo.create(user_model).await?;
|
||||
|
||||
// 6. 生成 Token
|
||||
let (access_token, refresh_token) = TokenService::generate_token_pair(
|
||||
&created_user.id,
|
||||
self.auth_config.access_token_expiration_minutes,
|
||||
self.auth_config.refresh_token_expiration_days,
|
||||
&self.auth_config.jwt_secret,
|
||||
)?;
|
||||
|
||||
// 7. 保存 Refresh Token 到 Redis
|
||||
self.save_refresh_token(&created_user.id, &refresh_token, self.auth_config.refresh_token_expiration_days).await?;
|
||||
|
||||
Ok((created_user, access_token, refresh_token))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**职责边界**:
|
||||
- ✅ 实现业务逻辑
|
||||
- ✅ 协调 Repository 和基础设施
|
||||
- ✅ 事务管理
|
||||
- ❌ 不处理 HTTP 请求/响应
|
||||
- ❌ 不直接访问外部资源(通过 Repository)
|
||||
|
||||
### 3. 数据访问层(repositories/)
|
||||
|
||||
**职责**:封装数据库访问逻辑
|
||||
|
||||
**位置**:`src/repositories/`
|
||||
|
||||
**关键文件**:
|
||||
- `user_repository.rs`:用户数据访问(增删改查)
|
||||
|
||||
**示例**:
|
||||
```rust
|
||||
// src/repositories/user_repository.rs
|
||||
|
||||
pub struct UserRepository {
|
||||
pool: DbPool,
|
||||
}
|
||||
|
||||
impl UserRepository {
|
||||
pub fn new(pool: DbPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
/// 创建用户
|
||||
pub async fn create(&self, user_model: users::Model) -> Result<users::Model> {
|
||||
let result = users::Entity::insert(user_model.into_active_model())
|
||||
.exec(&self.pool)
|
||||
.await
|
||||
.map_err(|e| anyhow!("创建用户失败: {}", e))?;
|
||||
|
||||
Ok(users::Entity::find_by_id(result.last_insert_id))
|
||||
.one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| anyhow!("查询用户失败: {}", e))?
|
||||
.ok_or_else(|| anyhow!("用户不存在"))
|
||||
}
|
||||
|
||||
/// 根据邮箱查询用户
|
||||
pub async fn find_by_email(&self, email: &str) -> Result<Option<users::Model>> {
|
||||
Ok(users::Entity::find()
|
||||
.filter(users::Column::Email.eq(email))
|
||||
.one(&self.pool)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// 根据ID查询用户
|
||||
pub async fn find_by_id(&self, id: &str) -> Result<Option<users::Model>> {
|
||||
Ok(users::Entity::find_by_id(id.to_string())
|
||||
.one(&self.pool)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// 统计相同ID的用户数量
|
||||
pub async fn count_by_id(&self, id: &str) -> Result<u64> {
|
||||
Ok(users::Entity::find_by_id(id.to_string())
|
||||
.count(&self.pool)
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**职责边界**:
|
||||
- ✅ 数据库 CRUD 操作
|
||||
- ✅ 封装 SeaORM 细节
|
||||
- ❌ 不包含业务逻辑
|
||||
- ❌ 不处理 HTTP 请求
|
||||
|
||||
### 4. 领域层(domain/)
|
||||
|
||||
**职责**:定义核心业务模型
|
||||
|
||||
**位置**:`src/domain/`
|
||||
|
||||
#### DTO(Data Transfer Object)
|
||||
|
||||
**职责**:定义 API 请求和响应的数据结构
|
||||
|
||||
**位置**:`src/domain/dto/`
|
||||
|
||||
**示例**:
|
||||
```rust
|
||||
// src/domain/dto/auth.rs
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RegisterRequest {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LoginRequest {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
```
|
||||
|
||||
#### Entities(实体)
|
||||
|
||||
**职责**:定义数据库表模型
|
||||
|
||||
**位置**:`src/domain/entities/`
|
||||
|
||||
**示例**:
|
||||
```rust
|
||||
// src/domain/entities/users.rs
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "users")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: String,
|
||||
#[sea_orm(column_type = "Text", unique)]
|
||||
pub email: String,
|
||||
pub password_hash: String,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
```
|
||||
|
||||
#### VO(View Object)
|
||||
|
||||
**职责**:定义 API 响应的数据结构
|
||||
|
||||
**位置**:`src/domain/vo/`
|
||||
|
||||
**示例**:
|
||||
```rust
|
||||
// src/domain/vo/auth.rs
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RegisterResult {
|
||||
pub email: String,
|
||||
pub created_at: String,
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct LoginResult {
|
||||
pub id: String,
|
||||
pub email: String,
|
||||
pub created_at: String,
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 基础设施层(infra/)
|
||||
|
||||
**职责**:提供技术基础设施
|
||||
|
||||
**位置**:`src/infra/`
|
||||
|
||||
#### 中间件(middleware/)
|
||||
|
||||
**职责**:请求拦截和处理
|
||||
|
||||
**位置**:`src/infra/middleware/`
|
||||
|
||||
**关键文件**:
|
||||
- `auth.rs`:JWT 认证中间件
|
||||
- `logging.rs`:日志中间件
|
||||
|
||||
**示例**:
|
||||
```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..];
|
||||
|
||||
// 3. 验证 JWT
|
||||
let claims = TokenService::decode_user_id(token, &state.config.auth.jwt_secret)?;
|
||||
|
||||
// 4. 将 user_id 添加到请求扩展
|
||||
request.extensions_mut().insert(claims.sub);
|
||||
|
||||
// 5. 继续处理请求
|
||||
Ok(next.run(request).await)
|
||||
}
|
||||
```
|
||||
|
||||
#### Redis 客户端(redis/)
|
||||
|
||||
**职责**:封装 Redis 操作
|
||||
|
||||
**位置**:`src/infra/redis/`
|
||||
|
||||
**关键文件**:
|
||||
- `redis_client.rs`:Redis 客户端封装
|
||||
- `redis_key.rs`:Redis Key 命名规范
|
||||
|
||||
### 6. 工具层(utils/)
|
||||
|
||||
**职责**:提供通用工具函数
|
||||
|
||||
**位置**:`src/utils/`
|
||||
|
||||
**关键文件**:
|
||||
- `jwt.rs`:JWT Token 生成和验证
|
||||
|
||||
**示例**:
|
||||
```rust
|
||||
// src/utils/jwt.rs
|
||||
|
||||
pub struct TokenService;
|
||||
|
||||
impl TokenService {
|
||||
/// 生成 Access Token
|
||||
pub fn generate_access_token(
|
||||
user_id: &str,
|
||||
expiration_minutes: u64,
|
||||
jwt_secret: &str,
|
||||
) -> Result<String> {
|
||||
// ... 生成 JWT Token
|
||||
}
|
||||
|
||||
/// 验证 Token 并提取 user_id
|
||||
pub fn decode_user_id(token: &str, jwt_secret: &str) -> Result<String> {
|
||||
// ... 验证并解码 JWT Token
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据流转
|
||||
|
||||
### 用户注册流程
|
||||
|
||||
```
|
||||
1. 客户端发起 POST /auth/register 请求
|
||||
↓
|
||||
2. handlers/auth.rs::register() 接收请求
|
||||
- 提取请求参数(RegisterRequest)
|
||||
↓
|
||||
3. services/auth_service.rs::register() 处理业务逻辑
|
||||
- 验证邮箱格式
|
||||
- 生成唯一用户 ID
|
||||
- 哈希密码
|
||||
- 创建用户实体
|
||||
↓
|
||||
4. repositories/user_repository.rs::create() 保存到数据库
|
||||
- 使用 SeaORM 插入数据
|
||||
↓
|
||||
5. services/auth_service.rs 生成 Token
|
||||
- 生成 Access Token
|
||||
- 生成 Refresh Token
|
||||
↓
|
||||
6. Redis 保存 Refresh Token
|
||||
↓
|
||||
7. handlers/auth.rs 封装响应(RegisterResult)
|
||||
↓
|
||||
8. 返回 JSON 响应给客户端
|
||||
```
|
||||
|
||||
### 访问受保护接口流程
|
||||
|
||||
```
|
||||
1. 客户端发起 POST /auth/delete 请求
|
||||
- 携带 Authorization: Bearer <access_token>
|
||||
↓
|
||||
2. infra/middleware/auth.rs::auth_middleware() 拦截
|
||||
- 验证 Token 格式
|
||||
- 验证 JWT 签名
|
||||
- 检查 Token 过期时间
|
||||
- 提取 user_id 并添加到请求扩展
|
||||
↓
|
||||
3. handlers/auth.rs::delete_account() 接收请求
|
||||
- 从扩展中提取 user_id
|
||||
↓
|
||||
4. services/auth_service.rs::delete_account() 处理业务逻辑
|
||||
- 验证密码
|
||||
- 调用 Repository 删除用户
|
||||
↓
|
||||
5. repositories/user_repository.rs::delete() 删除数据库记录
|
||||
↓
|
||||
6. 返回响应
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心组件
|
||||
|
||||
### AppState
|
||||
|
||||
**职责**:应用全局状态
|
||||
|
||||
**位置**:`src/main.rs`
|
||||
|
||||
```rust
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub pool: db::DbPool, // 数据库连接池
|
||||
pub config: config::app::AppConfig, // 应用配置
|
||||
pub redis_client: infra::redis::redis_client::RedisClient, // Redis 客户端
|
||||
}
|
||||
```
|
||||
|
||||
**用途**:
|
||||
- 通过 Axum State 机制注入到所有处理器
|
||||
- 提供数据库访问
|
||||
- 提供配置信息
|
||||
- 提供 Redis 访问
|
||||
|
||||
### 路由配置
|
||||
|
||||
**位置**:`src/main.rs`
|
||||
|
||||
```rust
|
||||
// 公开路由
|
||||
let public_routes = Router::new()
|
||||
.route("/health", get(handlers::health::health_check))
|
||||
.route("/info", get(handlers::health::server_info))
|
||||
.route("/auth/register", post(handlers::auth::register))
|
||||
.route("/auth/login", post(handlers::auth::login))
|
||||
.route("/auth/refresh", post(handlers::auth::refresh));
|
||||
|
||||
// 受保护路由
|
||||
let protected_routes = Router::new()
|
||||
.route("/auth/delete", post(handlers::auth::delete_account))
|
||||
.route("//auth/delete-refresh-token", post(handlers::auth::delete_refresh_token))
|
||||
.route_layer(axum::middleware::from_fn_with_state(
|
||||
app_state.clone(),
|
||||
infra::middleware::auth::auth_middleware,
|
||||
));
|
||||
|
||||
// 合并所有路由
|
||||
let app = public_routes
|
||||
.merge(protected_routes)
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any),
|
||||
)
|
||||
.layer(axum::middleware::from_fn(
|
||||
infra::middleware::logging::logging_middleware,
|
||||
));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [DDD 架构规范](ddd-architecture.md) - DDD 设计原则和最佳实践
|
||||
- [代码风格规范](code-style.md) - Rust 代码风格和命名规范
|
||||
- [快速开始指南](getting-started.md) - 安装和运行项目
|
||||
|
||||
---
|
||||
|
||||
**提示**:遵循 DDD 分层架构可以提高代码质量和可维护性。
|
||||
23
docs/sql/init.sql
Normal file
23
docs/sql/init.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
-- ============================================
|
||||
-- Web Template 数据库初始化脚本
|
||||
-- ============================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS `web_template` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE `web_template`;
|
||||
|
||||
-- ============================================
|
||||
-- 1. 用户表
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id VARCHAR(10) PRIMARY KEY COMMENT '10位数字用户ID',
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
created_at DATETIME NOT NULL COMMENT '创建时间',
|
||||
updated_at DATETIME NOT NULL COMMENT '更新时间'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ============================================
|
||||
-- 初始化完成
|
||||
-- ============================================
|
||||
SELECT '✅ 数据库初始化完成' AS status;
|
||||
SHOW TABLES;
|
||||
Reference in New Issue
Block a user