91 lines
3.3 KiB
Rust
91 lines
3.3 KiB
Rust
//! 全局认证拦截中间件
|
||
//!
|
||
//! - 功能:对进入 /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<Db>,
|
||
mut req: Request<Body>,
|
||
next: Next,
|
||
) -> Result<Response, AppError> {
|
||
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 <token>
|
||
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::<u8>().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 中实现。
|