当前位置:首页 > 技术 > 正文内容

Smithy 结合 Axum 实战:业务逻辑对接与用户鉴权机制

访客 技术 2026年7月1日 1

通过编写 .smithy 接口定义语言(IDL)文件,smithy-rs 工具链能够为我们生成类型安全的 Axum 服务端骨架。该骨架包含了数据模型、错误枚举、业务接口 Trait 以及可运行的 Axum Router。然而,生成的 Trait 方法默认均为 unimplemented!()。接下来的核心任务是赋予这个骨架实际的业务能力:将底层 Service 和 Repository 的逻辑接入生成的接口,并整合身份验证功能。

1. 实现自动生成的 Trait 接口

smithy-rs 产出的核心接口(例如 chat_server::server::ChatService)需要我们提供具体实现。我们将创建一个名为 ChatApiHandler 的结构体来承载这些逻辑。

use crate::ServerContext;
use chat_server::{
    error::{ChatServiceError, ListConversationsError, GetConversationError, NotFoundError},
    input::{GetConversationInput, ListConversationsInput},
    model::Conversation,
    output::ListConversationsOutput,
    server::ChatService,
};
use async_trait::async_trait;

#[derive(Debug, Clone)]
pub struct ChatApiHandler {
    ctx: ServerContext,
}

impl ChatApiHandler {
    pub fn with_context(ctx: ServerContext) -> Self {
        Self { ctx }
    }
}

#[async_trait]
impl ChatService for ChatApiHandler {
    async fn list_conversations(
        &self,
        req: ListConversationsInput,
    ) -> Result {
        tracing::info!("Fetching conversation list: {:?}", req);
        
        // 构造模拟响应数据
        let mock_data = vec![
            Conversation::builder().id("c-101").user_id(42).title("Project Sync").build().unwrap(),
            Conversation::builder().id("c-102").user_id(42).title("Weekly Review").build().unwrap(),
        ];
        
        Ok(ListConversationsOutput::builder()
            .set_conversations(Some(mock_data))
            .build()
            .unwrap())
    }

    async fn get_conversation(
        &self,
        req: GetConversationInput,
    ) -> Result {
        let target_id = req.id();
        tracing::info!("Retrieving conversation details for ID: {}", target_id);

        if target_id == "not-found" {
            let err = NotFoundError::builder()
                .message("The requested conversation does not exist.")
                .build();
            return Err(ChatServiceError::from(GetConversationError::NotFoundError(err)));
        }
        
        Ok(Conversation::builder()
            .id(target_id)
            .user_id(42)
            .title("Detailed Discussion")
            .build()
            .unwrap())
    }
}

在错误处理方面,当发生异常时,需将具体的错误结构体(如 NotFoundError)逐层包装为操作级错误(GetConversationError),最终转换为服务级错误枚举(ChatServiceError),以保证严格的类型安全。

2. 状态注入与依赖管理

ChatApiHandler 需要访问数据库连接池和内部服务,这些通常封装在 ServerContext 中。我们可以借助 Axum 的 Extension 中间件将上下文注入到请求生命周期中。

use chat_server::server::ChatServiceServer;
use std::net::SocketAddr;
use tower::ServiceBuilder;
use axum::Extension;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // 初始化包含数据库池和内部服务的上下文
    let app_ctx = ServerContext::init().await?;

    // 实例化 API 处理器
    let api_handler = ChatApiHandler::with_context(app_ctx.clone());

    // 构建 Axum 路由并注入上下文
    let router = ChatServiceServer::new(api_handler)
        .layer(
            ServiceBuilder::new()
                .layer(Extension(app_ctx))
        );

    let bind_addr: SocketAddr = "127.0.0.1:8080".parse()?;
    tracing::info!("Server started on {}", bind_addr);
    
    axum::Server::bind(&bind_addr)
        .serve(router.into_make_service())
        .await?;

    Ok(())
}

smithy-rs 在底层为生成的服务器实现了 FromRequestParts,能够自动从请求的 extensions 中提取所需的状态,使得依赖注入过程对开发者几乎透明。

3. 桥接核心业务层

接下来,将生成的接口与实际的内部业务服务(如 ChatDomainService)对接,完成模型转换与错误映射。

use crate::domain::ChatDomainService;

#[async_trait]
impl ChatService for ChatApiHandler {
    async fn list_conversations(
        &self,
        req: ListConversationsInput,
    ) -> Result {
        let uid = 42; // 假设从鉴权上下文中获取

        // 调用领域服务获取内部模型
        let domain_models = self.ctx.chat_service
            .fetch_user_chats(uid)
            .await
            .map_err(translate_domain_error)?;

        // 将内部模型映射为 Smithy 生成的 API 模型
        let api_models: Vec<Conversation> = domain_models
            .into_iter()
            .map(|m| Conversation::builder()
                .id(m.uuid.to_string())
                .user_id(m.owner_id)
                .title(m.subject)
                .build()
                .unwrap())
            .collect();
            
        Ok(ListConversationsOutput::builder()
            .set_conversations(Some(api_models))
            .build()
            .unwrap())
    }
}

fn translate_domain_error(err: anyhow::Error) -> ChatServiceError {
    // 实际项目中可根据 err 的具体类型进行精细化匹配
    ChatServiceError::InternalServerError(
        chat_server::error::InternalServerError::builder()
            .message(format!("Internal failure: {}", err))
            .build()
    )
}

这种设计实现了内部领域模型与外部 API 契约的彻底解耦。虽然增加了模型转换的代码量,但确保了 API 演进不会直接影响核心业务逻辑,同时通过 translate_domain_error 实现了内部异常到 API 标准错误的精确映射。

4. 集成用户鉴权机制

为了保护接口,我们需要引入身份验证。虽然 Smithy 提供了 @auth 注解来声明认证方案,但在 Axum 生态中,结合 Tower 中间件和请求扩展(Extensions)往往更加灵活且易于调试。

通过在 ChatApiHandler 的方法签名中直接引入 Axum 的提取器,可以优雅地获取鉴权信息:

use axum::Extension;
use crate::auth::AuthenticatedUser;

#[async_trait]
impl ChatService for ChatApiHandler {
    async fn list_conversations(
        &self,
        Extension(current_user): Extension<AuthenticatedUser>,
        req: ListConversationsInput,
    ) -> Result {
        // 使用从 Token 中解析出的真实用户 ID
        let domain_models = self.ctx.chat_service
            .fetch_user_chats(current_user.uid)
            .await
            .map_err(translate_domain_error)?;
            
        // 后续模型转换与响应构建逻辑与前述一致
        let api_models: Vec<Conversation> = domain_models
            .into_iter()
            .map(|m| Conversation::builder()
                .id(m.uuid.to_string())
                .user_id(m.owner_id)
                .title(m.subject)
                .build()
                .unwrap())
            .collect();

        Ok(ListConversationsOutput::builder()
            .set_conversations(Some(api_models))
            .build()
            .unwrap())
    }
}

现代版本的 smithy-rs 深度兼容 Axum 的提取器机制。只需在 Trait 方法的参数列表中前置 Extension 提取器,代码生成器便会自动处理 HTTP 请求头解析与上下文注入。这使得鉴权逻辑与业务代码完美融合,无需在路由层编写冗余的拦截逻辑,同时保持了处理函数签名的清晰与类型安全。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。