This commit is contained in:
2025-08-28 00:55:35 +08:00
commit 410f54a65e
93 changed files with 9863 additions and 0 deletions

View File

@ -0,0 +1,56 @@
use axum::{http::HeaderMap, http::header::AUTHORIZATION};
use chrono::{Utc, Duration as ChronoDuration};
use jsonwebtoken::{EncodingKey, DecodingKey, Header, Validation};
use serde::{Serialize, Deserialize};
use crate::error::AppError;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
pub sub: String,
pub uid: i64,
pub iss: String,
pub exp: usize,
pub typ: String, // access or refresh
}
pub fn encode_token(claims: &Claims, secret: &str) -> Result<String, AppError> {
let key = EncodingKey::from_secret(secret.as_bytes());
Ok(jsonwebtoken::encode(&Header::default(), claims, &key)?)
}
pub fn decode_token(token: &str, secret: &str) -> Result<Claims, AppError> {
let key = DecodingKey::from_secret(secret.as_bytes());
let data = jsonwebtoken::decode::<Claims>(token, &key, &Validation::default())?;
Ok(data.claims)
}
#[derive(Clone, Debug)]
pub struct AuthUser { pub uid: i64, pub username: String }
impl<S> axum::extract::FromRequestParts<S> for AuthUser where S: Send + Sync + 'static {
type Rejection = AppError;
async fn from_request_parts(parts: &mut axum::http::request::Parts, _state: &S) -> Result<Self, Self::Rejection> {
let headers: &HeaderMap = &parts.headers;
let auth = 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 = decode_token(token, &secret)?;
if claims.typ != "access" { return Err(AppError::Unauthorized); }
Ok(AuthUser { uid: claims.uid, username: claims.sub })
}
}
pub fn new_access_claims(uid: i64, username: &str) -> Claims {
let iss = std::env::var("JWT_ISS").unwrap_or_else(|_| "udmin".into());
let exp_secs: i64 = std::env::var("JWT_ACCESS_EXP_SECS").ok().and_then(|v| v.parse().ok()).unwrap_or(1800);
let exp = (Utc::now() + ChronoDuration::seconds(exp_secs)).timestamp() as usize;
Claims { sub: username.to_string(), uid, iss, exp, typ: "access".into() }
}
pub fn new_refresh_claims(uid: i64, username: &str) -> Claims {
let iss = std::env::var("JWT_ISS").unwrap_or_else(|_| "udmin".into());
let exp_secs: i64 = std::env::var("JWT_REFRESH_EXP_SECS").ok().and_then(|v| v.parse().ok()).unwrap_or(1209600);
let exp = (Utc::now() + ChronoDuration::seconds(exp_secs)).timestamp() as usize;
Claims { sub: username.to_string(), uid, iss, exp, typ: "refresh".into() }
}

View File

@ -0,0 +1,73 @@
use axum::{extract::State, http::{Request, HeaderMap, header::AUTHORIZATION}, middleware::Next, response::Response, body::Body};
use chrono::{Utc, FixedOffset};
use std::time::Instant;
use crate::{db::Db, services::log_service::{self, CreateLogInput}};
const BODY_LIMIT: usize = 256 * 1024; // 256 KiB 上限
fn parse_user(headers: &HeaderMap) -> (Option<i64>, Option<String>) {
if let Some(auth) = headers.get(AUTHORIZATION) {
if let Ok(s) = auth.to_str() {
if let Some(token) = s.strip_prefix("Bearer ") {
if let Ok(secret) = std::env::var("JWT_SECRET") {
if let Ok(claims) = crate::middlewares::jwt::decode_token(token, &secret) {
return (Some(claims.uid), Some(claims.sub));
}
}
}
}
}
(None, None)
}
pub async fn request_logger(State(db): State<Db>, req: Request<Body>, next: Next) -> Response {
// 跳过 GET 请求的日志记录
if *req.method() == axum::http::Method::GET {
return next.run(req).await;
}
let start = Instant::now();
let request_time = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
let path = req.uri().path().to_string();
let method = req.method().to_string();
let (user_id, username) = parse_user(req.headers());
// capture query and body up to a limit
let query = req.uri().query().unwrap_or("");
let mut req_params = if !query.is_empty() { format!("?{}", query) } else { String::new() };
// 读取并还原请求 Body限制大小
let (parts, body) = req.into_parts();
let body_bytes = match axum::body::to_bytes(body, BODY_LIMIT).await { Ok(b) => b, Err(_) => axum::body::Bytes::new() };
if !body_bytes.is_empty() {
let mut s = String::from_utf8_lossy(&body_bytes).to_string();
if s.len() > 4096 { s.truncate(4096); }
if !req_params.is_empty() { req_params.push_str(" "); }
req_params.push_str(&s);
}
let req = Request::from_parts(parts, Body::from(body_bytes.clone()));
let res = next.run(req).await;
// capture response body
let status = res.status().as_u16() as i32;
let duration_ms = start.elapsed().as_millis() as i64;
let (parts, body) = res.into_parts();
let resp_bytes = match axum::body::to_bytes(body, BODY_LIMIT).await { Ok(b) => b, Err(_) => axum::body::Bytes::new() };
let mut resp_str = String::from_utf8_lossy(&resp_bytes).to_string();
if resp_str.len() > 4096 { resp_str.truncate(4096); }
let res = Response::from_parts(parts, Body::from(resp_bytes));
let _ = log_service::create(&db, CreateLogInput {
path,
method,
request_params: if req_params.is_empty() { None } else { Some(req_params) },
response_params: if resp_str.is_empty() { None } else { Some(resp_str) },
status_code: status,
user_id,
username,
request_time,
duration_ms,
}).await;
res
}

View File

@ -0,0 +1,2 @@
pub mod jwt;
pub mod logging;