mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 16:06:08 +00:00
fix(api): bucket admin login throttling by client ip
The AdminLoginSource extension was never injected, so every login request fell back to the shared "unknown" failure bucket: five wrong passwords from any unauthenticated visitor locked the real administrator out for fifteen minutes, renewable indefinitely. Attach a login-route middleware that keys the bucket on the socket peer IP taken from ConnectInfo; forwarded headers are client-forgeable and stay excluded, so reverse-proxy deployments retain the previous shared-bucket behavior. Add a router-level regression test proving throttling of one source leaves other sources unaffected. Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
//! 管理员登录、会话与管理请求鉴权接线。
|
||||
|
||||
use std::fmt;
|
||||
use std::{fmt, net::SocketAddr};
|
||||
|
||||
use axum::{
|
||||
Extension, Json, Router,
|
||||
extract::{FromRequestParts, State},
|
||||
extract::{ConnectInfo, FromRequestParts, Request, State},
|
||||
http::{HeaderMap, HeaderValue, StatusCode, header::SET_COOKIE, request::Parts},
|
||||
middleware::{self, Next},
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
};
|
||||
@@ -207,11 +208,30 @@ where
|
||||
S: AdminSessionState + Clone + Send + Sync + 'static,
|
||||
{
|
||||
Router::new()
|
||||
.route("/api/admin/auth/login", post(login::<S>))
|
||||
.route(
|
||||
"/api/admin/auth/login",
|
||||
post(login::<S>).layer(middleware::from_fn(attach_login_source)),
|
||||
)
|
||||
.route("/api/admin/auth/status", get(session_status::<S>))
|
||||
.route("/api/admin/auth/logout", post(logout::<S>))
|
||||
}
|
||||
|
||||
/// 登录失败限流以 socket 对端 IP 为计数桶,使不同来源的失败互不影响;
|
||||
/// 转发头(X-Forwarded-For 等)可由客户端伪造,不参与来源判定。
|
||||
/// 经反向代理部署时所有请求同源于代理地址,退化为共享单桶。
|
||||
async fn attach_login_source(mut request: Request, next: Next) -> Response {
|
||||
if let Some(ConnectInfo(peer)) = request
|
||||
.extensions()
|
||||
.get::<ConnectInfo<SocketAddr>>()
|
||||
.copied()
|
||||
{
|
||||
request
|
||||
.extensions_mut()
|
||||
.insert(AdminLoginSource::new(peer.ip().to_string()));
|
||||
}
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
async fn login<S>(
|
||||
State(state): State<S>,
|
||||
source: Option<Extension<AdminLoginSource>>,
|
||||
|
||||
@@ -172,3 +172,57 @@ async fn audit_failure_should_revoke_new_session_before_returning_it() {
|
||||
);
|
||||
assert_eq!(fixture.auth.session_count(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_route_should_bucket_throttling_by_client_ip() {
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use axum::{body::Body, extract::ConnectInfo, http::Request};
|
||||
use tower::ServiceExt as _;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AuthState(gateway_admin::AdminServices);
|
||||
|
||||
impl gateway_api::admin::AdminSessionState for AuthState {
|
||||
fn admin_services(&self) -> &gateway_admin::AdminServices {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
let fixture = AdminTestFixture::new().await;
|
||||
let app = gateway_api::admin::auth::router::<AuthState>()
|
||||
.with_state(AuthState(fixture.services.clone()));
|
||||
|
||||
let failed_login_from = |ip: [u8; 4]| {
|
||||
let app = app.clone();
|
||||
async move {
|
||||
let mut request = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/admin/auth/login")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
r#"{"username":"admin_1","password":"wrong-password"}"#,
|
||||
))
|
||||
.expect("build login request");
|
||||
request
|
||||
.extensions_mut()
|
||||
.insert(ConnectInfo(SocketAddr::from((ip, 50_000))));
|
||||
app.oneshot(request).await.expect("login response").status()
|
||||
}
|
||||
};
|
||||
|
||||
for _ in 0..4 {
|
||||
assert_eq!(
|
||||
failed_login_from([10, 0, 0, 1]).await,
|
||||
axum::http::StatusCode::UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
failed_login_from([10, 0, 0, 1]).await,
|
||||
axum::http::StatusCode::TOO_MANY_REQUESTS
|
||||
);
|
||||
assert_eq!(
|
||||
failed_login_from([10, 0, 0, 2]).await,
|
||||
axum::http::StatusCode::UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user