fbc_starter/auth/context.rs
1use axum::{
2 extract::{FromRequestParts, Request},
3 http::{request::Parts, StatusCode},
4 middleware::Next,
5 response::{IntoResponse, Response},
6};
7
8/// 请求上下文(网关透传的用户信息)
9///
10/// 网关验证 JWT 后,将用户信息通过 HTTP 头透传到下游服务:
11/// - `X-User-Id`: 用户 ID
12/// - `X-Tenant-Id`: 租户 ID(可选)
13/// - `X-Username`: 用户名
14#[derive(Debug, Clone)]
15pub struct RequestContext {
16 /// 用户 ID
17 pub user_id: i64,
18 /// 租户 ID(可选,无租户时为 None)
19 pub tenant_id: Option<i64>,
20 /// 用户名
21 pub username: String,
22}
23
24/// 中间件:从网关透传的 X-User-* Header 中解析用户上下文
25///
26/// 将解析后的 `RequestContext` 注入到请求扩展中,
27/// 下游 handler 可通过 Axum extractor 直接使用。
28///
29/// 注意:如果请求头中没有 X-User-Id(白名单接口),
30/// 中间件仍然放行但不注入上下文。Handler 需要自行判断。
31pub async fn user_context_middleware(request: Request, next: Next) -> Response {
32 let mut request = request;
33
34 // 尝试从 header 解析用户上下文
35 let user_id = request
36 .headers()
37 .get("X-User-Id")
38 .and_then(|v| v.to_str().ok())
39 .and_then(|s| s.parse::<i64>().ok());
40
41 if let Some(user_id) = user_id {
42 let tenant_id = request
43 .headers()
44 .get("X-Tenant-Id")
45 .and_then(|v| v.to_str().ok())
46 .and_then(|s| s.parse::<i64>().ok());
47
48 let username = request
49 .headers()
50 .get("X-Username")
51 .and_then(|v| v.to_str().ok())
52 .unwrap_or("")
53 .to_string();
54
55 let ctx = RequestContext {
56 user_id,
57 tenant_id,
58 username,
59 };
60
61 request.extensions_mut().insert(ctx);
62 }
63
64 next.run(request).await
65}
66
67/// Axum Extractor:从请求扩展中提取 RequestContext(必须认证)
68///
69/// 使用方式:
70/// ```rust
71/// async fn handler(ctx: RequestContext) -> impl IntoResponse {
72/// format!("Hello, user {}", ctx.user_id)
73/// }
74/// ```
75///
76/// 如果请求未经网关认证(无 X-User-Id header),返回 401。
77impl<S> FromRequestParts<S> for RequestContext
78where
79 S: Send + Sync,
80{
81 type Rejection = Response;
82
83 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
84 parts
85 .extensions
86 .get::<RequestContext>()
87 .cloned()
88 .ok_or_else(|| {
89 (StatusCode::UNAUTHORIZED, "Missing user context").into_response()
90 })
91 }
92}
93
94// 可选的请求上下文(白名单接口使用)
95//
96// 使用方式:
97// ```rust
98// async fn handler(ctx: Option<RequestContext>) -> impl IntoResponse {
99// match ctx {
100// Some(c) => format!("Hello, user {}", c.user_id),
101// None => "Hello, anonymous".to_string(),
102// }
103// }
104// ```
105//
106// 注意:`Option<RequestContext>` 由 Axum 原生支持,
107// 当 `RequestContext` 提取失败时返回 `None`,无需额外实现。