之前写过一篇文章,介绍微服务场景下的权限处理,方案如下:
在实践中,上面的网关选型为Spring Cloud Gateway,所以这里就存在一个问题,即网关如何调用用户服务进行鉴权的问题。
在微服务场景下,服务间的调用可以通过feign的方式,但这里的问题是,网关是reactor模式,即异步调用模式,而feign调用为同步方式,这里直接通过feign调用会报错。
那Spring Cloud Gateway如何优雅的进行feign调用呢,今天的文章带大家来看下。
1 Spring Cloud Gateway直接进行feign调用
不做特殊处理,在Spring Cloud Gateway中直接进行feign调用的代码如下(这里贴出整个鉴权的GatewayFilterFactory代码以方便理解):
@SuppressWarnings("rawtypes")@Component@Slf4jpublic class ApiAuthGatewayFilterFactory extends AbstractGatewayFilterFactory { private static final String USER_HEADER_NAME = "User-Info"; @Autowired private UserClient userClient; public ApiAuthGatewayFilterFactory() { super(Config.class); } @Override public List shortcutFieldOrder() { return Collections.singletonList("checkAuth"); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { if (config.checkAuth) { String cookie = exchange.getRequest().getHeaders().getFirst("Cookie"); String url = exchange.getRequest().getPath().toString(); String httpMethod = exchange.getRequest().getMethodValue(); // 这里调用了feign接口,到用户模块进行鉴权 ResultResponse resultResponse = userClient.checkPermission(url, httpMethod, cookie); if (resultResponse.isSuccess()) { // 鉴权通过,则将用户信息放入header中,传到下游服务 ServerHttpRequest request = exchange.getRequest().mutate().header(USER_HEADER_NAME, JSON.toJSONString(resultResponse.getData())).build(); return chain.filter(exchange.mutate().request(request).build()); } else { return Mono.defer(() -> { exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); final ServerHttpResponse response = exchange.getResponse(); byte[] bytes = JSON.toJSONString(resultResponse).getBytes(StandardCharsets.UTF_8); DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); return response.writeWith(Flux.just(buffer)); }); } } else { return chain.filter(exchange); } }; } @NoArgsConstructor @Getter @Setter @ToString public static class Config { private boolean checkAuth; }}
不出意外的话,你将会出现如下错误:
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-3at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83)Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s):|_ checkpoint ⇢ org.springframework.web.cors.reactive.CorsWebFilter [DefaultWebFilterChain]|_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]|_ checkpoint ⇢ org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter [DefaultWebFilterChain]|_ checkpoint ⇢ HTTP GET "/api/v1/users/getUserInfo" [ExceptionHandlingWebHandler]
上述错误则说明了,不能再Spring Cloud Gateway中使用同步调用,而普通的feign调用又是同步的,所以会有问题。
2 如何解决Spring Cloud Gateway同步调用feign问题
一、通过线程池来将feign同步调用转为异步调用
在搜索引擎上搜索关于Spring Cloud Gateway调用feign的问题,你可能大概率会得到下面的解决方案,及通过将feign同步调用封装成异步调用来解决。
关键代码如下:
// 将feign调用封装成异步任务,通过线程池的方式提交 Future> future = executorService.submit(() -> { userClient.checkPermission(url, httpMethod, cookie); }); try { // 通过future方式获取结果 ResultResponse resultResponse = (ResultResponse) future.get(); if (resultResponse.isSuccess()) { ServerHttpRequest request = exchange.getRequest().mutate().header(USER_HEADER_NAME, JSON.toJSONString(resultResponse.getData())).build(); return chain.filter(exchange.mutate().request(request).build()); } else { return Mono.defer(() -> { exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); final ServerHttpResponse response = exchange.getResponse(); byte[] bytes = JSON.toJSONString(resultResponse).getBytes(StandardCharsets.UTF_8); DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); return response.writeWith(Flux.just(buffer)); }); } } catch (InterruptedException | ExecutionException e) { // ignore exception } // 异常返回 return Mono.defer(() -> { exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST); final ServerHttpResponse response = exchange.getResponse(); byte[] bytes = JSON.toJSONString("ERROR").getBytes(StandardCharsets.UTF_8); DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); return response.writeWith(Flux.just(buffer)); });
遗憾的是,上述代码我在调试的时候虽然能够解决上面block的报错,但是并没有调通,还是会报错,初步定位是异步任务调用获取返回值的时候有问题,因为此处只是作为一个解决思路展示,而且最终也没有采用上述方案,就没有继续花时间去解决了。各位如果有解决该问题的欢迎指教。
二、真正的异步调用——ReactiveFeign
排除方案一的调试问题,假设方案一可以解决feign同步调用的问题,那么该方案有什么问题呢?
在我看来方案一的问题有二:一是并不是真正意义上的异步调用,只不过通过线程池强行提交了feign调用,而且获取feign调用返回结果的future.get()
方法也是同步的;二是此种方式实在算不上优雅。
实际上feign无法进行异步调用的问题,早已被程序员们注意到,并且现在已经有了比较成熟的解决方案,即feign-reactive项目,项目地址:GitHub - PlaytikaOSS/feign-reactive。
该项目通过Spring WebClient实现了feign的功能,实现了真正意义上的异步feign调用。
下面就让我们通过使用ReactiveFeign来解决Spring Cloud Gateway调用feign接口的问题,直接看代码(这里贴出整个鉴权的GatewayFilterFactory代码以方便理解):
@Component@Slf4jpublic class ApiAuthGatewayFilterFactory extends AbstractGatewayFilterFactory { private static final String USER_HEADER_NAME = "User-Info"; @Autowired private UserReactiveClient userReactiveClient; public ApiAuthGatewayFilterFactory() { super(Config.class); } @Override public List shortcutFieldOrder() { return Collections.singletonList("checkAuth"); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { if (config.checkAuth) { String cookie = exchange.getRequest().getHeaders().getFirst("Cookie"); String url = exchange.getRequest().getPath().toString(); String httpMethod = exchange.getRequest().getMethodValue(); // ReactiveFeign异步调用,获取鉴权结果 return userReactiveClient.checkPermission(url, httpMethod, cookie).flatMap(commonResponse -> { // 鉴权不通过则返回异常 if (!commonResponse.isSuccess()) { return Mono.defer(() -> {exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);final ServerHttpResponse response = exchange.getResponse();byte[] bytes = JSON.toJSONString(commonResponse).getBytes(StandardCharsets.UTF_8);DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);return response.writeWith(Flux.just(buffer)); }); } else { // 鉴权通过将用户信息带入后端 log.info("User-Info: [{}]", JSON.toJSONString(commonResponse.getData())); ServerHttpRequest request = exchange.getRequest().mutate().header(USER_HEADER_NAME, JSON.toJSONString(commonResponse.getData())).build(); return chain.filter(exchange.mutate().request(request).build()); } }); } else { return chain.filter(exchange); } }; } @NoArgsConstructor @Getter @Setter @ToString public static class Config { private boolean checkAuth; }}
上述方案,完美解决了Spring Cloud Gateway同步feign调用的问题,而且看起来也要优雅的多,符合异步编程的风格(上述方案的完整代码,将会在文末给出)。
写在最后
Spring Cloud Gateway通过WebFlux响应式框架实现了全异步处理,看过Spring Cloud Gateway源码的同学应该都深有体会,响应式编程的代码有多么难理解。
正因为Spring Cloud Gateway的响应式编程,导致它直接调用feign会有问题,因为feign的调用是同步调用。
遇到feign同步调用的问题,直接通过线程池强制将feign调用转成异步调用,简单粗暴,在我看来也并不是一个好的方案。
继续深入探究,找到解决feign同步调用问题的根本解决方案,才是一个合格程序员应该做的事。
通过使用ReactiveFeign,可以优雅地解决Spring Cloud Gateway feign同步调用的问题。
完整示例代码,请关注公众号:WU双,对话框回复【网关】即可获取。
完整示例代码除了包含网关的ReactiveFeign异步调用,还包含了XSS过滤器,缓存请求体等网关常用功能。
来源地址:https://blog.csdn.net/sslulu520/article/details/130127048