init
This commit is contained in:
56
backend/src/middlewares/jwt.rs
Normal file
56
backend/src/middlewares/jwt.rs
Normal 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() }
|
||||
}
|
||||
73
backend/src/middlewares/logging.rs
Normal file
73
backend/src/middlewares/logging.rs
Normal 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
|
||||
}
|
||||
2
backend/src/middlewares/mod.rs
Normal file
2
backend/src/middlewares/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod jwt;
|
||||
pub mod logging;
|
||||
Reference in New Issue
Block a user