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

View File

@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(cargo check:*)",
"Bash(cargo update:*)",
"Bash(find:*)"
]
}
}

20
.env.example Normal file
View File

@@ -0,0 +1,20 @@
# 环境变量参考配置
#
# ⚠️ 重要提示:
# - 本项目不支持 .env 文件
# - 开发环境和生产环境都请使用 config/ 目录下的 toml 配置文件
# - 环境变量仅用于 Docker/Kubernetes/systemd 等部署场景
#
# ============================================
# ============================================
# CLI 参数环境变量
# ============================================
# 运行环境development, production
# ENV=development
# 调试模式true, false
# DEBUG=false
# 配置文件路径
# CONFIG=config/production.toml

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# Rust
/target/
**/*.rs.bk
*.pdb
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Config (不要提交敏感配置)
.env
db.sqlite*
data
API_FLOW*
SEAORM*

3613
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

55
Cargo.toml Normal file
View File

@@ -0,0 +1,55 @@
[package]
name = "web-rust-template"
version = "0.1.0"
edition = "2021"
[dependencies]
# ===== Web 框架 =====
axum = "0.7"
tokio = { version = "1", features = ["full"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "trace"] }
# ===== 数据库(支持 MySQL、SQLite、PostgreSQL =====
# SeaORM - 数据库 ORM替代 SQLX 直接使用)
sea-orm = { version = "1.1", features = [
"runtime-tokio-rustls",
"sqlx-mysql",
"sqlx-sqlite",
"sqlx-postgres",
"macros",
"with-chrono",
"with-uuid",
] }
# ===== 序列化 =====
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# ===== 认证与加密 =====
jsonwebtoken = "9"
argon2 = "0.5"
sha2 = "0.10"
base64 = "0.22"
# ===== Redis =====
redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] }
# ===== 工具库 =====
uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
anyhow = "1"
thiserror = "1"
async-trait = "0.1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
config = "0.13"
rand = "0.8"
clap = { version = "4", features = ["derive", "env"] }
# 优化发布版本
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
strip = true
panic = "abort"

218
README.md Normal file
View File

@@ -0,0 +1,218 @@
# Web Rust Template
基于 Rust + Axum 0.7 的生产级 Web 服务器模板,采用 DDD 分层架构设计。
## 核心特性
### 架构特色
- **DDD 分层架构**:领域层、基础设施层、应用层清晰分离
- **生产就绪**JWT 双 Token 认证、Argon2 密码哈希、结构化日志
- **多数据库支持**MySQL / PostgreSQL / SQLite 无缝切换
### 技术栈
- **Web 框架**Axum 0.7 + Tokio
- **数据库 ORM**SeaORM 1.1(支持多数据库)
- **认证**JWT (Access Token 15min + Refresh Token 7天)
- **缓存**Redis 存储 Refresh Token
- **安全**Argon2 密码哈希、CORS 支持
## 快速开始
### 1. 克隆并安装依赖
```bash
git clone <repository>
cd web-rust-template
cargo build
```
### 2. 配置项目
**使用默认配置SQLite最简单**
无需配置,直接运行即可:
```bash
cargo run
```
**使用 MySQL/PostgreSQL**
复制对应的配置文件并修改:
```bash
# 使用 MySQL
cp config/development.mysql.toml config/local.toml
# 或使用 PostgreSQL
cp config/development.postgresql.toml config/local.toml
# 编辑 config/local.toml修改数据库连接信息
# 然后运行
cargo run -- -c config/local.toml
```
### 3. 运行服务
```bash
# 使用默认配置SQLite
cargo run
# 或使用指定配置文件
cargo run -- -c config/local.toml
```
服务将在 http://localhost:3000 启动
## 快速测试
### 用户注册
```bash
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "password123"}'
```
响应:
```json
{
"code": 200,
"message": "success",
"data": {
"email": "user@example.com",
"created_at": "2026-02-13T12:00:00.000Z",
"access_token": "eyJ...",
"refresh_token": "eyJ..."
}
}
```
> 查看 [完整 API 文档](docs/api/api-overview.md) 了解所有接口
## 核心配置
### 配置方式
**开发环境**
使用 `config/` 目录下的配置文件:
```bash
# SQLite默认
cargo run
# MySQL
cp config/development.mysql.toml config/local.toml
# 编辑 config/local.toml
cargo run -- -c config/local.toml
# PostgreSQL
cp config/development.postgresql.toml config/local.toml
# 编辑 config/local.toml
cargo run -- -c config/local.toml
```
**生产环境**
使用环境变量或配置文件:
```bash
# 使用环境变量
DATABASE_TYPE=postgresql DATABASE_HOST=localhost cargo run -- -e production
# 或使用配置文件
cargo run -- -e production -c config/production.toml
```
> 查看 [完整配置文档](docs/deployment/configuration.md) 或 [环境变量配置](docs/deployment/environment-variables.md)
## API 接口概览
### 公开接口
- `GET /health` - 健康检查
- `GET /info` - 服务器信息
- `POST /auth/register` - 用户注册
- `POST /auth/login` - 用户登录
- `POST /auth/refresh` - 刷新 Token
### 需要认证的接口
- `POST /auth/delete` - 删除账号
- `POST /auth/delete-refresh-token` - 删除 Refresh Token
> 查看 [完整 API 文档](docs/api/api-overview.md)
## 项目结构
```
src/
├── main.rs # 入口文件
├── config/ # 配置模块
│ ├── app.rs
│ ├── auth.rs
│ ├── database.rs
│ └── redis.rs
├── domain/ # 领域层DDD
│ ├── dto/ # 数据传输对象
│ ├── entities/ # 实体
│ └── vo/ # 视图对象
├── handlers/ # HTTP 处理器层
├── services/ # 业务逻辑层
├── repositories/ # 数据访问层
└── infra/ # 基础设施层
├── middleware/ # 中间件
└── redis/ # Redis 客户端
```
> 查看 [完整项目结构文档](docs/development/project-structure.md)
## 技术栈
| 组件 | 技术 | 版本 |
|------|------|------|
| Web 框架 | Axum | 0.7 |
| 异步运行时 | Tokio | 1.x |
| 数据库 ORM | SeaORM | 1.1 |
| 认证 | JWT | 9.x |
| 密码哈希 | Argon2 | 0.5 |
| 缓存 | Redis | 0.27 |
| 日志 | tracing | 0.1 |
## 文档导航
- [API 接口文档](docs/api/api-overview.md) - 完整的 API 接口说明和示例
- [快速开始指南](docs/development/getting-started.md) - 详细的安装和配置指南
- [开发规范](docs/development/ddd-architecture.md) - DDD 架构和代码规范
- [部署文档](docs/deployment/configuration.md) - 配置和部署指南
## 日志格式
日志采用三段式结构:
1. 📥 **请求开始**:显示请求方法和路径
2. 🔧 **请求处理**:显示请求参数和响应内容
3.**请求完成**:显示状态码和耗时
示例:
```
================================================================================
GET /health
================================================================================
[uuid-...] 📥 查询参数: 无 | 时间: 2026-02-12 13:30:45.123
[uuid-...] ✅ 状态码: 200 | 耗时: 5ms
================================================================================
```
## 安全特性
- ✅ 密码使用 Argon2 哈希
- ✅ JWT Token 认证
- ✅ Refresh Token 轮换机制
- ✅ Token 过期时间可配置
- ✅ 密码验证后才删除账号
## 许可证
MIT

33
config/development.toml Normal file
View File

@@ -0,0 +1,33 @@
# 开发环境配置 - SQLite 数据库
[server]
host = "0.0.0.0"
port = 3000
[database]
# 数据库类型: mysql, sqlite, postgresql
database_type = "sqlite"
# MySQL/PostgreSQL 配置
# host = "localhost"
# port = 3306
# user = "root"
# password = "root"
# database = "web_template"
# SQLite 配置(当 database_type = "sqlite" 时使用)
path = "data/app.db"
# 连接池配置
max_connections = 10
[auth]
jwt_secret = "9f7d3c7a564dfkopp26smb2644nqzfvbsao9f7d3c7a1a8f28544b5e6d7a"
# 分开配置两个 token 的过期时间
access_token_expiration_minutes = 15 # access_token 15 分钟
refresh_token_expiration_days = 7 # refresh_token 7 天
[redis]
host = "localhost"
port = 6379
password = "" # 可选
db = 0

33
config/production.toml Normal file
View File

@@ -0,0 +1,33 @@
# 生产环境配置 - PostgreSQL 数据库
[server]
host = "0.0.0.0" # 服务器监听地址0.0.0.0=允许所有网络访问)
port = 3000 # 服务器监听端口(确保防火墙已开放)
[database]
database_type = "postgresql" # 数据库类型sqlite/mysql/postgresql
host = "localhost" # PostgreSQL 服务器地址
port = 5432 # PostgreSQL 端口(默认 5432
user = "postgres" # PostgreSQL 用户名(请创建专用用户)
password = "postgres" # PostgreSQL 密码(请修改为强密码)
database = "web_template" # 数据库名称(不存在会自动创建)
max_connections = 20 # 最大连接数(生产环境建议 20-100
[auth]
jwt_secret = "9f7d3c7a564dfkopp26smb2644nqzfvbsao9f7d3c7a1a8f28544b5e6d7a" # JWT 签名密钥(必须修改为强随机字符串!)
access_token_expiration_minutes = 15 # Access Token 过期时间(分钟)
refresh_token_expiration_days = 7 # Refresh Token 过期时间(天)
[redis]
host = "localhost" # Redis 服务器地址
port = 6379 # Redis 端口(默认 6379
password = "" # Redis 密码(强烈建议设置密码)
db = 0 # Redis 数据库编号0-15
# 安全检查清单:部署前请确认
# ✅ 1. 已修改 jwt_secret 为强随机字符串
# ✅ 2. 已修改数据库密码为强密码
# ✅ 3. 已设置 Redis 密码
# ✅ 4. 已配置防火墙规则
# ✅ 5. 已启用 HTTPS使用 Nginx/Caddy 等反向代理)
# ✅ 6. 已设置数据库定期备份

116
docs/README.md Normal file
View 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
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) - 需要认证的接口
---
**提示**:以上示例代码仅供参考,实际使用时请根据项目需求调整。

View 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` 文件到版本控制。

View 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 到项目仓库。

View 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/`
#### DTOData 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,
}
```
#### VOView 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
View 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;

234
src/cli.rs Normal file
View File

@@ -0,0 +1,234 @@
/// 命令行参数和配置管理
/// 支持优先级CLI 参数 > 环境变量 > 配置文件 > 默认值
use clap::{Parser, ValueEnum};
use std::path::PathBuf;
/// 运行环境(强类型)
#[derive(ValueEnum, Clone, Debug)]
pub enum Environment {
/// 开发环境
Development,
/// 生产环境
Production,
}
impl Environment {
/// 转换为小写字符串
pub fn as_str(&self) -> &'static str {
match self {
Environment::Development => "development",
Environment::Production => "production",
}
}
}
/// 命令行参数
#[derive(Parser, Debug)]
#[command(name = "web-rust-template")]
#[command(about = "Web Server Template", long_about = None)]
#[command(author = "Your Name <your.email@example.com>")]
#[command(version = "0.1.0")]
#[command(propagate_version = true)]
pub struct CliArgs {
/// 指定配置文件路径
///
/// 支持相对路径和绝对路径
/// 例如:-c config/production.toml
#[arg(short, long, value_name = "FILE")]
pub config: Option<PathBuf>,
/// 指定运行环境
///
/// 自动加载对应环境的配置文件(如 config/development.toml
/// 可通过环境变量 ENV 设置
#[arg(
short = 'e',
long,
value_enum,
env = "ENV",
default_value = "development"
)]
pub env: Environment,
/// 指定服务器监听端口
///
/// 覆盖配置文件中的 port 设置
/// 可通过环境变量 SERVER_PORT 设置
#[arg(short, long, global = true, env = "SERVER_PORT")]
pub port: Option<u16>,
/// 指定服务器监听地址
///
/// 覆盖配置文件中的 host 设置
/// 可通过环境变量 SERVER_HOST 设置
#[arg(long, global = true, env = "SERVER_HOST")]
pub host: Option<String>,
/// 启用调试日志
///
/// 输出详细的日志信息,包括 SQL 查询
/// 可通过环境变量 DEBUG 设置
/// 注意:与 -v 冲突,推荐使用 -v/-vv/-vvv
#[arg(long, global = true, env = "DEBUG", conflicts_with = "verbose")]
pub debug: bool,
/// 工作目录
///
/// 指定配置文件和数据库的基准目录
#[arg(short, long, global = true)]
pub work_dir: Option<PathBuf>,
/// 显示详细日志(多级 verbose
///
/// -v : info 级别日志
/// -vv : debug 级别日志(等同于 --debug
/// -vvv : trace 级别日志(最详细)
#[arg(short, long, global = true, action = clap::ArgAction::Count)]
pub verbose: u8,
}
impl CliArgs {
/// 获取是否启用调试
pub fn is_debug_enabled(&self) -> bool {
self.debug || self.verbose >= 2
}
/// 获取日志级别
pub fn get_log_level(&self) -> &'static str {
if self.debug {
return "debug";
}
match self.verbose {
0 => "info",
1 => "debug",
_ => "trace",
}
}
/// 获取环境变量的日志过滤器(工程化版本)
pub fn get_log_filter(&self) -> String {
let level = self.get_log_level();
match level {
"trace" => "web_rust_template=trace,tower_http=trace,axum=trace,sqlx=debug".into(),
"debug" => "web_rust_template=debug,tower_http=debug,axum=debug,sqlx=debug".into(),
_ => "web_rust_template=info,tower_http=info,axum=info".into(),
}
}
/// 获取配置文件路径
///
/// 优先级:
/// 1. CLI 参数 --config
/// 2. 环境变量 CONFIG
/// 3. {work_dir}/config/{env}.toml
/// 4. ./config/{env}.toml
/// 5. ./config/default.toml
///
/// 如果找不到配置文件,返回 None允许仅使用环境变量运行
pub fn resolve_config_path(&self) -> Option<PathBuf> {
use std::env;
// 1. CLI 参数优先
if let Some(ref config) = self.config {
if config.exists() {
return Some(config.clone());
}
eprintln!("⚠ 警告:指定的配置文件不存在: {}", config.display());
eprintln!(" 将仅使用环境变量运行");
return None;
}
// 2. 环境变量
if let Ok(config_path) = env::var("CONFIG") {
let config = PathBuf::from(&config_path);
if config.exists() {
return Some(config);
}
eprintln!("⚠ 警告:环境变量 CONFIG 指定的配置文件不存在: {}", config_path);
eprintln!(" 将仅使用环境变量运行");
return None;
}
// 3-6. 查找配置文件
let work_dir = self
.work_dir
.clone()
.or_else(|| env::current_dir().ok())
.unwrap_or_else(|| PathBuf::from("."));
let env_name = self.env.as_str();
// 按优先级尝试的位置
let candidates = [
// 工作目录下的环境配置
work_dir.join("config").join(format!("{}.toml", env_name)),
// 当前目录的环境配置
PathBuf::from(format!("config/{}.toml", env_name)),
// 工作目录下的默认配置
work_dir.join("config").join("default.toml"),
// 当前目录的默认配置
PathBuf::from("config/default.toml"),
];
for candidate in &candidates {
if candidate.exists() {
// 使用 println! 而非 tracing::info!
println!("✓ Found configuration file: {}", candidate.display());
return Some(candidate.clone());
}
}
// 所有候选路径都找不到配置文件,返回 None
eprintln!(" 未找到配置文件,将仅使用环境变量和默认值");
None
}
/// 获取覆盖配置
///
/// CLI 参数可以覆盖配置文件中的值(仅 Web 服务器参数)
pub fn get_overrides(&self) -> ConfigOverrides {
ConfigOverrides {
host: self.host.clone(),
port: self.port,
}
}
/// 显示启动信息(工程化版本:打印实际解析的配置)
///
/// 使用 println! 而非 tracing::info!,因为 logger 可能尚未初始化
pub fn print_startup_info(&self) {
let separator = "=".repeat(60);
println!("{}", separator);
println!("Web Rust Template Server v0.1.0");
println!("Environment: {}", self.env.as_str());
// 打印实际解析的配置路径(而非 CLI 参数)
if let Some(config_path) = self.resolve_config_path() {
println!("Config file: {}", config_path.display());
} else {
println!("Config file: None (using environment variables)");
}
if let Some(ref work_dir) = self.work_dir {
println!("Work directory: {}", work_dir.display());
}
// 打印实际的日志级别
println!("Log level: {}", self.get_log_level());
if self.is_debug_enabled() {
println!("Debug mode: ENABLED");
}
println!("{}", separator);
}
}
/// CLI 参数覆盖的配置(仅 Web 服务器参数)
#[derive(Debug, Clone)]
pub struct ConfigOverrides {
/// Web 服务器主机覆盖
pub host: Option<String>,
/// Web 服务器端口覆盖
pub port: Option<u16>,
}

135
src/config/app.rs Normal file
View File

@@ -0,0 +1,135 @@
use super::{auth::AuthConfig, database::DatabaseConfig, redis::RedisConfig, server::ServerConfig};
use config::{Config, ConfigError, Environment, File};
use serde::Deserialize;
use std::path::PathBuf;
// 导入 redis 默认值函数(使用完整路径)
use crate::config::redis::default_redis_host;
#[derive(Debug, Deserialize, Clone)]
pub struct AppConfig {
pub server: ServerConfig,
pub database: DatabaseConfig,
pub auth: AuthConfig,
pub redis: RedisConfig,
}
impl AppConfig {
/// 加载配置(支持 CLI 覆盖)
///
/// 如果 config_path 为 None则仅使用环境变量和默认值
pub fn load_with_overrides(
cli_config_path: Option<std::path::PathBuf>,
overrides: crate::cli::ConfigOverrides,
_environment: &str,
) -> Result<Self, ConfigError> {
// 使用 ConfigBuilder 设置配置
let mut builder = Config::builder();
// 如果提供了配置文件,先加载它
if let Some(config_path) = cli_config_path {
if !config_path.exists() {
tracing::error!("Configuration file not found: {}", config_path.display());
return Err(ConfigError::NotFound(
config_path.to_string_lossy().to_string(),
));
}
tracing::info!("Loading configuration from: {}", config_path.display());
builder = builder.add_source(File::from(config_path));
} else {
tracing::info!("No configuration file found, using environment variables and defaults");
tracing::warn!("⚠️ 没有找到配置文件,将使用 SQLite 作为默认数据库");
tracing::warn!(" 默认数据库路径: db.sqlite3");
tracing::warn!(" 如需使用其他数据库,请创建配置文件或设置环境变量");
// 直接使用 set_default 设置默认值
// 注意:这些值会被环境变量覆盖
builder = builder.set_default("server.host", default_server_host())?;
builder = builder.set_default("server.port", default_server_port())?;
// 设置 database 默认值(使用 SQLite 作为默认数据库)
builder = builder.set_default("database.database_type", "sqlite")?;
builder = builder.set_default("database.path", "db.sqlite3")?;
builder = builder.set_default("database.max_connections", 10)?;
// 设置 auth 默认值
builder = builder.set_default("auth.jwt_secret", default_jwt_secret())?;
builder = builder.set_default("auth.access_token_expiration_minutes", 15)?;
builder = builder.set_default("auth.refresh_token_expiration_days", 7)?;
// 设置 redis 默认值
builder = builder.set_default("redis.host", default_redis_host())?;
builder = builder.set_default("redis.port", 6379)?;
builder = builder.set_default("redis.db", 0)?;
}
// 添加环境变量源(会覆盖配置文件的值)
builder = builder.add_source(Environment::default().separator("_"));
// 应用 CLI 覆盖(仅 Web 服务器参数)
if let Some(host) = overrides.host {
builder = builder.set_override("server.host", host)?;
}
if let Some(port) = overrides.port {
builder = builder.set_override("server.port", port)?;
}
let settings = builder.build()?;
let config: AppConfig = settings.try_deserialize()?;
// 安全警告:检查是否使用了默认的 JWT 密钥
if config.auth.jwt_secret == "change-this-to-a-strong-secret-key-in-production" {
tracing::warn!("⚠️ 警告:正在使用不安全的默认 JWT 密钥!");
tracing::warn!(" 请通过环境变量 AUTH_JWT_SECRET 或配置文件设置强密钥");
tracing::warn!(" 示例AUTH_JWT_SECRET=your-secure-random-string-here");
}
// 验证数据库配置
if let Err(e) = config.database.validate() {
tracing::error!("数据库配置无效: {}", e);
return Err(ConfigError::Message(format!("数据库配置无效: {}", e)));
}
Ok(config)
}
/// 从指定路径加载配置
pub fn load_from_path(path: &str) -> Result<Self, ConfigError> {
tracing::info!("Loading configuration from: {}", path);
let settings = Config::builder()
.add_source(File::from(PathBuf::from(path)))
.add_source(Environment::default().separator("_"))
.build()?;
let config: AppConfig = settings.try_deserialize()?;
// 安全警告:检查是否使用了默认的 JWT 密钥
if config.auth.jwt_secret == "change-this-to-a-strong-secret-key-in-production" {
tracing::warn!("⚠️ 警告:正在使用不安全的默认 JWT 密钥!");
tracing::warn!(" 请通过环境变量 AUTH_JWT_SECRET 或配置文件设置强密钥");
tracing::warn!(" 示例AUTH_JWT_SECRET=your-secure-random-string-here");
}
// 验证数据库配置
if let Err(e) = config.database.validate() {
tracing::error!("数据库配置无效: {}", e);
return Err(ConfigError::Message(format!("数据库配置无效: {}", e)));
}
Ok(config)
}
}
// 默认值函数(复用)
fn default_server_host() -> String {
"127.0.0.1".to_string()
}
fn default_server_port() -> u16 {
3000
}
fn default_jwt_secret() -> String {
"change-this-to-a-strong-secret-key-in-production".to_string()
}

25
src/config/auth.rs Normal file
View File

@@ -0,0 +1,25 @@
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
pub struct AuthConfig {
#[serde(default = "default_jwt_secret")]
pub jwt_secret: String,
#[serde(default = "default_access_token_expiration_minutes")]
pub access_token_expiration_minutes: u64,
#[serde(default = "default_refresh_token_expiration_days")]
pub refresh_token_expiration_days: i64,
}
fn default_jwt_secret() -> String {
// ⚠️ 警告:这是一个不安全的默认值,仅用于开发测试
// 生产环境必须通过环境变量或配置文件设置强密钥
"change-this-to-a-strong-secret-key-in-production".to_string()
}
fn default_access_token_expiration_minutes() -> u64 {
15
}
fn default_refresh_token_expiration_days() -> i64 {
7
}

168
src/config/database.rs Normal file
View File

@@ -0,0 +1,168 @@
use serde::Deserialize;
use std::path::PathBuf;
/// 数据库类型
#[derive(Debug, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum DatabaseType {
MySQL,
SQLite,
PostgreSQL,
}
#[derive(Debug, Deserialize, Clone)]
pub struct DatabaseConfig {
/// 数据库类型
#[serde(default = "default_database_type")]
pub database_type: DatabaseType,
/// 网络数据库配置MySQL/PostgreSQL
pub host: Option<String>,
#[serde(default)]
pub port: Option<u16>,
pub user: Option<String>,
pub password: Option<String>,
pub database: Option<String>,
/// SQLite 文件路径
pub path: Option<PathBuf>,
/// 连接池最大连接数
#[serde(default = "default_max_connections")]
pub max_connections: u32,
}
impl DatabaseConfig {
/// 获取端口号(根据数据库类型返回默认值)
pub fn get_port(&self) -> u16 {
self.port.unwrap_or_else(|| match self.database_type {
DatabaseType::MySQL => 3306,
DatabaseType::PostgreSQL => 5432,
DatabaseType::SQLite => 0,
})
}
/// 构建数据库连接 URL
///
/// # 错误
///
/// 当缺少必需的配置字段时返回错误
pub fn build_url(&self) -> Result<String, String> {
match self.database_type {
DatabaseType::MySQL => {
let host = self
.host
.as_ref()
.ok_or_else(|| "MySQL 需要配置 database.host".to_string())?;
let user = self
.user
.as_ref()
.ok_or_else(|| "MySQL 需要配置 database.user".to_string())?;
let password = self
.password
.as_ref()
.ok_or_else(|| "MySQL 需要配置 database.password".to_string())?;
let database = self
.database
.as_ref()
.ok_or_else(|| "MySQL 需要配置 database.database".to_string())?;
Ok(format!(
"mysql://{}:{}@{}:{}/{}",
user,
password,
host,
self.get_port(),
database
))
}
DatabaseType::SQLite => {
let path = self
.path
.as_ref()
.ok_or_else(|| "SQLite 需要配置 database.path".to_string())?;
// SQLite URL 格式
// 相对路径sqlite:./db.sqlite3
// 绝对路径sqlite:C:/path/to/db.sqlite3
let path_str = path.to_string_lossy().replace('\\', "/");
Ok(format!("sqlite:{}", path_str))
}
DatabaseType::PostgreSQL => {
let host = self
.host
.as_ref()
.ok_or_else(|| "PostgreSQL 需要配置 database.host".to_string())?;
let user = self
.user
.as_ref()
.ok_or_else(|| "PostgreSQL 需要配置 database.user".to_string())?;
let password = self
.password
.as_ref()
.ok_or_else(|| "PostgreSQL 需要配置 database.password".to_string())?;
let database = self
.database
.as_ref()
.ok_or_else(|| "PostgreSQL 需要配置 database.database".to_string())?;
Ok(format!(
"postgresql://{}:{}@{}:{}/{}",
user,
password,
host,
self.get_port(),
database
))
}
}
}
/// 验证配置是否完整
pub fn validate(&self) -> Result<(), String> {
match self.database_type {
DatabaseType::MySQL => {
if self.host.is_none() {
return Err("MySQL 需要配置 database.host".to_string());
}
if self.user.is_none() {
return Err("MySQL 需要配置 database.user".to_string());
}
if self.password.is_none() {
return Err("MySQL 需要配置 database.password".to_string());
}
if self.database.is_none() {
return Err("MySQL 需要配置 database.database".to_string());
}
}
DatabaseType::SQLite => {
if self.path.is_none() {
return Err("SQLite 需要配置 database.path".to_string());
}
}
DatabaseType::PostgreSQL => {
if self.host.is_none() {
return Err("PostgreSQL 需要配置 database.host".to_string());
}
if self.user.is_none() {
return Err("PostgreSQL 需要配置 database.user".to_string());
}
if self.password.is_none() {
return Err("PostgreSQL 需要配置 database.password".to_string());
}
if self.database.is_none() {
return Err("PostgreSQL 需要配置 database.database".to_string());
}
}
}
Ok(())
}
}
fn default_database_type() -> DatabaseType {
DatabaseType::MySQL
}
fn default_max_connections() -> u32 {
10
}

5
src/config/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod app;
pub mod auth;
pub mod database;
pub mod redis;
pub mod server;

52
src/config/redis.rs Normal file
View File

@@ -0,0 +1,52 @@
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
pub struct RedisConfig {
/// Redis 主机地址
#[serde(default = "default_redis_host")]
pub host: String,
/// Redis 端口
#[serde(default = "default_redis_port")]
pub port: u16,
/// Redis 密码(可选)
#[serde(default)]
pub password: Option<String>,
/// Redis 数据库编号(可选)
#[serde(default = "default_redis_db")]
pub db: u8,
}
pub fn default_redis_host() -> String {
"localhost".to_string()
}
pub fn default_redis_port() -> u16 {
6379
}
pub fn default_redis_db() -> u8 {
0
}
impl RedisConfig {
/// 构建 Redis 连接 URL
pub fn build_url(&self) -> String {
// 判断密码是否存在且非空
match &self.password {
Some(password) if !password.is_empty() => {
// 有密码redis://:password@host:port/db
format!(
"redis://:{}@{}:{}/{}",
password, self.host, self.port, self.db
)
}
_ => {
// 无密码None 或空字符串redis://host:port/db
format!("redis://{}:{}/{}", self.host, self.port, self.db)
}
}
}
}

17
src/config/server.rs Normal file
View File

@@ -0,0 +1,17 @@
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
pub struct ServerConfig {
#[serde(default = "default_server_host")]
pub host: String,
#[serde(default = "default_server_port")]
pub port: u16,
}
fn default_server_host() -> String {
"127.0.0.1".to_string()
}
fn default_server_port() -> u16 {
3000
}

329
src/db.rs Normal file
View File

@@ -0,0 +1,329 @@
use crate::config::database::{DatabaseConfig, DatabaseType};
use sea_orm::{
ConnectionTrait, Database, DatabaseConnection, DbBackend, EntityName, EntityTrait, ConnectOptions, Schema,
Statement,
};
use std::time::Duration;
/// 数据库连接池SeaORM 统一接口)
pub type DbPool = DatabaseConnection;
/// 创建数据库连接池
pub async fn create_pool(config: &DatabaseConfig) -> anyhow::Result<DbPool> {
let url = config
.build_url()
.map_err(|e| anyhow::anyhow!("数据库配置错误: {}", e))?;
tracing::debug!("数据库连接 URL: {}", url);
let mut opt = ConnectOptions::new(&url);
opt.max_connections(config.max_connections)
.min_connections(1)
.connect_timeout(Duration::from_secs(8))
.idle_timeout(Duration::from_secs(8))
.max_lifetime(Duration::from_secs(7200))
.sqlx_logging(true);
let pool = Database::connect(opt)
.await
.map_err(|e| anyhow::anyhow!("数据库连接失败: {}", e))?;
tracing::info!("已连接到数据库: {}", sanitize_url(&url));
Ok(pool)
}
/// 隐藏 URL 中的敏感信息(用于日志输出)
fn sanitize_url(url: &str) -> String {
// 隐藏密码mysql://user:password@host -> mysql://user:***@host
if let Some(at_pos) = url.find('@') {
if let Some(scheme_end) = url.find("://") {
if scheme_end < at_pos {
return format!("{}***@{}", &url[..scheme_end + 3], &url[at_pos + 1..]);
}
}
}
url.to_string()
}
/// 健康检查(保持向后兼容)
pub async fn health_check(pool: &DbPool) -> anyhow::Result<()> {
// 使用官方推荐的 ping 方法
pool.ping()
.await
.map_err(|e| anyhow::anyhow!("数据库健康检查失败: {}", e))
}
/// 初始化数据库和表结构
/// 每次启动时检查数据库和表是否存在,不存在则创建
pub async fn init_database(config: &DatabaseConfig) -> anyhow::Result<DatabaseConnection> {
match config.database_type {
DatabaseType::MySQL => {
init_mysql_database(config).await?;
}
DatabaseType::PostgreSQL => {
init_postgresql_database(config).await?;
}
DatabaseType::SQLite => {
// 确保 SQLite 数据库文件的目录存在
init_sqlite_database(config).await?;
}
}
// 连接到数据库
let pool = create_pool(config).await?;
// 创建表
create_tables(&pool).await?;
Ok(pool)
}
/// 获取端口号(根据数据库类型返回默认值)
fn get_database_port(config: &DatabaseConfig) -> u16 {
config.port.unwrap_or_else(|| match config.database_type {
DatabaseType::MySQL => 3306,
DatabaseType::PostgreSQL => 5432,
DatabaseType::SQLite => 0,
})
}
/// 为 MySQL 创建数据库(如果不存在)
async fn init_mysql_database(config: &DatabaseConfig) -> anyhow::Result<()> {
let database_name = config
.database
.as_ref()
.ok_or_else(|| anyhow::anyhow!("MySQL 需要配置 database.database"))?;
let host = config
.host
.as_ref()
.ok_or_else(|| anyhow::anyhow!("MySQL 需要配置 database.host"))?;
let user = config
.user
.as_ref()
.ok_or_else(|| anyhow::anyhow!("MySQL 需要配置 database.user"))?;
let password = config
.password
.as_ref()
.ok_or_else(|| anyhow::anyhow!("MySQL 需要配置 database.password"))?;
// 连接到 MySQL 服务器(不指定数据库)
let url = format!(
"mysql://{}:{}@{}:{}",
user,
password,
host,
get_database_port(config)
);
let mut opt = ConnectOptions::new(&url);
opt.max_connections(1)
.connect_timeout(Duration::from_secs(8))
.sqlx_logging(true);
let conn = Database::connect(opt)
.await
.map_err(|e| anyhow::anyhow!("连接 MySQL 服务器失败: {}", e))?;
// 检查数据库是否存在,不存在则创建
let query = format!(
"CREATE DATABASE IF NOT EXISTS `{}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci",
database_name
);
conn.execute(Statement::from_string(
sea_orm::DatabaseBackend::MySql,
query,
))
.await
.map_err(|e| anyhow::anyhow!("创建 MySQL 数据库失败: {}", e))?;
tracing::info!("✅ MySQL 数据库 '{}' 检查完成", database_name);
Ok(())
}
/// 为 PostgreSQL 创建数据库(如果不存在)
async fn init_postgresql_database(config: &DatabaseConfig) -> anyhow::Result<()> {
let database_name = config
.database
.as_ref()
.ok_or_else(|| anyhow::anyhow!("PostgreSQL 需要配置 database.database"))?;
let host = config
.host
.as_ref()
.ok_or_else(|| anyhow::anyhow!("PostgreSQL 需要配置 database.host"))?;
let user = config
.user
.as_ref()
.ok_or_else(|| anyhow::anyhow!("PostgreSQL 需要配置 database.user"))?;
let password = config
.password
.as_ref()
.ok_or_else(|| anyhow::anyhow!("PostgreSQL 需要配置 database.password"))?;
// 连接到 PostgreSQL 默认数据库postgres
let url = format!(
"postgresql://{}:{}@{}:{}/postgres",
user,
password,
host,
get_database_port(config)
);
let mut opt = ConnectOptions::new(&url);
opt.max_connections(1)
.connect_timeout(Duration::from_secs(8))
.sqlx_logging(true);
let conn = Database::connect(opt)
.await
.map_err(|e| anyhow::anyhow!("连接 PostgreSQL 服务器失败: {}", e))?;
// 检查数据库是否存在,不存在则创建
// PostgreSQL 不支持 CREATE DATABASE IF NOT EXISTS需要先查询
let check_query = format!(
"SELECT 1 FROM pg_database WHERE datname='{}'",
database_name
);
let result = conn
.execute(Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
check_query,
))
.await;
match result {
Ok(_) => {
tracing::info!("PostgreSQL 数据库 '{}' 已存在", database_name);
}
Err(_) => {
// 数据库不存在,创建它
let create_query = format!(
"CREATE DATABASE {} WITH ENCODING 'UTF8' LC_COLLATE='en_US.UTF-8' LC_CTYPE='en_US.UTF-8'",
database_name
);
conn.execute(Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
create_query,
))
.await
.map_err(|e| anyhow::anyhow!("创建 PostgreSQL 数据库失败: {}", e))?;
tracing::info!("✅ PostgreSQL 数据库 '{}' 创建成功", database_name);
}
}
Ok(())
}
/// 为 SQLite 确保数据库文件目录存在
async fn init_sqlite_database(config: &DatabaseConfig) -> anyhow::Result<()> {
let path = config
.path
.as_ref()
.ok_or_else(|| anyhow::anyhow!("SQLite 需要配置 database.path"))?;
// 如果是相对路径,转换为绝对路径
let absolute_path = if path.is_absolute() {
path.clone()
} else {
std::env::current_dir()
.map_err(|e| anyhow::anyhow!("获取当前目录失败: {}", e))?
.join(path)
};
tracing::info!("SQLite 数据库路径: {}", absolute_path.display());
// 获取数据库文件的父目录
if let Some(parent) = absolute_path.parent() {
// 如果父目录不存在,则创建
if !parent.exists() {
std::fs::create_dir_all(parent)
.map_err(|e| anyhow::anyhow!("创建 SQLite 数据库目录失败: {}", e))?;
tracing::info!("✅ SQLite 数据库目录创建成功: {}", parent.display());
} else {
tracing::info!("SQLite 数据库目录已存在: {}", parent.display());
}
}
// 如果数据库文件不存在,创建空文件
if !absolute_path.exists() {
std::fs::File::create(&absolute_path)
.map_err(|e| anyhow::anyhow!("创建 SQLite 数据库文件失败: {}", e))?;
tracing::info!("✅ SQLite 数据库文件创建成功: {}", absolute_path.display());
} else {
tracing::info!("SQLite 数据库文件已存在: {}", absolute_path.display());
}
Ok(())
}
/// 辅助函数:创建单个表(如果不存在)
async fn create_single_table<E>(
db: &DatabaseConnection,
schema: &Schema,
builder: &DbBackend,
entity: E,
table_name: &str,
) -> anyhow::Result<()>
where
E: EntityName + EntityTrait,
{
let create_table = schema.create_table_from_entity(entity);
let sql = match builder {
DbBackend::MySql => {
use sea_orm::sea_query::MysqlQueryBuilder;
create_table.to_string(MysqlQueryBuilder {})
}
DbBackend::Postgres => {
use sea_orm::sea_query::PostgresQueryBuilder;
create_table.to_string(PostgresQueryBuilder {})
}
DbBackend::Sqlite => {
use sea_orm::sea_query::SqliteQueryBuilder;
create_table.to_string(SqliteQueryBuilder {})
}
};
let sql = sql.replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS");
match db.execute(Statement::from_string(*builder, sql)).await {
Ok(_) => {
tracing::info!("✅ {}检查完成", table_name);
}
Err(e) => {
let err_msg = e.to_string();
if err_msg.contains("already exists") || (err_msg.contains("table") && err_msg.contains("exists")) {
tracing::info!("✅ {}已存在", table_name);
} else {
return Err(anyhow::anyhow!("创建{}失败: {}", table_name, e));
}
}
}
Ok(())
}
/// 创建数据库表结构
async fn create_tables(db: &DatabaseConnection) -> anyhow::Result<()> {
tracing::info!("检查数据库表结构...");
let builder = db.get_database_backend();
let schema = Schema::new(builder);
// 导入所有 entities
use crate::domain::entities::users;
// 创建所有表(添加新表只需一行!)
create_single_table(db, &schema, &builder, users::Entity, "用户表").await?;
tracing::info!("✅ 数据库表结构检查完成");
Ok(())
}

57
src/domain/dto/auth.rs Normal file
View File

@@ -0,0 +1,57 @@
use serde::Deserialize;
use std::fmt;
/// 注册请求
#[derive(Deserialize)]
pub struct RegisterRequest {
pub email: String,
pub password: String,
}
// 实现 Debug trait对密码进行脱敏
impl fmt::Debug for RegisterRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "RegisterRequest {{ email: {}, password: *** }}", self.email)
}
}
/// 登录请求
#[derive(Deserialize)]
pub struct LoginRequest {
pub email: String,
pub password: String,
}
// 实现 Debug trait
impl fmt::Debug for LoginRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "LoginRequest {{ email: {}, password: *** }}", self.email)
}
}
/// 删除用户请求
#[derive(Deserialize)]
pub struct DeleteUserRequest {
pub user_id: String,
pub password: String,
}
// 实现 Debug trait
impl fmt::Debug for DeleteUserRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DeleteUserRequest {{ user_id: {}, password: *** }}", self.user_id)
}
}
/// 刷新令牌请求
#[derive(Deserialize)]
pub struct RefreshRequest {
pub refresh_token: String,
}
// RefreshRequest 的 refresh_token 是敏感字段,需要脱敏
impl fmt::Debug for RefreshRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "RefreshRequest {{ refresh_token: *** }}")
}
}

1
src/domain/dto/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod auth;

1
src/domain/dto/user.rs Normal file
View File

@@ -0,0 +1 @@
// 用户相关 DTO预留

View File

@@ -0,0 +1,2 @@
pub mod users;

View File

@@ -0,0 +1,41 @@
use sea_orm::entity::prelude::*;
use sea_orm::Set;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "users")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(unique)]
pub email: String,
pub password_hash: String,
pub created_at: DateTime,
pub updated_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
#[async_trait::async_trait]
impl ActiveModelBehavior for ActiveModel {
/// 在保存前自动填充时间戳
async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
let mut this = self;
let now = chrono::Utc::now().naive_utc();
if insert {
// 插入时:设置创建时间和更新时间
this.created_at = Set(now);
this.updated_at = Set(now);
} else {
// 更新时:只更新更新时间
this.updated_at = Set(now);
}
Ok(this)
}
}

3
src/domain/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod dto;
pub mod vo;
pub mod entities;

50
src/domain/vo/auth.rs Normal file
View File

@@ -0,0 +1,50 @@
use serde::Serialize;
/// 注册结果
#[derive(Debug, Serialize)]
pub struct RegisterResult {
pub email: String,
pub created_at: String, // ISO 8601 格式
pub access_token: String,
pub refresh_token: String,
}
impl From<(crate::domain::entities::users::Model, String, String)> for RegisterResult {
fn from((user_model, access_token, refresh_token): (crate::domain::entities::users::Model, String, String)) -> Self {
Self {
email: user_model.email,
created_at: user_model.created_at.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
access_token,
refresh_token,
}
}
}
/// 登录结果
#[derive(Debug, Serialize)]
pub struct LoginResult {
pub id: String,
pub email: String,
pub created_at: String, // ISO 8601 格式
pub access_token: String,
pub refresh_token: String,
}
impl From<(crate::domain::entities::users::Model, String, String)> for LoginResult {
fn from((user_model, access_token, refresh_token): (crate::domain::entities::users::Model, String, String)) -> Self {
Self {
id: user_model.id,
email: user_model.email,
created_at: user_model.created_at.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
access_token,
refresh_token,
}
}
}
/// 刷新 Token 结果
#[derive(Debug, Serialize)]
pub struct RefreshResult {
pub access_token: String,
pub refresh_token: String,
}

56
src/domain/vo/mod.rs Normal file
View File

@@ -0,0 +1,56 @@
pub mod auth;
pub mod user;
/// 统一的 API 响应结构
use serde::Serialize;
use axum::http::StatusCode;
#[derive(Debug, Serialize)]
pub struct ApiResponse<T> {
/// HTTP 状态码
pub code: u16,
/// 响应消息
pub message: String,
/// 响应数据
pub data: Option<T>,
}
impl<T: Serialize> ApiResponse<T> {
/// 成功响应200
pub fn success(data: T) -> Self {
Self {
code: 200,
message: "Success".to_string(),
data: Some(data),
}
}
/// 成功响应(自定义消息)
pub fn success_with_message(data: T, message: &str) -> Self {
Self {
code: 200,
message: message.to_string(),
data: Some(data),
}
}
/// 错误响应
#[allow(dead_code)]
pub fn error(status_code: StatusCode, message: &str) -> ApiResponse<()> {
ApiResponse {
code: status_code.as_u16(),
message: message.to_string(),
data: None,
}
}
/// 错误响应(带数据)
#[allow(dead_code)]
pub fn error_with_data(status_code: StatusCode, message: &str, data: T) -> ApiResponse<T> {
ApiResponse {
code: status_code.as_u16(),
message: message.to_string(),
data: Some(data),
}
}
}

1
src/domain/vo/user.rs Normal file
View File

@@ -0,0 +1 @@
// 用户相关 VO预留

89
src/error.rs Normal file
View File

@@ -0,0 +1,89 @@
use crate::domain::vo::ApiResponse;
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
/// 应用错误类型
#[allow(dead_code)]
pub struct AppError(pub anyhow::Error);
impl IntoResponse for AppError {
fn into_response(self) -> Response {
tracing::error!("Application error: {:?}", self.0);
let (status, message) = match self.0.downcast_ref::<&str>() {
Some(&"not_found") => (StatusCode::NOT_FOUND, "Resource not found"),
Some(&"unauthorized") => (StatusCode::UNAUTHORIZED, "Unauthorized"),
_ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"),
};
let body = ApiResponse::<()> {
code: status.as_u16(),
message: message.to_string(),
data: None,
};
(status, Json(body)).into_response()
}
}
// 为具体类型实现 From
impl From<anyhow::Error> for AppError {
fn from(err: anyhow::Error) -> Self {
Self(err)
}
}
/// 统一的 API 错误响应结构
#[derive(Debug)]
pub struct ErrorResponse {
pub status: StatusCode,
pub message: String,
}
impl ErrorResponse {
pub fn new(message: impl Into<String>) -> Self {
Self {
status: StatusCode::BAD_REQUEST,
message: message.into(),
}
}
#[allow(dead_code)]
pub fn not_found(message: impl Into<String>) -> Self {
Self {
status: StatusCode::NOT_FOUND,
message: message.into(),
}
}
#[allow(dead_code)]
pub fn unauthorized(message: impl Into<String>) -> Self {
Self {
status: StatusCode::UNAUTHORIZED,
message: message.into(),
}
}
#[allow(dead_code)]
pub fn internal(message: impl Into<String>) -> Self {
Self {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: message.into(),
}
}
}
impl IntoResponse for ErrorResponse {
fn into_response(self) -> Response {
let body = ApiResponse::<()> {
code: self.status.as_u16(),
message: self.message,
data: None,
};
(self.status, Json(body)).into_response()
}
}

157
src/handlers/auth.rs Normal file
View File

@@ -0,0 +1,157 @@
use crate::error::ErrorResponse;
use crate::infra::middleware::logging::{log_info, RequestId};
use crate::domain::dto::auth::{RegisterRequest, LoginRequest, RefreshRequest, DeleteUserRequest};
use crate::domain::vo::auth::{RegisterResult, LoginResult, RefreshResult};
use crate::domain::vo::ApiResponse;
use crate::repositories::user_repository::UserRepository;
use crate::services::auth_service::AuthService;
use crate::AppState;
use axum::{
extract::{Extension, State},
Json,
};
use serde_json::json;
/// 注册
pub async fn register(
Extension(request_id): Extension<RequestId>,
State(state): State<AppState>,
Json(payload): Json<RegisterRequest>,
) -> Result<Json<ApiResponse<RegisterResult>>, ErrorResponse> {
log_info(&request_id, "注册请求参数", &payload);
let user_repo = UserRepository::new(state.pool.clone());
let service = AuthService::new(user_repo, state.redis_client.clone(), state.config.auth.clone());
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()))
}
}
}
/// 登录
pub async fn login(
Extension(request_id): Extension<RequestId>,
State(state): State<AppState>,
Json(payload): Json<LoginRequest>,
) -> Result<Json<ApiResponse<LoginResult>>, ErrorResponse> {
log_info(&request_id, "登录请求参数", &payload);
let user_repo = UserRepository::new(state.pool.clone());
let service = AuthService::new(user_repo, state.redis_client.clone(), state.config.auth.clone());
match service.login(payload).await {
Ok((user_model, access_token, refresh_token)) => {
let data = LoginResult::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()))
}
}
}
/// 刷新 Token
pub async fn refresh(
Extension(request_id): Extension<RequestId>,
State(state): State<AppState>,
Json(payload): Json<RefreshRequest>,
) -> Result<Json<ApiResponse<RefreshResult>>, ErrorResponse> {
log_info(
&request_id,
"刷新 token 请求",
&json!({"device_id": "default"}),
);
let user_repo = UserRepository::new(state.pool.clone());
let service = AuthService::new(user_repo, state.redis_client.clone(), state.config.auth.clone());
match service
.refresh_access_token(&payload.refresh_token)
.await
{
Ok((access_token, refresh_token)) => {
let data = RefreshResult {
access_token,
refresh_token,
};
let response = ApiResponse::success(data);
log_info(
&request_id,
"刷新成功",
&json!({"access_token": "***"}),
);
Ok(Json(response))
}
Err(e) => {
log_info(&request_id, "刷新失败", &e.to_string());
Err(ErrorResponse::new(e.to_string()))
}
}
}
/// 删除账号
pub async fn delete_account(
Extension(request_id): Extension<RequestId>,
State(state): State<AppState>,
Extension(user_id): Extension<String>,
Json(payload): Json<DeleteUserRequest>,
) -> Result<Json<ApiResponse<()>>, ErrorResponse> {
log_info(&request_id, "删除账号请求", &format!("user_id={}", user_id));
let user_repo = UserRepository::new(state.pool.clone());
let service = AuthService::new(user_repo, state.redis_client.clone(), state.config.auth.clone());
let delete_request = DeleteUserRequest {
user_id: user_id.clone(),
password: payload.password,
};
match service.delete_user(delete_request).await {
Ok(_) => {
log_info(&request_id, "账号删除成功", &format!("user_id={}", user_id));
let response = ApiResponse::success_with_message((), "账号删除成功");
Ok(Json(response))
}
Err(e) => {
log_info(&request_id, "账号删除失败", &e.to_string());
Err(ErrorResponse::new(e.to_string()))
}
}
}
/// 刷新令牌
pub async fn delete_refresh_token(
Extension(request_id): Extension<RequestId>,
State(state): State<AppState>,
Extension(user_id): Extension<String>,
) -> Result<Json<ApiResponse<()>>, ErrorResponse> {
log_info(&request_id, "删除刷新令牌请求", &format!("user_id={}", user_id));
let user_repo = UserRepository::new(state.pool.clone());
let service = AuthService::new(user_repo, state.redis_client.clone(), state.config.auth.clone());
match service.delete_refresh_token(&user_id).await {
Ok(_) => {
log_info(&request_id, "刷新令牌删除成功", &format!("user_id={}", user_id));
let response = ApiResponse::success_with_message((), "刷新令牌删除成功");
Ok(Json(response))
}
Err(e) => {
log_info(&request_id, "刷新令牌删除失败", &e.to_string());
Err(ErrorResponse::new(e.to_string()))
}
}
}

25
src/handlers/health.rs Normal file
View File

@@ -0,0 +1,25 @@
use crate::AppState;
use crate::db;
use axum::{
extract::State,
response::{IntoResponse, Json},
};
use serde_json::json;
/// 健康检查端点
pub async fn health_check(State(state): State<AppState>) -> impl IntoResponse {
match db::health_check(&state.pool).await {
Ok(_) => Json(json!({"status": "ok"})),
Err(_) => Json(json!({"status": "unavailable"})),
}
}
/// 获取服务器信息
pub async fn server_info() -> impl IntoResponse {
Json(json!({
"name": "web-rust-template",
"version": "0.1.0",
"status": "running",
"timestamp": chrono::Utc::now().timestamp()
}))
}

2
src/handlers/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod auth;
pub mod health;

View File

@@ -0,0 +1,51 @@
use crate::AppState;
use axum::{
extract::{Request, State},
http::{HeaderMap, StatusCode},
middleware::Next,
response::Response,
};
use jsonwebtoken::{decode, DecodingKey, Validation};
use serde::Deserialize;
#[derive(Deserialize)]
pub struct Claims {
pub sub: String, // user_id
#[allow(dead_code)]
pub exp: usize,
}
/// JWT 认证中间件
pub async fn auth_middleware(
State(state): State<AppState>,
headers: HeaderMap,
mut req: Request,
next: Next,
) -> Result<Response, StatusCode> {
// 1. 提取 Authorization header
let auth_header = headers
.get("Authorization")
.and_then(|h| h.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
if !auth_header.starts_with("Bearer ") {
return Err(StatusCode::UNAUTHORIZED);
}
let token = &auth_header[7..];
// 2. 验证 JWT
let jwt_secret = &state.config.auth.jwt_secret;
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(jwt_secret.as_ref()),
&Validation::default(),
)
.map_err(|_| StatusCode::UNAUTHORIZED)?;
// 3. 将 user_id 添加到请求扩展
req.extensions_mut().insert(token_data.claims.sub);
Ok(next.run(req).await)
}

View File

@@ -0,0 +1,71 @@
use axum::{extract::Request, response::Response};
use std::time::Instant;
/// Request ID 标记
#[derive(Clone)]
pub struct RequestId(pub String);
/// 请求日志中间件
pub async fn request_logging_middleware(
mut req: Request,
next: axum::middleware::Next,
) -> Response {
let start = Instant::now();
// 提取请求信息
let method = req.method().clone();
let path = req.uri().path().to_string();
let query = req.uri().query().map(|s| s.to_string());
// 生成请求 ID
let request_id = uuid::Uuid::new_v4().to_string();
// 将 request_id 存储到请求扩展中
req.extensions_mut().insert(RequestId(request_id.clone()));
// 第1条日志请求开始
let separator = "=".repeat(80);
let header = format!("{} {}", method, path);
tracing::info!("{}", separator);
tracing::info!("{}", header);
tracing::info!("{}", separator);
let now_beijing = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
let query_str = query.as_deref().unwrap_or("");
tracing::info!(
"[{}] 📥 查询参数: {} | 时间: {}",
request_id,
query_str,
now_beijing
);
// 调用下一个处理器
let response = next.run(req).await;
// 第3条日志请求完成
let duration = start.elapsed();
let status = response.status();
tracing::info!(
"[{}] ✅ 状态码: {} | 耗时: {}ms",
request_id,
status.as_u16(),
duration.as_millis()
);
tracing::info!("{}", separator);
response
}
/// 请求日志辅助工具
pub fn log_info<T: std::fmt::Debug>(request_id: &RequestId, label: &str, data: T) {
let data_str = format!("{:?}", data);
let truncated = if data_str.len() > 300 {
format!("{}...", &data_str[..300])
} else {
data_str
};
tracing::info!("[{}] 🔧 {} | {}", request_id.0, label, truncated);
}

View File

@@ -0,0 +1,2 @@
pub mod auth;
pub mod logging;

2
src/infra/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod middleware;
pub mod redis;

20
src/infra/redis/errors.rs Normal file
View File

@@ -0,0 +1,20 @@
use thiserror::Error;
/// Redis 错误类型
#[derive(Error, Debug)]
pub enum RedisError {
#[error("Redis 连接失败: {0}")]
ConnectionError(#[from] redis::RedisError),
#[error("Redis 序列化失败: {0}")]
SerializationError(#[from] serde_json::Error),
#[error("Redis 数据不存在: {key}")]
NotFound { key: String },
#[error("Redis 操作失败: {message}")]
OperationError { message: String },
#[error("Failed to create redis pool: {0}")]
PoolCreation(#[from] deadpool_redis::CreatePoolError),
}

2
src/infra/redis/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod redis_client;
pub mod redis_key;

View File

@@ -0,0 +1,135 @@
use super::redis_key::RedisKey;
use redis::aio::MultiplexedConnection;
use redis::{AsyncCommands, Client};
use serde::Serialize;
use std::sync::Arc;
use tokio::sync::Mutex;
/// Redis 客户端(使用 MultiplexedConnection
#[derive(Clone)]
pub struct RedisClient {
conn: Arc<Mutex<MultiplexedConnection>>,
}
impl RedisClient {
/// 创建新的 Redis 客户端
pub async fn new(url: &str) -> redis::RedisResult<Self> {
let client = Client::open(url)?;
let conn = client.get_multiplexed_async_connection().await?;
Ok(Self {
conn: Arc::new(Mutex::new(conn)),
})
}
/// 设置字符串值
pub async fn set(&self, k: &str, v: &str) -> redis::RedisResult<()> {
let mut c = self.conn.lock().await;
c.set(k, v).await
}
/// 获取字符串值
pub async fn get(&self, k: &str) -> redis::RedisResult<Option<String>> {
let mut c = self.conn.lock().await;
c.get(k).await
}
/// 设置字符串值并指定过期时间(秒)
pub async fn set_ex(&self, k: &str, v: &str, seconds: u64) -> redis::RedisResult<()> {
let mut c = self.conn.lock().await;
c.set_ex(k, v, seconds).await
}
/// 删除键
pub async fn del(&self, k: &str) -> redis::RedisResult<()> {
let mut c = self.conn.lock().await;
c.del(k).await
}
/// 设置键的过期时间(秒)
pub async fn expire(&self, k: &str, seconds: u64) -> redis::RedisResult<()> {
let mut c = self.conn.lock().await;
c.expire(k, seconds as i64).await
}
/// 使用 RedisKey 设置 JSON 值
pub async fn set_key<T: Serialize>(
&self,
key: &RedisKey,
value: &T,
) -> redis::RedisResult<()> {
let json = serde_json::to_string(value).map_err(|e| {
redis::RedisError::from((
redis::ErrorKind::TypeError,
"JSON serialization failed",
e.to_string(),
))
})?;
let mut c = self.conn.lock().await;
c.set(key.build(), json).await
}
/// 使用 RedisKey 设置 JSON 值并指定过期时间(秒)
pub async fn set_key_ex<T: Serialize>(
&self,
key: &RedisKey,
value: &T,
expiration_seconds: u64,
) -> redis::RedisResult<()> {
let json = serde_json::to_string(value).map_err(|e| {
redis::RedisError::from((
redis::ErrorKind::TypeError,
"JSON serialization failed",
e.to_string(),
))
})?;
let mut c = self.conn.lock().await;
c.set_ex(key.build(), json, expiration_seconds).await
}
/// 使用 RedisKey 获取字符串值
pub async fn get_key(&self, key: &RedisKey) -> redis::RedisResult<Option<String>> {
let mut c = self.conn.lock().await;
let json: Option<String> = c.get(key.build()).await?;
Ok(json)
}
/// 使用 RedisKey 获取并反序列化 JSON 值
pub async fn get_key_json<T: for<'de> serde::Deserialize<'de>>(
&self,
key: &RedisKey,
) -> redis::RedisResult<Option<T>> {
let mut c = self.conn.lock().await;
let json: Option<String> = c.get(key.build()).await?;
match json {
Some(data) => {
let value = serde_json::from_str(&data).map_err(|e| {
redis::RedisError::from((
redis::ErrorKind::TypeError,
"JSON deserialization failed",
e.to_string(),
))
})?;
Ok(Some(value))
}
None => Ok(None),
}
}
/// 使用 RedisKey 删除键
pub async fn delete_key(&self, key: &RedisKey) -> redis::RedisResult<()> {
let mut c = self.conn.lock().await;
c.del(key.build()).await
}
/// 使用 RedisKey 检查键是否存在
pub async fn exists_key(&self, key: &RedisKey) -> redis::RedisResult<bool> {
let mut c = self.conn.lock().await;
c.exists(key.build()).await
}
/// 使用 RedisKey 设置键的过期时间(秒)
pub async fn expire_key(&self, key: &RedisKey, seconds: u64) -> redis::RedisResult<()> {
let mut c = self.conn.lock().await;
c.expire(key.build(), seconds as i64).await
}
}

View File

@@ -0,0 +1,61 @@
use serde::{Deserialize, Serialize};
use std::fmt;
/// 业务类型枚举
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum BusinessType {
#[serde(rename = "auth")]
Auth,
#[serde(rename = "user")]
User,
#[serde(rename = "cache")]
Cache,
#[serde(rename = "session")]
Session,
#[serde(rename = "rate_limit")]
RateLimit,
}
impl BusinessType {
pub fn prefix(self) -> &'static str {
match self {
BusinessType::Auth => "auth",
BusinessType::User => "user",
BusinessType::Cache => "cache",
BusinessType::Session => "session",
BusinessType::RateLimit => "rate_limit",
}
}
}
/// Redis 键构建器
#[derive(Debug, Clone)]
pub struct RedisKey {
business: BusinessType,
identifiers: Vec<String>,
}
impl RedisKey {
pub fn new(business: BusinessType) -> Self {
Self {
business,
identifiers: Vec::new(),
}
}
pub fn add_identifier(mut self, id: impl Into<String>) -> Self {
self.identifiers.push(id.into());
self
}
pub fn build(&self) -> String {
format!("{}:{}", self.business.prefix(), self.identifiers.join(":"))
}
}
// 兼容现有格式
impl fmt::Display for RedisKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.build())
}
}

131
src/main.rs Normal file
View File

@@ -0,0 +1,131 @@
mod cli;
mod config;
mod db;
mod domain;
mod error;
mod handlers;
mod infra;
mod repositories;
mod services;
mod utils;
use axum::{
routing::{get, post},
Router,
};
use clap::Parser;
use cli::CliArgs;
use tower_http::cors::{Any, CorsLayer};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
/// 应用状态
#[derive(Clone)]
pub struct AppState {
pub pool: db::DbPool,
pub config: config::app::AppConfig,
pub redis_client: infra::redis::redis_client::RedisClient,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 解析命令行参数
let args = CliArgs::parse();
// 初始化日志
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| args.get_log_filter().into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
// 打印启动信息
args.print_startup_info();
// 设置工作目录(如果指定)
if let Some(ref work_dir) = args.work_dir {
std::env::set_current_dir(work_dir).ok();
println!("Working directory set to: {}", work_dir.display());
}
// 解析配置文件路径(可选)
let config_path = args.resolve_config_path();
// 加载配置(支持 CLI 覆盖)
// 如果没有配置文件,将仅使用环境变量和默认值
let config = config::app::AppConfig::load_with_overrides(
config_path,
args.get_overrides(),
args.env.as_str(),
)?;
tracing::info!("Configuration loaded successfully");
tracing::info!("Environment: {}", args.env.as_str());
tracing::info!("Debug mode: {}", args.is_debug_enabled());
// 初始化数据库(自动创建数据库和表)
let pool = db::init_database(&config.database).await?;
// 初始化 Redis 客户端
let redis_client = infra::redis::redis_client::RedisClient::new(&config.redis.build_url())
.await
.map_err(|e| anyhow::anyhow!("Redis 初始化失败: {}", e))?;
tracing::info!("Redis 连接池初始化成功");
// 创建应用状态
let app_state = AppState {
pool: pool.clone(),
config: config.clone(),
redis_client,
};
// ========== 公开路由 ==========
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),
)
// JWT 认证中间件(仅应用于受保护路由)
.route_layer(axum::middleware::from_fn_with_state(
app_state.clone(),
infra::middleware::auth::auth_middleware,
));
// ========== 合并路由 ==========
let app = public_routes
.merge(protected_routes)
// CORS应用于所有路由
.layer(
CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any)
)
// 日志中间件(应用于所有路由)
.layer(axum::middleware::from_fn_with_state(
app_state.clone(),
infra::middleware::logging::request_logging_middleware,
))
.with_state(app_state);
// 启动服务器
let addr = format!("{}:{}", config.server.host, config.server.port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
tracing::info!("Server listening on {}", addr);
tracing::info!("Press Ctrl+C to stop");
axum::serve(listener, app).await?;
Ok(())
}

2
src/repositories/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod user_repository;

View File

@@ -0,0 +1,85 @@
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, DatabaseConnection, Set, ActiveModelTrait, PaginatorTrait};
use crate::domain::entities::users;
use anyhow::Result;
/// 用户数据访问仓库
pub struct UserRepository {
db: DatabaseConnection,
}
impl UserRepository {
pub fn new(db: DatabaseConnection) -> Self {
Self { db }
}
/// 根据 email 查询用户
pub async fn find_by_email(&self, email: &str) -> Result<Option<users::Model>> {
let user = users::Entity::find()
.filter(users::Column::Email.eq(email))
.one(&self.db)
.await
.map_err(|e| anyhow::anyhow!("查询失败: {}", e))?;
Ok(user)
}
/// 统计邮箱数量
pub async fn count_by_email(&self, email: &str) -> Result<i64> {
let count = users::Entity::find()
.filter(users::Column::Email.eq(email))
.count(&self.db)
.await
.map_err(|e| anyhow::anyhow!("查询失败: {}", e))?;
Ok(count as i64)
}
/// 统计用户 ID 数量
pub async fn count_by_id(&self, id: &str) -> Result<i64> {
let count = users::Entity::find()
.filter(users::Column::Id.eq(id))
.count(&self.db)
.await
.map_err(|e| anyhow::anyhow!("查询失败: {}", e))?;
Ok(count as i64)
}
/// 获取密码哈希
pub async fn get_password_hash(&self, email: &str) -> Result<Option<String>> {
let user = users::Entity::find()
.filter(users::Column::Email.eq(email))
.one(&self.db)
.await
.map_err(|e| anyhow::anyhow!("查询失败: {}", e))?;
Ok(user.map(|u| u.password_hash))
}
/// 插入用户created_at 和 updated_at 会自动填充),返回插入后的用户对象
pub async fn insert(&self, id: String, email: String, password_hash: String) -> Result<users::Model> {
let user_model = users::ActiveModel {
id: Set(id),
email: Set(email),
password_hash: Set(password_hash),
// created_at 和 updated_at 由 ActiveModelBehavior 自动填充
..Default::default()
};
let inserted_user = user_model.insert(&self.db)
.await
.map_err(|e| anyhow::anyhow!("插入失败: {}", e))?;
Ok(inserted_user)
}
/// 根据 ID 删除用户
pub async fn delete_by_id(&self, id: &str) -> Result<()> {
users::Entity::delete_by_id(id)
.exec(&self.db)
.await
.map_err(|e| anyhow::anyhow!("删除失败: {}", e))?;
Ok(())
}
}

View File

@@ -0,0 +1,232 @@
use anyhow::Result;
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
use rand::Rng;
use crate::utils::jwt::TokenService;
use crate::domain::dto::auth::{RegisterRequest, LoginRequest, DeleteUserRequest};
use crate::domain::entities::users;
use crate::config::auth::AuthConfig;
use crate::infra::redis::{redis_client::RedisClient, redis_key::{BusinessType, RedisKey}};
use crate::repositories::user_repository::UserRepository;
pub struct AuthService {
user_repo: UserRepository,
redis_client: RedisClient,
auth_config: AuthConfig,
}
impl AuthService {
pub fn new(user_repo: UserRepository, redis_client: RedisClient, auth_config: AuthConfig) -> Self {
Self { user_repo, redis_client, auth_config }
}
/// 哈希密码
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)
}
/// 生成用户 ID
pub fn generate_user_id(&self) -> String {
let mut rng = rand::thread_rng();
rng.gen_range(1_000_000_000i64..10_000_000_000i64)
.to_string()
}
/// 生成唯一的用户 ID
pub async fn generate_unique_user_id(&self) -> Result<String> {
let mut attempts = 0;
const MAX_ATTEMPTS: u32 = 10;
loop {
let candidate_id = self.generate_user_id();
let existing = self.user_repo.count_by_id(&candidate_id).await?;
if existing == 0 {
return Ok(candidate_id);
}
attempts += 1;
if attempts >= MAX_ATTEMPTS {
return Err(anyhow::anyhow!("生成唯一用户 ID 失败"));
}
}
}
/// 保存 refresh_token 到 Redis
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(())
}
/// 获取并删除 refresh_token
async fn get_and_delete_refresh_token(&self, user_id: &str) -> Result<String> {
let key = RedisKey::new(BusinessType::Auth)
.add_identifier("refresh_token")
.add_identifier(user_id);
let token: Option<String> = self.redis_client
.get(&key.build())
.await
.map_err(|e| anyhow::anyhow!("Redis 查询失败: {}", e))?;
if token.is_some() {
self.redis_client
.delete_key(&key)
.await
.map_err(|e| anyhow::anyhow!("Redis 删除失败: {}", e))?;
}
token.ok_or_else(|| anyhow::anyhow!("刷新令牌无效或已过期"))
}
/// 删除用户的 refresh_token
pub async fn delete_refresh_token(&self, user_id: &str) -> Result<()> {
let key = RedisKey::new(BusinessType::Auth)
.add_identifier("refresh_token")
.add_identifier(user_id);
self.redis_client
.delete_key(&key)
.await
.map_err(|e| anyhow::anyhow!("Redis 删除失败: {}", e))?;
Ok(())
}
/// 注册用户
pub async fn register(
&self,
request: RegisterRequest,
) -> Result<(users::Model, String, String)> {
// 1. 检查邮箱是否已存在
let existing = self.user_repo.count_by_email(&request.email).await?;
if existing > 0 {
return Err(anyhow::anyhow!("邮箱已注册"));
}
// 2. 哈希密码
let password_hash = self.hash_password(&request.password)?;
// 3. 生成用户 ID
let user_id = self.generate_unique_user_id().await?;
// 4. 插入数据库并获取包含真实 created_at 的用户对象
let user = self.user_repo.insert(user_id.clone(), request.email, password_hash).await?;
// 5. 生成 token
let (access_token, refresh_token) = TokenService::generate_token_pair(
&user_id,
self.auth_config.access_token_expiration_minutes,
self.auth_config.refresh_token_expiration_days,
&self.auth_config.jwt_secret,
)?;
// 6. 保存 refresh_token
self.save_refresh_token(&user_id, &refresh_token, self.auth_config.refresh_token_expiration_days as i64).await?;
Ok((user, access_token, refresh_token))
}
/// 登录
pub async fn login(
&self,
request: LoginRequest,
) -> Result<(users::Model, String, String)> {
// 1. 查询用户
let user = self.user_repo.find_by_email(&request.email).await?
.ok_or_else(|| anyhow::anyhow!("邮箱或密码错误"))?;
// 2. 验证密码
let password_hash = self.user_repo.get_password_hash(&request.email).await?
.ok_or_else(|| anyhow::anyhow!("邮箱或密码错误"))?;
let parsed_hash = PasswordHash::new(&password_hash)
.map_err(|e| anyhow::anyhow!("解析密码哈希失败: {}", e))?;
let argon2 = Argon2::default();
argon2
.verify_password(request.password.as_bytes(), &parsed_hash)
.map_err(|_| anyhow::anyhow!("邮箱或密码错误"))?;
// 3. 生成 token
let (access_token, refresh_token) = TokenService::generate_token_pair(
&user.id,
self.auth_config.access_token_expiration_minutes,
self.auth_config.refresh_token_expiration_days,
&self.auth_config.jwt_secret,
)?;
// 4. 保存 refresh_token
self.save_refresh_token(&user.id, &refresh_token, self.auth_config.refresh_token_expiration_days as i64).await?;
Ok((user, access_token, refresh_token))
}
/// 使用 refresh_token 刷新 access_token
pub async fn refresh_access_token(
&self,
refresh_token: &str,
) -> Result<(String, String)> {
// 1. 从 refresh_token 中解码出 user_id
let user_id = TokenService::decode_user_id(refresh_token, &self.auth_config.jwt_secret)?;
// 2. 从 Redis 获取存储的 token 并删除
let stored_token = self.get_and_delete_refresh_token(&user_id).await?;
// 3. 验证 token 是否匹配
if stored_token != refresh_token {
return Err(anyhow::anyhow!("刷新令牌无效"));
}
// 4. 生成新的 token 对
let (new_access_token, new_refresh_token) = TokenService::generate_token_pair(
&user_id,
self.auth_config.access_token_expiration_minutes,
self.auth_config.refresh_token_expiration_days,
&self.auth_config.jwt_secret,
)?;
// 5. 保存新的 refresh_token
self.save_refresh_token(&user_id, &new_refresh_token, self.auth_config.refresh_token_expiration_days as i64).await?;
Ok((new_access_token, new_refresh_token))
}
/// 删除用户
pub async fn delete_user(&self, request: DeleteUserRequest) -> Result<()> {
let password_hash = self.user_repo.get_password_hash(&request.user_id).await?
.ok_or_else(|| anyhow::anyhow!("用户不存在"))?;
let parsed_hash = PasswordHash::new(&password_hash)
.map_err(|e| anyhow::anyhow!("解析密码哈希失败: {}", e))?;
let argon2 = Argon2::default();
argon2
.verify_password(request.password.as_bytes(), &parsed_hash)
.map_err(|_| anyhow::anyhow!("密码错误"))?;
self.user_repo.delete_by_id(&request.user_id).await?;
Ok(())
}
}

1
src/services/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod auth_service;

101
src/utils/jwt.rs Normal file
View File

@@ -0,0 +1,101 @@
use anyhow::Result;
use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
/// JWT 工具类,负责生成和验证 JWT token
pub struct TokenService;
impl TokenService {
/// 生成 JWT 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)
}
/// 生成 access token 和 refresh token
pub fn generate_token_pair(
user_id: &str,
access_token_expiration_minutes: u64,
refresh_token_expiration_days: i64,
jwt_secret: &str,
) -> Result<(String, String)> {
let access_token =
Self::generate_access_token(user_id, access_token_expiration_minutes, jwt_secret)?;
let refresh_token =
Self::generate_refresh_token(user_id, refresh_token_expiration_days, jwt_secret)?;
Ok((access_token, refresh_token))
}
/// 从 token 中解码出 user_id
pub fn decode_user_id(token: &str, jwt_secret: &str) -> Result<String> {
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(jwt_secret.as_ref()),
&Validation::default(),
)
.map_err(|e| anyhow::anyhow!("Token 解码失败: {}", e))?;
Ok(token_data.claims.sub)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
pub sub: String, // user_id
pub exp: usize, // 过期时间
pub token_type: TokenType,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum TokenType {
Access,
Refresh,
}

1
src/utils/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod jwt;