Skip to main content

tdm_server_rust/middleware/
auth.rs

1//! 登录鉴权与路由权限中间件
2//!
3//! 对齐 Java `LoginCheckInterceptor` + `PermissionCheckInterceptor`。
4//!
5//! ## 鉴权流程
6//!
7//! 1. 放行 `/login`、`/reg`、`OPTIONS` 请求
8//! 2. 从 Header `token` 或 `Authorization` 提取 JWT
9//! 3. 解析 Token → 加载组员信息
10//! 4. 特殊路由权限校验(evaluations、takeEpisode、download 等)
11//! 5. 注入 [`AuthMember`] 到请求扩展
12
13use crate::{
14    app::AppState,
15    cache::get_auth_snapshot_cached,
16    common::result::permission_denied_response,
17    entity::member::Member,
18    error::AppError,
19    utils::jwt::{JwtClaims, JwtUtil},
20};
21use axum::{
22    body::Body,
23    extract::State,
24    http::{Method, Request},
25    middleware::Next,
26    response::Response,
27};
28
29/// 请求扩展中的当前组员
30#[derive(Debug, Clone)]
31pub struct AuthMember(pub Option<Member>);
32
33/// 鉴权中间件
34#[tracing::instrument(skip_all, level = "info")]
35pub async fn auth_middleware(
36    State(state): State<AppState>,
37    mut req: Request<Body>,
38    next: Next,
39) -> Result<Response, AppError> {
40    let uri = req.uri().path().to_string();
41    let method = req.method().clone();
42
43    if uri.contains("login") || uri.contains("reg") {
44        req.extensions_mut().insert(AuthMember(None));
45        return Ok(next.run(req).await);
46    }
47
48    if method == Method::OPTIONS {
49        req.extensions_mut().insert(AuthMember(None));
50        return Ok(next.run(req).await);
51    }
52
53    if uri.contains("undefined") {
54        return Err(AppError::login_expired("未登录哦 快加入提灯喵接坑吧喵!"));
55    }
56
57    let token = extract_token(req.headers());
58    if method != Method::GET && token.is_none() {
59        return Err(AppError::login_expired("请登录一下哦喵……"));
60    }
61
62    let mut member: Option<Member> = None;
63    if let Some(jwt) = token.as_deref() {
64        let claims = parse_token(&state, jwt)?;
65        member = Some(load_member(&state, claims.id).await?);
66    }
67
68    check_route_permission(&uri, token.as_deref())?;
69
70    if uri.starts_with("/api/evaluations") {
71        if let Some(ref m) = member {
72            let posts = &m.post_ids;
73            let has = posts.contains(&2) || posts.contains(&4);
74            if !has && !posts.contains(&4) {
75                return Ok(permission_denied_response());
76            }
77        } else {
78            return Ok(permission_denied_response());
79        }
80    }
81
82    req.extensions_mut().insert(AuthMember(member));
83    Ok(next.run(req).await)
84}
85
86/// 解析 JWT token
87#[tracing::instrument(name = "auth::parse_token", skip(state, token), level = "info")]
88fn parse_token(state: &AppState, token: &str) -> Result<JwtClaims, AppError> {
89    let util = JwtUtil::new(&state.config.jwt.sign_key, state.config.jwt.expire_ms);
90    util.parse(token).map_err(|e| {
91        if e.to_string().contains("ExpiredSignature") {
92            AppError::login_expired("登录数据过期,请重新登录喵!")
93        } else {
94            AppError::login_expired("Token解析失败喵!请重新登录喵!")
95        }
96    })
97}
98
99/// 从数据库加载组员
100#[tracing::instrument(name = "auth::load_member", skip(state), level = "info")]
101async fn load_member(state: &AppState, member_id: i32) -> Result<Member, AppError> {
102    let m = get_auth_snapshot_cached(state, member_id).await?;
103    if crate::entity::enums::MemberInternEnum::is_left(m.intern) {
104        return Err(AppError::login_expired("你已经退出了提灯喵猫娘化计划喵……"));
105    }
106    Ok(m)
107}
108
109/// 路由级权限校验
110#[tracing::instrument(name = "auth::check_permission", skip(uri, token), level = "info")]
111fn check_route_permission(uri: &str, token: Option<&str>) -> Result<(), AppError> {
112    let needs_strict = uri.contains("takeEpisode")
113        || uri.contains("invitationCodes")
114        || uri.contains("download");
115    if needs_strict && token.is_none() {
116        return Err(AppError::login_expired("必须要登录才能访问这里喵!"));
117    }
118    Ok(())
119}
120
121/// 从请求头提取 token
122fn extract_token(headers: &axum::http::HeaderMap) -> Option<String> {
123    headers
124        .get("token")
125        .or_else(|| headers.get("Authorization"))
126        .and_then(|v| v.to_str().ok())
127        .map(|s| s.to_string())
128}