Files
web-rust-template-project/docs/api/authentication.md
2026-02-13 15:57:29 +08:00

15 KiB
Raw Blame History

认证机制详解

本文档详细说明 Web Rust Template 的 JWT 认证机制、安全特性和最佳实践。

目录


认证架构概述

本系统采用 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 传递

格式

Authorization: Bearer <access_token>

Refresh Token

用途:获取新的 Access Token

特点

  • 有效期7 天(可配置)
  • 存储在 Redis 中
  • 支持撤销和主动登出
  • 每次刷新会生成新的 Refresh Token

存储位置

  • 前端localStorage 或 sessionStorage
  • 后端RedisKeyauth:refresh_token:<user_id>

认证流程

1. 用户注册流程

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. 用户登录流程

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. 访问受保护接口流程

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 刷新流程

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. 用户登出流程

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 生成

// 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 验证

// 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 存储

// 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 破解)
  • 自动生成随机盐值
  • 哈希结果不可逆
// 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 结构

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 存储

推荐方案

// 存储 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. 请求拦截器

// 添加 Token 到请求头
api.interceptors.request.use((config) => {
  const accessToken = localStorage.getItem('access_token');
  if (accessToken) {
    config.headers.Authorization = `Bearer ${accessToken}`;
  }
  return config;
});

3. 响应拦截器(自动刷新 Token

// 处理 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. 密码强度要求

// 验证密码强度
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/ 目录下的配置文件:

# 方式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

生产环境

# 使用强随机密钥
AUTH_JWT_SECRET=$(openssl rand -base64 32)

3. Token 过期时间配置

# Access Token15 分钟(推荐)
AUTH_ACCESS_TOKEN_EXPIRATION_MINUTES=15

# Refresh Token7 天(推荐)
AUTH_REFRESH_TOKEN_EXPIRATION_DAYS=7

建议

  • Access Token5-30 分钟(权衡安全性和用户体验)
  • Refresh Token7-30 天(根据应用安全要求)

生产部署

1. HTTPS 强制

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 配置

开发环境可以允许所有来源:

CorsLayer::new()
    .allow_origin(Any)
    .allow_methods(Any)
    .allow_headers(Any)

生产环境应该限制允许的来源:

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 攻击(需要额外实现):

// 使用 governor 库实现速率限制
use governor::{Quota, RateLimiter};

let limiter = RateLimiter::direct(Quota::per_second(5));
// 每秒最多 5 个请求

相关文档


提示:生产环境部署前务必修改 JWT 密钥为强随机字符串!