diff --git a/backend/src/main.rs b/backend/src/main.rs index e577135..806583c 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -121,7 +121,8 @@ async fn main() -> anyhow::Result<()> { let app = Router::new() .nest("/api", api) .layer(cors) - .layer(middleware::from_fn_with_state(db.clone(), middlewares::logging::request_logger)); + .layer(middleware::from_fn_with_state(db.clone(), middlewares::logging::request_logger)) + .layer(middleware::from_fn_with_state(db.clone(), middlewares::auth_guard::auth_guard)); // 读取并记录最终使用的主机与端口(默认端口改为 9898) let app_host = std::env::var("APP_HOST").unwrap_or("0.0.0.0".into()); diff --git a/backend/src/middlewares/auth_guard.rs b/backend/src/middlewares/auth_guard.rs new file mode 100644 index 0000000..f445c1a --- /dev/null +++ b/backend/src/middlewares/auth_guard.rs @@ -0,0 +1,91 @@ +//! 全局认证拦截中间件 +//! +//! - 功能:对进入 /api 的请求进行统一的登录校验(Bearer access token) +//! - 支持按路径跳过认证:前缀白名单与精确路径白名单 +//! - 失败返回 401,消息体统一为 { code: 401, message: "unauthorized" } + +use axum::{extract::State, http::{Request, Method, header::AUTHORIZATION}, middleware::Next, response::Response}; +use axum::body::Body; +use crate::{db::Db, error::AppError}; + +/// 路径白名单配置 +/// - prefix_whitelist:按前缀匹配(例如 /api/auth/) +/// - exact_whitelist:按完整路径匹配(例如 /api/auth/login) +#[derive(Clone, Debug)] +pub struct AuthGuardConfig { + pub prefix_whitelist: Vec<&'static str>, + pub exact_whitelist: Vec<&'static str>, +} + +impl Default for AuthGuardConfig { + fn default() -> Self { + Self { + // 登录/刷新/公开动态接口等路径前缀允许匿名访问 + prefix_whitelist: vec![ + "/api/auth/", + "/api/dynamic_api/public/", + ], + // 精确路径白名单:如健康检查等 + exact_whitelist: vec![ + "/api/auth/login", + "/api/auth/refresh", + ], + } + } +} + +/// 全局认证拦截中间件 +pub async fn auth_guard( + State(_db): State, + mut req: Request, + next: Next, +) -> Result { + let path = req.uri().path(); + + // CORS 预检请求直接放行 + if req.method() == Method::OPTIONS { + return Ok(next.run(req).await); + } + + // 读取白名单配置(后续可支持从环境变量扩展) + let cfg = AuthGuardConfig::default(); + let is_whitelisted = cfg.exact_whitelist.iter().any(|&x| x == path) + || cfg.prefix_whitelist.iter().any(|&p| path.starts_with(p)); + + if is_whitelisted { + return Ok(next.run(req).await); + } + + // 仅拦截 /api 下的受保护接口;其他如 /ws /sse 在各自模块中单独校验 + if !path.starts_with("/api/") { + return Ok(next.run(req).await); + } + + // 从请求头解析 Authorization: Bearer + let auth = req.headers().get(AUTHORIZATION).ok_or(AppError::Unauthorized)?; + let auth = auth.to_str().map_err(|_| AppError::Unauthorized)?; + let token = auth.strip_prefix("Bearer ").ok_or(AppError::Unauthorized)?; + let secret = std::env::var("JWT_SECRET").map_err(|_| AppError::Unauthorized)?; + + // 解析并校验访问令牌 + let claims = crate::middlewares::jwt::decode_token(token, &secret)?; + if claims.typ != "access" { return Err(AppError::Unauthorized); } + + // 可选:Redis 二次校验(与 AuthUser 提取器一致) + let redis_validation_enabled = std::env::var("REDIS_TOKEN_VALIDATION") + .ok() + .and_then(|v| v.parse::().ok()) + .map(|x| x == 1) + .unwrap_or(false); + if redis_validation_enabled { + let is_valid = crate::redis::TokenRedis::validate_access_token(token, claims.uid).await.unwrap_or(false); + if !is_valid { return Err(AppError::Unauthorized); } + } + + // 将用户信息注入扩展,供后续处理链使用(可选) + req.extensions_mut().insert(claims); + + Ok(next.run(req).await) +} + +// 统一将 AppError 转换为响应说明:AppError 的 IntoResponse 已在 error.rs 中实现。 \ No newline at end of file diff --git a/backend/src/middlewares/mod.rs b/backend/src/middlewares/mod.rs index 36ace6b..0b993fa 100644 --- a/backend/src/middlewares/mod.rs +++ b/backend/src/middlewares/mod.rs @@ -3,4 +3,4 @@ pub mod logging; pub mod sse; pub mod http_client; pub mod ws; -// removed: pub mod sse_server; \ No newline at end of file +pub mod auth_guard; \ No newline at end of file