之前写过一篇文章,介绍微服务场景下的权限处理,方案如下:
在实践中,上面的网关选型为Spring Cloud Gateway,所以这里就存在一个问题,即网关如何调用用户服务进行鉴权的问题。
在微服务场景下,服务间的调用可以通过feign的方式,但这里的问题是,网关是reactor模式,即异步调用模式,而feign调用为同步方式,这里直接通过feign调用会报错。
那Spring Cloud Gateway如何优雅的进行feign调用呢,今天的文章带大家来看下。
1 Spring Cloud Gateway直接进行feign调用
不做特殊处理,在Spring Cloud Gateway中直接进行feign调用的代码如下(这里贴出整个鉴权的GatewayFilterFactory代码以方便理解):
@SuppressWarnings("rawtypes")
@Component
@Slf4j
public class ApiAuthGatewayFilterFactory extends AbstractGatewayFilterFactory<ApiAuthGatewayFilterFactory.Config> {
private static final String USER_HEADER_NAME = "User-Info";
@Autowired
private UserClient userClient;
public ApiAuthGatewayFilterFactory() {
super(Config.class);
}
@Override
public List<String> 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-3
at 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
@Slf4j
public class ApiAuthGatewayFilterFactory extends AbstractGatewayFilterFactory<ApiAuthGatewayFilterFactory.Config> {
private static final String USER_HEADER_NAME = "User-Info";
@Autowired
private UserReactiveClient userReactiveClient;
public ApiAuthGatewayFilterFactory() {
super(Config.class);
}
@Override
public List<String> 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过滤器,缓存请求体等网关常用功能。
版权归原作者 公众号-WU双 所有, 如有侵权,请联系我们删除。