之前寫過一篇文章,介紹微服務場景下的權(quán)限處理,方案如下:
在實踐中,上面的網(wǎng)關(guān)選型為Spring Cloud Gateway,所以這里就存在一個問題,即網(wǎng)關(guān)如何調(diào)用用戶服務進行鑒權(quán)的問題。
在微服務場景下,服務間的調(diào)用可以通過feign的方式,但這里的問題是,網(wǎng)關(guān)是reactor模式,即異步調(diào)用模式,而feign調(diào)用為同步方式,這里直接通過feign調(diào)用會報錯。
那Spring Cloud Gateway如何優(yōu)雅的進行feign調(diào)用呢,今天的文章帶大家來看下。
1 Spring Cloud Gateway直接進行feign調(diào)用
不做特殊處理,在Spring Cloud Gateway中直接進行feign調(diào)用的代碼如下(這里貼出整個鑒權(quán)的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();
// 這里調(diào)用了feign接口,到用戶模塊進行鑒權(quán)
ResultResponse resultResponse = userClient.checkPermission(url, httpMethod, cookie);
if (resultResponse.isSuccess()) {
// 鑒權(quán)通過,則將用戶信息放入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;
}
}
不出意外的話,你將會出現(xiàn)如下錯誤:
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中使用同步調(diào)用,而普通的feign調(diào)用又是同步的,所以會有問題。
2 如何解決Spring Cloud Gateway同步調(diào)用feign問題
一、通過線程池來將feign同步調(diào)用轉(zhuǎn)為異步調(diào)用
在搜索引擎上搜索關(guān)于Spring Cloud Gateway調(diào)用feign的問題,你可能大概率會得到下面的解決方案,及通過將feign同步調(diào)用封裝成異步調(diào)用來解決。
關(guān)鍵代碼如下:
// 將feign調(diào)用封裝成異步任務,通過線程池的方式提交
Future<?> future = executorService.submit(() -> {
userClient.checkPermission(url, httpMethod, cookie);
});
try {
// 通過future方式獲取結(jié)果
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));
});
遺憾的是,上述代碼我在調(diào)試的時候雖然能夠解決上面block的報錯,但是并沒有調(diào)通,還是會報錯,初步定位是異步任務調(diào)用獲取返回值的時候有問題,因為此處只是作為一個解決思路展示,而且最終也沒有采用上述方案,就沒有繼續(xù)花時間去解決了。各位如果有解決該問題的歡迎指教。
二、真正的異步調(diào)用——ReactiveFeign
排除方案一的調(diào)試問題,假設方案一可以解決feign同步調(diào)用的問題,那么該方案有什么問題呢?
在我看來方案一的問題有二:一是并不是真正意義上的異步調(diào)用,只不過通過線程池強行提交了feign調(diào)用,而且獲取feign調(diào)用返回結(jié)果的future.get()
方法也是同步的;二是此種方式實在算不上優(yōu)雅。
實際上feign無法進行異步調(diào)用的問題,早已被程序員們注意到,并且現(xiàn)在已經(jīng)有了比較成熟的解決方案,即feign-reactive項目,項目地址:GitHub - PlaytikaOSS/feign-reactive。
該項目通過Spring WebClient實現(xiàn)了feign的功能,實現(xiàn)了真正意義上的異步feign調(diào)用。
下面就讓我們通過使用ReactiveFeign來解決Spring Cloud Gateway調(diào)用feign接口的問題,直接看代碼(這里貼出整個鑒權(quán)的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異步調(diào)用,獲取鑒權(quán)結(jié)果
return userReactiveClient.checkPermission(url, httpMethod, cookie).flatMap(commonResponse -> {
// 鑒權(quán)不通過則返回異常
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 {
// 鑒權(quán)通過將用戶信息帶入后端
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調(diào)用的問題,而且看起來也要優(yōu)雅的多,符合異步編程的風格(上述方案的完整代碼,將會在文末給出)。
寫在最后
Spring Cloud Gateway通過WebFlux響應式框架實現(xiàn)了全異步處理,看過Spring Cloud Gateway源碼的同學應該都深有體會,響應式編程的代碼有多么難理解。
正因為Spring Cloud Gateway的響應式編程,導致它直接調(diào)用feign會有問題,因為feign的調(diào)用是同步調(diào)用。
遇到feign同步調(diào)用的問題,直接通過線程池強制將feign調(diào)用轉(zhuǎn)成異步調(diào)用,簡單粗暴,在我看來也并不是一個好的方案。
繼續(xù)深入探究,找到解決feign同步調(diào)用問題的根本解決方案,才是一個合格程序員應該做的事。
通過使用ReactiveFeign,可以優(yōu)雅地解決Spring Cloud Gateway feign同步調(diào)用的問題。
完整示例代碼,請關(guān)注公眾號:WU雙,對話框回復【網(wǎng)關(guān)】即可獲取。文章來源:http://www.zghlxwxcb.cn/news/detail-478110.html
完整示例代碼除了包含網(wǎng)關(guān)的ReactiveFeign異步調(diào)用,還包含了XSS過濾器,緩存請求體等網(wǎng)關(guān)常用功能。文章來源地址http://www.zghlxwxcb.cn/news/detail-478110.html
到了這里,關(guān)于Spring Cloud Gateway如何優(yōu)雅地進行feign調(diào)用的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!