likes
comments
collection
share

使用 Rust 开发 gRPC 服务:拦截器和认证在 gRPC 服务架构中,拦截器是一个核心概念,它在请求处理流程中扮演

作者站长头像
站长
· 阅读数 39

拦截器和认证

拦截器

在 gRPC 服务架构中,拦截器是一个核心概念,它在请求处理流程中扮演着重要角色。拦截器能够在不修改核心业务逻辑的情况下,实现诸如认证、授权和日志记录等横切关注点,从而提高服务的安全性和可维护性。

拦截器主要分为两类:服务端拦截器和客户端拦截器。服务端拦截器在服务器接收到请求后、执行实际处理逻辑之前进行拦截,可以用于请求验证、日志记录等。客户端拦截器则在客户端发送请求之前进行拦截,常用于添加认证信息、请求跟踪等场景。这两种拦截器共同构成了 gRPC 通信中的重要保障机制。

服务端拦截器

使用 tower 中间件

tonic 基于 Axum 实现,所以 tower 生态的各类中间件都可以直接使用。比如:需要打印每个 gRPC 请求的日志,可以使用 tower-http 中的 TraceLayer 中间件。

添加 tower-http 依赖:

cargo add tracing-subscriber
cargo add tower-http --features trace

main.rs 中添加 tracing_subscriber 初始化日志,并添加 TraceLayer 中间件:

// ...
use tower_http::trace::TraceLayer;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
  tracing_subscriber::fmt::init();

  // ...

  Server::builder()
    .layer(TraceLayer::new_for_grpc())
    .add_routes(make_grpc_routes())
    .serve(grpc_addr)
    .await?;

  Ok(())
}

使用 RUST_LOG="tower_http=debug" cargo run 启动服务,使用 grpcurl 发起 gRPC 请求。这时,就可以在终端看到 gRPC 请求的日志:

2024-09-19T14:19:01.671092Z DEBUG request{method=POST uri=http://localhost:9999/getting.v1.Auth/Signin version=HTTP/2.0}: tower_http::trace::on_request: started processing request
2024-09-19T14:19:01.671302Z DEBUG request{method=POST uri=http://localhost:9999/getting.v1.Auth/Signin version=HTTP/2.0}: tower_http::trace::on_response: finished processing request latency=0 ms
2024-09-19T14:19:01.671420Z DEBUG request{method=POST uri=http://localhost:9999/getting.v1.Auth/Signin version=HTTP/2.0}: tower_http::trace::on_eos: end of stream stream_duration=0 ms status=0

可以看到,一个 gRPC 请求的完整生命周期,包括请求开始(on_request)、响应结束(on_response)、请求结束(on_eos)。

默认 Rust 将捕获所有日志输出,需要在启动服务时添加 RUST_LOG="tower_http=debug" 环境变量才能在终端看到日志输出。Windows 系统需要使用不同的方式设置环境变量。

CMD:

set RUST_LOG=tower_http=debug && cargo run

Powershell:

$env:RUST_LOG="tower_http=debug"; cargo run
使用 tonic interceptor

tonic 还提供了 tonic::Interceptor trait,用于在服务端/客户端拦截器中使用。我们对 UserService 添加一个 Authentication Interceptor,用于在服务端拦截器中验证请求的认证信息。

定义拦截器函数:

pub fn auth_interceptor(request: tonic::Request<()>) -> Result<tonic::Request<()>, tonic::Status> {
  let authorization = request
    .metadata()
    .get("authorization")
    .ok_or_else(|| tonic::Status::unauthenticated("No auth token provided"))?
    .to_str()
    .map_err(|e| tonic::Status::unauthenticated(e.to_string()))?;
  let token = &authorization["Berere ".len()..];
  if token != SESSION_TOKEN {
    return Err(tonic::Status::unauthenticated("Invalid auth token"));
  }
  Ok(request)
}

上面代码首先从请求元数据中获取 authorization 信息,然后验证 token 是否正确(这里为演示简单,直接使用了一个固定值: SESSION_TOKEN)。若 token 验证失败,则返回 Status::unauthenticated 错误,tonic 会自动将错误转换为响应给客户端。

要应用拦截器,只需要调用 UserServer::with_interceptor 方法,在第2个参数传入 auth_interceptor 函数即可:

UserServer::with_interceptor(UserService, auth_interceptor)

这时候再次发起 gRPC 的 User/Update 请求,则会收到 Unauthenticated 错误:

grpcurl -plaintext -import-path ./proto \
  -proto getting/v1/user.proto \
  -d '{
    "id": 1,
    "name": "yangbajing"
  }' \
  localhost:9999 getting.v1.User/Update
# 下面是响应输出
ERROR:
  Code: Unauthenticated
  Message: No auth token provided

可以通过为 grpcurl 命令添加 -H 选项,指定 authorization 元数据(HTTP Header)。如:-H 'authorization: Bearer L1AhTRgFMiTkQMuGf8PnY6yHAmaV72ESQsEzo0cVWmiodIEx'

客户端拦截器

客户端拦截器用于在客户端发送请求时执行一些逻辑。常用的场景是添加认证信息,比如:session token。

我们通过 example 来演示客户端拦截器的用法,创建 examples/client-interceptor.rs 文件,添加以下 Rust 代码:

use tonic::{transport::Channel, Code, Request, Status};
use tonic_getting::pb::getting::v1::{user_client::UserClient, UpdateUserRequest};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
  let channel = Channel::from_static("http://localhost:9999").connect().await?;

  let mut client = UserClient::with_interceptor(channel, |mut request: Request<()>| {
    let token = "Bearer L1AhTRgFMiTkQMuGf8PnY6yHAmaV72ESQsEzo0cVWmiodIEx"
      .parse()
      .map_err(|_| Status::new(Code::Internal, "InvalidMetadataValue"))?;
    request.metadata_mut().insert("authorization", token);
    Ok(request)
  });

  let request = Request::new(UpdateUserRequest { id: 1, name: Some("yangbajing".to_string()), status: Some(100) });

  let response = client.update(request).await?;

  println!("Response: {:?}", response);

  Ok(())
}

客户端拦截器也使用了 with_interceptor 方法,这里与服务端拦截器不同的是,在第2个参数传入一个闭包函数。当然,也可以像服务端一样定义一个拦截器函数使用。

通过以下命令运行示例程序 cargo run --example client-interceptor,可以看到 User/Update 请求成功。

小结

本文完整示例代码见:github.com/yangbajing/…

下一篇文章计划介绍下怎样使用 gRPC、gRPC-WEB 来整合 Next.js 和 Rust:《Next.js + Rust 全栈开发:集成 gRPC 和 gRPC-WEB》

转载自:https://juejin.cn/post/7416204916038893578
评论
请登录