You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Actix-web中间件中修改响应类型及优化JWT验证

问题解答

1. 返回JSON格式的未授权响应

actix-web默认的ErrorUnauthorized返回纯文本响应,要改成JSON格式,需要自定义错误类型并实现ResponseError trait,步骤如下:

定义自定义错误类型

创建枚举表示JWT验证错误,并实现响应转换逻辑:

use actix_web::{http::StatusCode, HttpResponse, ResponseError};
use serde::Serialize;

#[derive(Debug, Serialize)]
struct ErrorResponse {
    error: String,
    message: String,
}

#[derive(Debug)]
enum AuthError {
    InvalidToken,
    ExpiredToken,
    Unauthorized,
    MissingAuthHeader,
}

impl ResponseError for AuthError {
    fn error_response(&self) -> HttpResponse {
        let (status_code, error, message) = match self {
            AuthError::InvalidToken => (
                StatusCode::UNAUTHORIZED,
                "invalid_token",
                "提供的JWT令牌无效",
            ),
            AuthError::ExpiredToken => (
                StatusCode::UNAUTHORIZED,
                "expired_token",
                "JWT令牌已过期",
            ),
            AuthError::Unauthorized => (
                StatusCode::UNAUTHORIZED,
                "unauthorized",
                "无权访问此资源",
            ),
            AuthError::MissingAuthHeader => (
                StatusCode::BAD_REQUEST,
                "missing_auth_header",
                "缺少Authorization请求头",
            ),
        };

        HttpResponse::build(status_code)
            .json(ErrorResponse {
                error: error.to_string(),
                message: message.to_string(),
            })
    }
}

// 实现转换为actix_web::Error
impl From<AuthError> for actix_web::Error {
    fn from(err: AuthError) -> Self {
        actix_web::error::ErrorUnauthorized(err.error_response())
    }
}

修改中间件错误返回

在JWTMiddleware的call方法中,替换默认错误为自定义错误,并优化Header处理逻辑:

fn call(&self, req: ServiceRequest) -> Self::Future {
    info!("JWT verification started...");
    let auth_header = req.headers().get("Authorization");

    // 提前处理Header错误
    let token = match auth_header {
        Some(header_value) => {
            let value_str = match header_value.to_str() {
                Ok(s) => s,
                Err(_) => return Box::pin(async move { Err(AuthError::Unauthorized.into()) }),
            };
            value_str.strip_prefix("Bearer ").ok_or(AuthError::InvalidToken)
        }
        None => Err(AuthError::MissingAuthHeader),
    };

    let token = match token {
        Ok(t) => t,
        Err(e) => return Box::pin(async move { Err(e.into()) }),
    };

    // 提前加载的密钥(建议在main中初始化全局变量)
    let secret_key = var("SECRET_KEY").expect("SECRET_KEY not set");
    let decoding_key = DecodingKey::from_secret(secret_key.as_ref());

    let fut = self.service.call(req);

    match decode::<Claims>(&token, &decoding_key, &Validation::new(Algorithm::HS256)) {
        Ok(_) => Box::pin(async move { Ok(fut.await?) }),
        Err(err) => {
            let auth_error = match err.kind() {
                ErrorKind::InvalidToken => AuthError::InvalidToken,
                ErrorKind::ExpiredSignature => AuthError::ExpiredToken,
                _ => AuthError::Unauthorized,
            };
            Box::pin(async move { Err(auth_error.into()) })
        }
    }
}

2. Rust中更简便的JWT验证方式

你的实现已经基础正确,可通过以下方式优化或简化:

优化现有代码

  • 提前加载环境变量:不要在call方法中重复调用dotenv()和var(),在main函数中初始化全局变量:

    use once_cell::sync::Lazy;
    
    dotenv().expect("Failed to load .env file");
    pub static SECRET_KEY: Lazy<String> = Lazy::new(|| var("SECRET_KEY").expect("SECRET_KEY not set"));
    

    需在Cargo.toml添加依赖:once_cell = "1.19.0"

  • 避免unwrap:用expect()或错误处理替代unwrap(),防止panic。

使用提取器简化验证

实现FromRequest trait做JWT提取器,可直接在路由函数中获取验证后的Claims:

use actix_web::{dev::Payload, FromRequest, HttpRequest};
use std::future::{ready, Ready};

pub struct JwtClaims(pub Claims);

impl FromRequest for JwtClaims {
    type Error = actix_web::Error;
    type Future = Ready<Result<Self, Self::Error>>;

    fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
        let auth_header = req.headers().get("Authorization");
        let token = match auth_header {
            Some(header) => header.to_str().map_err(|_| AuthError::Unauthorized)?,
            None => return ready(Err(AuthError::MissingAuthHeader.into())),
        };

        let token = token.strip_prefix("Bearer ").ok_or(AuthError::InvalidToken)?;

        let decoding_key = DecodingKey::from_secret(SECRET_KEY.as_ref());
        let validation = Validation::new(Algorithm::HS256);

        match decode::<Claims>(token, &decoding_key, &validation) {
            Ok(claims) => ready(Ok(JwtClaims(claims.claims))),
            Err(err) => {
                let auth_error = match err.kind() {
                    ErrorKind::InvalidToken => AuthError::InvalidToken,
                    ErrorKind::ExpiredSignature => AuthError::ExpiredToken,
                    _ => AuthError::Unauthorized,
                };
                ready(Err(auth_error.into()))
            }
        }
    }
}

路由中直接使用:

#[get("/protected")]
async fn protected_route(claims: JwtClaims) -> HttpResponse {
    HttpResponse::Ok().json(claims.0)
}

3. 中间件vs工具函数:推荐方案

中间件优势

  • 统一处理多路由验证,符合DRY原则,减少重复代码
  • 全局控制验证规则,统一错误响应格式
  • 维护成本低,无需在每个控制器中重复编写验证逻辑

工具函数优势

  • 适合少数路由需要自定义验证逻辑的场景
  • 局部使用更灵活,无需全局配置

推荐选择

优先使用中间件,因为JWT验证是API通用逻辑,中间件能集中管理规则和响应,提升可维护性。若有特殊路由需要自定义验证,可在中间件中排除这些路由,再单独用工具函数处理。

内容的提问来源于stack exchange,提问作者Tycho

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 01:22:04