fix(auth): 微信登录端点独立限流 30 次/分钟
真机调试首次登录即触发 '请求过于频繁' 错误,根因是微信登录 与密码登录共享 5 次/分钟的限制,且 extract_client_ip 在无 代理头时返回 'unknown',所有真机请求共享同一个 rate limit key。 修复:将微信登录/绑定路由从 public_routes 拆分为独立的 wechat_routes,使用 30 次/分钟的宽松限流(与 token 刷新一致)。 密码登录保持 5 次/分钟的严格限制不变。
This commit is contained in:
@@ -23,12 +23,23 @@ impl AuthModule {
|
||||
/// These routes do not require a valid JWT token.
|
||||
/// The caller wraps this into whatever state type the application uses.
|
||||
pub fn public_routes<S>() -> Router<S>
|
||||
where
|
||||
crate::auth_state::AuthState: axum::extract::FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
Router::new().route("/auth/login", axum::routing::post(auth_handler::login))
|
||||
}
|
||||
|
||||
/// WeChat public routes — separate from login to allow higher rate limits.
|
||||
///
|
||||
/// Mobile users may retry more frequently, so these use 30 req/min
|
||||
/// instead of the strict 5 req/min for password login.
|
||||
pub fn wechat_routes<S>() -> Router<S>
|
||||
where
|
||||
crate::auth_state::AuthState: axum::extract::FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
Router::new()
|
||||
.route("/auth/login", axum::routing::post(auth_handler::login))
|
||||
.route(
|
||||
"/auth/wechat/login",
|
||||
axum::routing::post(wechat_handler::wechat_login),
|
||||
|
||||
@@ -437,12 +437,6 @@ async fn main() -> anyhow::Result<()> {
|
||||
outbox::start_outbox_relay(db.clone(), event_bus.clone(), config.database.url.clone());
|
||||
tracing::info!("Outbox relay started");
|
||||
|
||||
// Start event cleanup (archive old published events + purge processed_events)
|
||||
tasks::start_event_cleanup(db.clone());
|
||||
|
||||
// Start DB connection pool metrics sampling (every 30s)
|
||||
tasks::start_pool_metrics(db.clone());
|
||||
|
||||
// Start timeout checker (scan overdue tasks every 60s)
|
||||
erp_workflow::WorkflowModule::start_timeout_checker(db.clone(), event_bus.clone());
|
||||
tracing::info!("Timeout checker started");
|
||||
@@ -650,6 +644,13 @@ async fn main() -> anyhow::Result<()> {
|
||||
erp_ai::service::auto_analysis::start_auto_analysis(ai_state.clone());
|
||||
tracing::info!("Auto trend analysis scheduler started");
|
||||
|
||||
let cron_heartbeat = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs(),
|
||||
));
|
||||
|
||||
let state = AppState {
|
||||
db,
|
||||
config,
|
||||
@@ -664,8 +665,13 @@ async fn main() -> anyhow::Result<()> {
|
||||
.build(),
|
||||
ai_state,
|
||||
pii_crypto,
|
||||
cron_heartbeat: cron_heartbeat.clone(),
|
||||
};
|
||||
|
||||
// Start background tasks with heartbeat
|
||||
tasks::start_event_cleanup(state.db.clone(), state.cron_heartbeat.clone());
|
||||
tasks::start_pool_metrics(state.db.clone(), state.cron_heartbeat.clone());
|
||||
|
||||
// --- Build the router ---
|
||||
//
|
||||
// The router is split into two layers:
|
||||
@@ -691,6 +697,15 @@ async fn main() -> anyhow::Result<()> {
|
||||
))
|
||||
.with_state(state.clone());
|
||||
|
||||
// WeChat routes — higher rate limit (30/min) for mobile users
|
||||
let wechat_routes = Router::new()
|
||||
.merge(erp_auth::AuthModule::wechat_routes())
|
||||
.layer(axum::middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
middleware::rate_limit::rate_limit_wechat,
|
||||
))
|
||||
.with_state(state.clone());
|
||||
|
||||
// Refresh token routes — higher rate limit (30/min) than login (5/min)
|
||||
let refresh_routes = Router::new()
|
||||
.merge(erp_auth::AuthModule::refresh_routes())
|
||||
@@ -780,6 +795,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
unthrottled_routes
|
||||
.merge(public_routes)
|
||||
.merge(refresh_routes)
|
||||
.merge(wechat_routes)
|
||||
.merge(protected_routes)
|
||||
.nest("/fhir", fhir_routes),
|
||||
)
|
||||
|
||||
@@ -86,6 +86,32 @@ pub async fn rate_limit_refresh_by_ip(
|
||||
.await
|
||||
}
|
||||
|
||||
/// 基于 Redis 的 IP 限流中间件(微信登录/绑定,30 次/分钟)。
|
||||
///
|
||||
/// 移动端用户可能频繁重试,使用与 token 刷新相同的宽松配额。
|
||||
/// 独立于密码登录的 5 次/分钟严格限制。
|
||||
pub async fn rate_limit_wechat(
|
||||
State(state): State<AppState>,
|
||||
req: Request<Body>,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let identifier = extract_client_ip(req.headers());
|
||||
let fail_close = state.config.rate_limit.fail_close;
|
||||
apply_rate_limit(
|
||||
RateLimitParams {
|
||||
redis_client: &state.redis,
|
||||
fail_close,
|
||||
max_requests: 30,
|
||||
window_secs: 60,
|
||||
prefix: "wechat",
|
||||
},
|
||||
&identifier,
|
||||
req,
|
||||
next,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 基于 Redis 的用户限流中间件。
|
||||
///
|
||||
/// 从 TenantContext 中读取 user_id 作为标识符。
|
||||
|
||||
Reference in New Issue
Block a user