跳到主要内容

gRPC 服务器

Doubao 中英对照 GRPC Server

本节介绍 Spring gRPC 在服务器侧使用的核心概念。建议您仔细阅读,以理解 Spring gRPC 实现背后的设计思想。只要 classpath 中包含 Server 的一个实现,您只需提供一个或多个 BindableService 类型的 Bean,即可创建 gRPC 服务器。BindableService 是一种可绑定到服务器的 gRPC 服务。Server 则是监听传入请求并将其路由到相应服务实现的 gRPC 服务器。

创建 gRPC 服务​

要创建 gRPC 服务器,您需要为 GrpcServerFactory 提供一个或多个 BindableService 类型的 Bean。有一些现成的 BindableService 可供纳入您的应用(例如 gRPC 反射 或 gRPC 健康 服务)。最常见的做法是:通过扩展由 Protobuf 文件生成的服务实现来创建您自己的 BindableService。启用它最简单的方式,就是在实现类上加一个 Spring @Service 注解,让它被 Spring Boot 应用中的 @ComponentScan 扫描到。

服务过滤​

所有可用的 BindableService Bean 都会绑定到所有正在运行的 gRPC 服务器。不过,您可以注册一个 ServerServiceDefinitionFilter Bean,用来决定哪些服务绑定到哪些服务器工厂。

以下示例阻止「health」和「reflection」服务绑定到应用了该过滤器的服务器工厂所创建的服务器(例如 InProcessGrpcServerFactory)。

@Bean
ServerServiceDefinitionFilter myServiceFilter() {
return (serviceDefinition, __) ->
!Set.of(HealthGrpc.SERVICE_NAME, ServerReflectionGrpc.SERVICE_NAME)
.contains(serviceDefinition.getServiceDescriptor().getName());
}

InProcessGrpcServerFactory 会自动拾取 ServerServiceDefinitionFilter。对于其他任何服务器工厂,您需要提供一个 GrpcServerFactoryCustomizer,在其中通过添加过滤器来修改工厂,如下例所示:

@Bean
GrpcServerFactoryCustomizer myServerFactoryCustomizer(ServerServiceDefinitionFilter myServiceFilter) {
return factory -> {
if (factory instanceof NettyGrpcServerFactory nettyServerFactory) {
nettyServerFactory.setServiceFilter(myServiceFilter);
}
};
}

Netty 服务器​

如果您使用 NettyGrpcServerFactory,服务器将是基于 Netty 的实现。您可以使用一个 ServerBuilderCustomizer Bean,在 ServerBuilder 用于创建服务器之前对其进行定制,从而配置服务器的常用功能。

遮蔽 Netty​

您可以通过添加 grpc-netty-shaded 依赖,并在应用上下文中创建一个 ShadedNettyGrpcServerFactory Bean,切换到由 gRPC 团队提供的遮蔽 Netty。

Servlet 服务器​

任何 Servlet 容器都可用于运行 gRPC 服务器。

要在此模式下运行,您必须借助容器自身的基础设施进行设置,在服务器上启用 HTTP/2。Spring Boot 为 Tomcat 和 Jetty 提供了自动配置,您可以使用常规的配置属性来启用 HTTP/2。创建的 Servlet 会被映射为:处理发往已注册服务所定义路径(形如 /<service-name>/*)的 HTTP POST 请求。客户端可以使用该路径连接到服务器,任何 gRPC 客户端库都会这样做。

在 Servlet 容器中运行时,gRPC 服务器的配置选项较少,因为网络层由 Servlet 容器负责。您仍然可以添加 ServerBuilderCustomizer Bean,在服务器构建过程中对其进行定制;但「原生」构建器中常见的某些功能不可用,并可能在运行时抛出异常。

Servlet 容器内的原生 gRPC 服务器​

原生 gRPC 服务器(基于 Netty 等)可以很顺畅地运行在 Web 应用内部,并监听另一个端口。

进程内服务器​

您可以在 classpath 中加入 io.grpc.grpc-inprocess 依赖,并使用 InProcessGrpcServerFactory,来运行一个进程内服务器(即不监听网络端口)。这对测试很有用;但如果您想运行一个只能从同一应用内部访问的 gRPC 服务器(例如用于同一应用不同模块之间的内部通信),它也可以在生产环境中使用。

在此模式下,除了常规服务器工厂(例如 Netty)之外,还会自动配置进程内服务器工厂。

备注

要使用进程内服务器,通道目标必须设置为 in-process:<in-process-name>

服务器拦截器​

全局​

要添加一个应用于所有服务的服务器拦截器,您只需注册一个服务器拦截器 Bean,然后用 @GlobalServerInterceptor 注解标注它。这些拦截器会按照 Bean 的自然顺序(即 @Order)排序。

@Bean
@Order(100)
@GlobalServerInterceptor
ServerInterceptor myGlobalLoggingInterceptor() {
return new MyLoggingInterceptor();
}

过滤​

默认情况下,所有全局拦截器都会应用到所有已创建的服务。不过,您可以注册一个 ServerInterceptorFilter Bean,用来决定哪些拦截器应用于哪些服务器工厂。

以下示例阻止 ExtraThingsInterceptor 拦截器应用于应用了该过滤器的服务器工厂所创建的任何服务器。

@Bean
ServerInterceptorFilter myInterceptorFilter() {
return (interceptor, service) ->
!(interceptor instanceof ExtraThingsInterceptor);
}

您需要提供一个 GrpcServerFactoryCustomizer,在其中通过添加过滤器来修改工厂,如下例所示:

@Bean
GrpcServerFactoryCustomizer myServerFactoryCustomizer() {
return factory -> {
if (factory instanceof NettyGrpcServerFactory) {
((DefaultGrpcServerFactory)factory).setInterceptorFilter(myInterceptorFilter());
}
};
}

按服务​

要为单个服务添加一个服务器拦截器,您只需注册一个服务器拦截器 Bean,然后用 @GrpcService 注解标注您的 BindableService Bean,并通过 interceptors 或 interceptorNames 属性指定该拦截器。

这些拦截器会按照它们在属性列表中的位置排序。当同时使用 interceptors 和 interceptorNames 时,前者中的条目排在后者之前。

在以下示例中,myServerInterceptor 将被应用到 myService 服务。

@Bean
MyServerInterceptor myServerInterceptor() {
return new MyServerInterceptor();
}

@GrpcService(interceptors = MyServerInterceptor.class)
BindableService myService() {
...
}
提示

当一个服务同时配置了全局拦截器和按服务拦截器时,先按排序后的顺序应用全局拦截器,再按排序后的顺序应用按服务拦截器。

不过,通过将 @GrpcService 注解上的 blendWithGlobalInterceptors 属性设置为 "true",您可以改变这一行为:所有拦截器会先合并在一起,然后按照 Bean 的自然顺序(即 @Order)排序。

如果您想在全局拦截器之间插入一个按服务拦截器,可以使用该选项。

反射​

Spring gRPC 允许您创建标准 gRPC 反射服务 的实例,该服务让客户端能够浏览您服务的元数据并下载 Protobuf 文件。

important

反射服务位于 io.grpc:grpc-services 库中,该库被 Spring gRPC 标记为 optional。您必须将此依赖添加到应用中,它才会被自动配置。

健康​

Spring gRPC 会自动配置标准 gRPC 健康服务,用于对 gRPC 服务器发起健康检查调用。健康服务会注册到 gRPC 服务器中,同时提供一个 HealthStatusManager Bean,可用于更新您服务的健康状态。

important

健康服务位于 io.grpc:grpc-services 库中,该库被 Spring gRPC 标记为 optional。您必须将此依赖添加到应用中,它才会被自动配置。

备注

当应用定义了至少一个 BindableService 时,服务器端 gRPC 健康检查默认启用。如果不存在任何服务器端 gRPC 服务,则健康检查默认禁用,必须通过 spring.grpc.server.health.enabled=true 显式启用。

可观测性​

可观测性功能由 Micrometer 提供。Spring Boot 为 Micrometer 提供了一些自动配置,而 Spring gRPC 额外提供了一些自动配置,用于添加 gRPC 特有的指标。

异常处理​

Spring gRPC 提供了一个自动配置的异常处理器,可为您的 gRPC 服务提供一致的异常处理方式。您只需将类型为 GrpcExceptionHandler 的 @Bean 添加到应用上下文中,它们就会被用于处理您服务抛出的异常。一个 GrpcExceptionHandler 既可用于处理特定类型的异常(对不支持的异常返回 null),也可用于处理所有异常。

测试​

如果您在项目中引入 spring-grpc-test,那么在 @SpringBootTest 中,您的 gRPC 服务器可以通过启用进程内服务器而以进程内方式启动(即不监听网络端口)。所有经由自动配置的 GrpcChannelFactory 连接到任何服务器的客户端,都将能够连接到它。

安全​

Netty​

基于 Netty 的服务器开箱即用地支持 TLS 和 mTLS。要启用它,您需要向服务器配置中添加相应的 KeyManager 和/或 TrustManager。您可以通过创建一个 ServerBuilderCustomizer 来实现,也可以直接在 NettyGrpcServerFactory 自身的 @Bean 定义中完成。

使用 Spring Security 实现声明式安全​

如果您想增强 gRPC 服务器的安全性,可以使用 Spring Security,采用与常规 HTTP 安全类似的机制。按常规方式启用 Spring Security,并确保上下文中存在一个 AuthenticationManager Bean。然后创建一个 AuthenticationProcessInterceptor Bean,它使用 AuthenticationManager 对请求进行认证,并将其作为全局服务器拦截器添加(见上文)。之后您就可以在 BindableService Bean 上使用 @PreAuthorize,以角色(用 Spring Security 的术语更准确地说是权限)来强制执行授权规则。

任何用过 Spring Security 的人都会对这种模式感到熟悉。下面是一个示例:

@Bean
@GlobalServerInterceptor
AuthenticationProcessInterceptor jwtSecurityFilterChain(GrpcSecurity grpc) throws Exception {
return grpc
.authorizeRequests(requests -> requests
.methods("Simple/StreamHello").hasAuthority("ROLE_ADMIN")
.methods("Simple/SayHello").hasAuthority("ROLE_USER")
.methods("grpc.*/*").permitAll()
.allRequests().denyAll())
.httpBasic(withDefaults())
.preauth(withDefaults())
.build();
}

这里我们配置了一个过滤器:将某个方法仅允许管理员访问,另一个方法仅允许具有「USER」角色的用户访问;允许所有人访问所有 gRPC 服务(例如反射和健康指示器);并拒绝所有其他请求。我们还启用了 HTTP Basic 认证和预认证(mTLS)(withDefaults() 是从 Spring Security 中 Customizer 静态导入的方法)。

OAuth2 资源服务器​

与 Spring Boot 在普通 Web 应用中的工作方式类似,如果 classpath 中存在 spring-security-oauth2-resource-server 依赖,Spring gRPC 就能通过 GrpcSecurity 配置器来配置一个 OAuth2 资源服务器。

Servlet​

基于 Servlet 的服务器支持 Servlet 容器所支持的任何安全配置,包括 Spring Security。这意味着您可以轻松实现自己偏好的认证与授权机制。与普通 HTTP API 一样,服务器会以 401 状态码拒绝未认证的请求,并以 403 状态码拒绝携带无效令牌的请求。它还会发送相应的 WWW-Authenticate 头,例如取值为 Bearer,以表明(举例来说)它期望接收一个令牌。gRPC 响应中还会包含带有相应错误码和消息的 Status。对于授权检查(例如基于角色的访问控制),您的 BindableService Bean 可以用 @PreAuthorize 注解标注。

注意:如果您自定义了 gRPC 服务器的调用执行器,需要确保将它们包装在 DelegatingSecurityContextExecutor(来自 Spring Security)中。

下面是一个使用 HTTP Basic 认证的示例:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.httpBasic(Customizer.withDefaults())
.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated())
.csrf((csrf) -> csrf.disable())
.build();
}

默认情况下,gRPC 请求应禁用 CSRF 保护,因为它与该协议不兼容。

保护单个方法​

可以通过在方法定义上添加 @PreAuthorize 来保护单个 gRPC 方法。或者,您可以利用「HTTP 端点形如 <service>/<method>」这一事实,使用常规的 HttpSecurity 配置来配置安全。示例:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests((requests) -> requests
.requestMatchers("/Simple/SayHello").hasRole("USER")
.requestMatchers("/Simple/StreamHello").hasRole("ADMIN")
.requestMatchers("/grpc.*/*").permitAll()
.anyRequest().authenticated())
.build();
}

这里我们允许具有 USER 角色的用户访问 Simple/SayHello 方法,允许具有 ADMIN 角色的用户访问 Simple/StreamHello 方法,并允许访问所有由 gRPC 提供的服务(如反射和健康指示器),同时拒绝访问所有其他方法,除非已通过认证。