# 认证机制详解 本文档详细说明 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 ``` ### Refresh Token **用途**:获取新的 Access Token **特点**: - 有效期:7 天(可配置) - 存储在 Redis 中 - 支持撤销和主动登出 - 每次刷新会生成新的 Refresh Token **存储位置**: - 前端:localStorage 或 sessionStorage - 后端:Redis(Key:`auth:refresh_token:`) --- ## 认证流程 ### 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
Authorization: Bearer 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
{"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
Authorization: Bearer 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 { 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 { 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, mut request: Request, next: Next, ) -> Result { // 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: ``` **过期时间**:7 天(与 Refresh Token 有效期一致) --- ## 安全特性 ### 1. 密码安全 **Argon2 哈希**: - 使用 Argon2 算法(内存 hard,抗 GPU/ASIC 破解) - 自动生成随机盐值 - 哈希结果不可逆 ```rust // src/services/auth_service.rs pub fn hash_password(&self, password: &str) -> Result { let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); let password_hash = argon2 .hash_password(password.as_bytes(), &salt) .map_err(|e| anyhow::anyhow!("密码哈希失败: {}", e))? .to_string(); Ok(password_hash) } ``` ### 2. JWT 安全 **签名算法**:HS256 (HMAC-SHA256) **Claims 结构**: ```rust pub struct Claims { pub sub: String, // 用户 ID pub exp: usize, // 过期时间(Unix 时间戳) pub token_type: TokenType, // Token 类型(Access/Refresh) } ``` **安全措施**: - 使用强密钥(至少 32 位随机字符串) - Token 包含过期时间 - Token 类型区分(防止混用) - 签名验证防止篡改 ### 3. Refresh Token 安全 **存储安全**: - 存储在 Redis 中,支持快速撤销 - 每次刷新生成新 Token,旧 Token 失效 - 支持主动登出,删除 Refresh Token **使用限制**: - Refresh Token 只能使用一次 - 过期后无法续期 - 需要重新登录 ### 4. 防护措施 **防重放攻击**: - Refresh Token 单次使用 - 刷新后立即失效 **防 Token 泄露**: - Access Token 短期有效(15 分钟) - 只通过 HTTPS 传输 - 不在 URL 中传递 **防暴力破解**: - 限制登录频率(可选实现) - 记录失败尝试(日志) - 密码哈希使用 Argon2 --- ## 最佳实践 ### 前端集成 #### 1. Token 存储 **推荐方案**: ```typescript // 存储 Token localStorage.setItem('access_token', access_token); localStorage.setItem('refresh_token', refresh_token); // 读取 Token const accessToken = localStorage.getItem('access_token'); const refreshToken = localStorage.getItem('refresh_token'); // 清除 Token localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); ``` #### 2. 请求拦截器 ```typescript // 添加 Token 到请求头 api.interceptors.request.use((config) => { const accessToken = localStorage.getItem('access_token'); if (accessToken) { config.headers.Authorization = `Bearer ${accessToken}`; } return config; }); ``` #### 3. 响应拦截器(自动刷新 Token) ```typescript // 处理 401 错误并自动刷新 api.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { const refreshToken = localStorage.getItem('refresh_token'); const response = await axios.post('/auth/refresh', { refresh_token: refreshToken, }); const { access_token, refresh_token } = response.data.data; localStorage.setItem('access_token', access_token); localStorage.setItem('refresh_token', refresh_token); // 重试原请求 originalRequest.headers.Authorization = `Bearer ${access_token}`; return axios(originalRequest); } catch (refreshError) { // 刷新失败,跳转登录页 localStorage.clear(); window.location.href = '/login'; return Promise.reject(refreshError); } } return Promise.reject(error); } ); ``` ### 后端开发 #### 1. 密码强度要求 ```rust // 验证密码强度 fn validate_password(password: &str) -> Result<()> { if password.len() < 8 { return Err(anyhow!("密码长度至少 8 位")); } if password.len() > 100 { return Err(anyhow!("密码长度最多 100 位")); } // 可添加更多规则(如必须包含大小写、数字等) Ok(()) } ``` #### 2. JWT 密钥管理 **开发环境**: 使用 `config/` 目录下的配置文件: ```bash # 方式1:使用默认配置(推荐) # JWT 密钥已在 config/default.toml 中配置 # 方式2:创建本地配置文件 cp config/default.toml config/local.toml # 编辑 config/local.toml,修改 jwt_secret nano config/local.toml # 运行 cargo run -- -c config/local.toml ``` **生产环境**: ```bash # 使用强随机密钥 AUTH_JWT_SECRET=$(openssl rand -base64 32) ``` #### 3. Token 过期时间配置 ```bash # Access Token:15 分钟(推荐) AUTH_ACCESS_TOKEN_EXPIRATION_MINUTES=15 # Refresh Token:7 天(推荐) AUTH_REFRESH_TOKEN_EXPIRATION_DAYS=7 ``` **建议**: - Access Token:5-30 分钟(权衡安全性和用户体验) - Refresh Token:7-30 天(根据应用安全要求) ### 生产部署 #### 1. HTTPS 强制 ```nginx server { listen 80; server_name api.yourdomain.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl; server_name api.yourdomain.com; # SSL 配置... } ``` #### 2. CORS 配置 开发环境可以允许所有来源: ```rust CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any) ``` 生产环境应该限制允许的来源: ```rust CorsLayer::new() .allow_origin("https://yourdomain.com".parse::().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 密钥为强随机字符串!