fix: resolve image/upload 500 errors and refactor rate limiting
- Separate /uploads and /api/upload routes from Dioxus app to avoid IncrementalRenderer intercepting non-page requests - Remove broken SmartIpKeyExtractor-based general_limit() that failed under Dioxus dev server proxy (Unable To Extract Key) - Move rate limiting into handlers using governor::RateLimiter directly - Add IMAGE_LIMITER for /uploads/* serving - Make all rate limits configurable via environment variables - Add rate limit config to .env.example with sensible defaults
This commit is contained in:
parent
4f368e6fb8
commit
3974856f3d
10
.env.example
10
.env.example
@ -1,2 +1,12 @@
|
|||||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/yggdrasil
|
DATABASE_URL=postgres://postgres:postgres@localhost:5432/yggdrasil
|
||||||
RUST_LOG=info
|
RUST_LOG=info
|
||||||
|
|
||||||
|
# Rate Limit — 严格限流(登录、注册)
|
||||||
|
RATE_LIMIT_STRICT_PER_SEC=1
|
||||||
|
RATE_LIMIT_STRICT_BURST=5
|
||||||
|
# Rate Limit — 上传限流(图片上传)
|
||||||
|
RATE_LIMIT_UPLOAD_PER_SEC=2
|
||||||
|
RATE_LIMIT_UPLOAD_BURST=15
|
||||||
|
# Rate Limit — 图片访问限流(/uploads/*)
|
||||||
|
RATE_LIMIT_IMAGE_PER_SEC=10
|
||||||
|
RATE_LIMIT_IMAGE_BURST=50
|
||||||
|
|||||||
@ -224,8 +224,19 @@ fn is_path_safe(path: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub async fn serve_image(Path(path): Path<String>, Query(params): Query<ImageParams>) -> Response {
|
use axum::http::HeaderMap;
|
||||||
// Path traversal protection
|
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
pub async fn serve_image(
|
||||||
|
Path(path): Path<String>,
|
||||||
|
Query(params): Query<ImageParams>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> Response {
|
||||||
|
let ip = crate::api::rate_limit::get_client_ip(&headers);
|
||||||
|
if let Err(status) = crate::api::rate_limit::check_image_limit(&ip) {
|
||||||
|
return status.into_response();
|
||||||
|
}
|
||||||
|
|
||||||
if !is_path_safe(&path) {
|
if !is_path_safe(&path) {
|
||||||
return StatusCode::FORBIDDEN.into_response();
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,58 +1,57 @@
|
|||||||
#![allow(clippy::unused_unit)]
|
#![allow(clippy::unused_unit)]
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
use std::sync::Arc;
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
use std::num::NonZeroU32;
|
use std::num::NonZeroU32;
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
use tower_governor::governor::GovernorConfigBuilder;
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
use tower_governor::GovernorLayer;
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
use tower_governor::key_extractor::SmartIpKeyExtractor;
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
use governor::middleware::NoOpMiddleware;
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
use governor::{DefaultKeyedRateLimiter, Quota, RateLimiter};
|
use governor::{DefaultKeyedRateLimiter, Quota, RateLimiter};
|
||||||
|
|
||||||
/// 通用限流配置:每秒 1 请求,突发 30
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub fn general_limit() -> GovernorLayer<SmartIpKeyExtractor, NoOpMiddleware> {
|
use axum::http::StatusCode;
|
||||||
let config = GovernorConfigBuilder::default()
|
|
||||||
.per_second(1)
|
#[cfg(feature = "server")]
|
||||||
.burst_size(30)
|
fn env_or(key: &str, default: u32) -> NonZeroU32 {
|
||||||
.key_extractor(SmartIpKeyExtractor)
|
let val = std::env::var(key)
|
||||||
.finish()
|
.ok()
|
||||||
.unwrap();
|
.and_then(|s| s.parse::<u32>().ok())
|
||||||
GovernorLayer {
|
.unwrap_or(default);
|
||||||
config: Arc::new(config),
|
NonZeroU32::new(val.max(1)).unwrap()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 严格限流:每秒 1,突发 5(用于登录、注册等敏感操作)
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
static STRICT_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> = LazyLock::new(|| {
|
static STRICT_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> = LazyLock::new(|| {
|
||||||
RateLimiter::keyed(
|
RateLimiter::keyed(
|
||||||
Quota::per_second(NonZeroU32::new(1).unwrap())
|
Quota::per_second(env_or("RATE_LIMIT_STRICT_PER_SEC", 1))
|
||||||
.allow_burst(NonZeroU32::new(5).unwrap())
|
.allow_burst(env_or("RATE_LIMIT_STRICT_BURST", 5)),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
// 上传限流:每秒 1,突发 10
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
static UPLOAD_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> = LazyLock::new(|| {
|
static UPLOAD_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> = LazyLock::new(|| {
|
||||||
RateLimiter::keyed(
|
RateLimiter::keyed(
|
||||||
Quota::per_second(NonZeroU32::new(1).unwrap())
|
Quota::per_second(env_or("RATE_LIMIT_UPLOAD_PER_SEC", 2))
|
||||||
.allow_burst(NonZeroU32::new(10).unwrap())
|
.allow_burst(env_or("RATE_LIMIT_UPLOAD_BURST", 15)),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 从请求 headers 中提取客户端 IP
|
#[cfg(feature = "server")]
|
||||||
|
static IMAGE_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> = LazyLock::new(|| {
|
||||||
|
RateLimiter::keyed(
|
||||||
|
Quota::per_second(env_or("RATE_LIMIT_IMAGE_PER_SEC", 10))
|
||||||
|
.allow_burst(env_or("RATE_LIMIT_IMAGE_BURST", 50)),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
pub fn check_image_limit(ip: &str) -> Result<(), StatusCode> {
|
||||||
|
IMAGE_LIMITER
|
||||||
|
.check_key(&ip.to_string())
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|_| StatusCode::TOO_MANY_REQUESTS)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub fn get_client_ip(headers: &http::HeaderMap) -> String {
|
pub fn get_client_ip(headers: &http::HeaderMap) -> String {
|
||||||
// 1. X-Forwarded-For
|
|
||||||
if let Some(ip) = headers
|
if let Some(ip) = headers
|
||||||
.get("x-forwarded-for")
|
.get("x-forwarded-for")
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
@ -60,7 +59,6 @@ pub fn get_client_ip(headers: &http::HeaderMap) -> String {
|
|||||||
{
|
{
|
||||||
return ip.trim().to_string();
|
return ip.trim().to_string();
|
||||||
}
|
}
|
||||||
// 2. X-Real-Ip
|
|
||||||
if let Some(ip) = headers
|
if let Some(ip) = headers
|
||||||
.get("x-real-ip")
|
.get("x-real-ip")
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
@ -70,7 +68,6 @@ pub fn get_client_ip(headers: &http::HeaderMap) -> String {
|
|||||||
"unknown".to_string()
|
"unknown".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 检查严格限流(用于登录、注册等敏感操作)
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub fn check_strict_limit(ip: &str) -> Result<(), String> {
|
pub fn check_strict_limit(ip: &str) -> Result<(), String> {
|
||||||
STRICT_LIMITER
|
STRICT_LIMITER
|
||||||
@ -79,7 +76,6 @@ pub fn check_strict_limit(ip: &str) -> Result<(), String> {
|
|||||||
.map_err(|_| "请求过于频繁,请稍后再试".to_string())
|
.map_err(|_| "请求过于频繁,请稍后再试".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 检查上传限流
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub fn check_upload_limit(ip: &str) -> Result<(), String> {
|
pub fn check_upload_limit(ip: &str) -> Result<(), String> {
|
||||||
UPLOAD_LIMITER
|
UPLOAD_LIMITER
|
||||||
|
|||||||
17
src/main.rs
17
src/main.rs
@ -44,15 +44,22 @@ fn main() {
|
|||||||
dioxus::server::IncrementalRendererConfig::default()
|
dioxus::server::IncrementalRendererConfig::default()
|
||||||
.invalidate_after(std::time::Duration::from_secs(300)),
|
.invalidate_after(std::time::Duration::from_secs(300)),
|
||||||
);
|
);
|
||||||
let router = axum::Router::new()
|
let api_routes = axum::Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/api/upload",
|
"/api/upload",
|
||||||
axum::routing::post(crate::api::upload::upload_image)
|
axum::routing::post(crate::api::upload::upload_image)
|
||||||
.layer(axum::extract::DefaultBodyLimit::disable()),
|
.layer(axum::extract::DefaultBodyLimit::disable()),
|
||||||
)
|
);
|
||||||
.route("/uploads/{*path}", axum::routing::get(crate::api::image::serve_image))
|
|
||||||
.layer(crate::api::rate_limit::general_limit())
|
let static_routes = axum::Router::new()
|
||||||
.serve_dioxus_application(config, router::AppRouter)
|
.route("/uploads/{*path}", axum::routing::get(crate::api::image::serve_image));
|
||||||
|
|
||||||
|
let dioxus_app = axum::Router::new()
|
||||||
|
.serve_dioxus_application(config, router::AppRouter);
|
||||||
|
|
||||||
|
let router = api_routes
|
||||||
|
.merge(static_routes)
|
||||||
|
.merge(dioxus_app)
|
||||||
.layer(
|
.layer(
|
||||||
TraceLayer::new_for_http()
|
TraceLayer::new_for_http()
|
||||||
.make_span_with(
|
.make_span_with(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user