實(shí)現(xiàn)Spring Cloud Gateway 動(dòng)態(tài)路由和內(nèi)置過(guò)濾器
目前我們公司所有的業(yè)務(wù)服務(wù)都接入了Spring Cloud Gateway,而在接入的過(guò)程中,肯定會(huì)涉及到動(dòng)態(tài)路由這塊:路由配置從數(shù)據(jù)庫(kù)或者Redis中加載。 ?同時(shí),我們還實(shí)現(xiàn)了一些自定義的過(guò)濾器,有GlobalFilter
和GatewayFilter
類型。
GlobalFilter
全局的過(guò)濾器,所有的請(qǐng)求都會(huì)經(jīng)過(guò)這種類型的過(guò)濾器。GatewayFilter
某個(gè)路由的過(guò)濾器,可以掛載到具體某個(gè)路由,非全局。
今天主要介紹下如果實(shí)現(xiàn)Spring Cloud Gateway的動(dòng)態(tài)路由以及如何基于GatewayFilter
實(shí)現(xiàn)一個(gè)內(nèi)置過(guò)濾器
在實(shí)現(xiàn)動(dòng)態(tài)路由之前,我們稍微閱讀了下Spring Cloud Gateway的源碼,會(huì)發(fā)現(xiàn)有一個(gè)InMemoryRouteDefinitionRepository
類,它有3個(gè)實(shí)現(xiàn)方法
save
保存路由delete
刪除路由getRouteDefinitions
獲取路由
這里操作的對(duì)象都是RouteDefinition
實(shí)例,它只是一個(gè)路由信息定義,具體的路由實(shí)現(xiàn)是Route
。
知道了Spring Cloud Gateway對(duì)于路由的處理方式,那么我們自己實(shí)現(xiàn)一套動(dòng)態(tài)路由就非常簡(jiǎn)單了。 這里我們新增一個(gè)RedisRouteDefinitionLocator
類,關(guān)鍵路由加載代碼試下如下:
public Mono<Void> refresh(){
? ?// 從Redis加載配置的路由信息
? ?List<SysRouteDTO> routes = cacheTemplate.valueGetList(CommonConstants.SYS_ROUTE_KEY, SysRouteDTO.class);
? ?for(SysRouteDTO route : routes){
? ? ? ?try {
? ? ? ? ? ?// 斷言
? ? ? ? ? ?List<PredicateDefinition> predicates = Lists.newArrayList();
? ? ? ? ? ?PredicateDefinition predicateDefinition = buildPredicateDefinition(route);
? ? ? ? ? ?predicates.add(predicateDefinition);
? ? ? ? ? ?// 過(guò)濾器
? ? ? ? ? ?List<FilterDefinition> filters = Lists.newArrayList();
? ? ? ? ? ?FilterDefinition stripPrefixFilterDefinition = buildStripPrefixFilterDefinition(route);
? ? ? ? ? ?filters.add(stripPrefixFilterDefinition);
? ? ? ? ? ?if(StringUtil.isNotEmpty(route.getFilters())){
? ? ? ? ? ? ? ?List<FilterDefinition> customFilters = JsonUtil.fromListJson(route.getFilters(), FilterDefinition.class);
? ? ? ? ? ? ? ?this.reloadArgs(customFilters);
? ? ? ? ? ? ? ?filters.addAll(customFilters);
? ? ? ? ? ?}
? ? ? ? ? ?// 元數(shù)據(jù)
? ? ? ? ? ?Map<String, Object> metadata = this.buildMetadata(route);
? ? ? ? ? ?// 代理路徑
? ? ? ? ? ?String targetUri = StringUtil.isNotEmpty(route.getUrl()) ? route.getUrl() : "lb://" + route.getServiceId();
? ? ? ? ? ?URI uri = UriComponentsBuilder.fromUriString(targetUri).build().toUri();
? ? ? ? ? ?// 構(gòu)建路由信息
? ? ? ? ? ?RouteDefinition routeDefinition = new RouteDefinition();
? ? ? ? ? ?routeDefinition.setId(route.getRouteName());
? ? ? ? ? ?routeDefinition.setPredicates(predicates);
? ? ? ? ? ?routeDefinition.setUri(uri);
? ? ? ? ? ?routeDefinition.setFilters(filters);
? ? ? ? ? ?routeDefinition.setMetadata(metadata);
? ? ? ? ? ?this.repository.save(Mono.just(routeDefinition)).subscribe();
? ? ? ?}catch (Exception ex) {
? ? ? ? ? ?log.error("路由加載失敗: name={}, error={}", ex.getMessage(), ex);
? ? ? ?}
? ?}
? ?return Mono.empty();
}
實(shí)現(xiàn)-內(nèi)置過(guò)濾器
在Spring Cloud Gateway其實(shí)已經(jīng)有很多的內(nèi)置過(guò)濾器了,例如:AddRequestParameterGatewayFilter
、AddRequestHeaderGatewayFilter
等等。這些內(nèi)置的過(guò)濾器都是GatewayFilter
類型的,有需要才對(duì)某個(gè)路由進(jìn)行配置,該路由才會(huì)加載該過(guò)濾器。
那么內(nèi)置過(guò)濾器在動(dòng)態(tài)路由的場(chǎng)景下,如果加載內(nèi)置過(guò)濾器呢?其實(shí)很簡(jiǎn)單,和動(dòng)態(tài)路由類似,我們把內(nèi)置過(guò)濾器的相關(guān)信息,配置到數(shù)據(jù)庫(kù)中,在加載路由的時(shí)候,把內(nèi)置過(guò)濾器掛載到具體的路由即可。
在我們實(shí)現(xiàn)動(dòng)態(tài)路由的代碼中,有以下代碼片段
if(StringUtil.isNotEmpty(route.getFilters())){
? ? ? ? ? ? ? ?List<FilterDefinition> customFilters = JsonUtil.fromListJson(route.getFilters(), FilterDefinition.class);
? ? ? ? ? ? ? ?this.reloadArgs(customFilters);
? ? ? ? ? ? ? ?filters.addAll(customFilters);
? ? ? ? ? ?}
在數(shù)據(jù)庫(kù)中,我們配置了路由的filter字段,其實(shí)是一個(gè)json的字符串,同json反序列化為FilterDefinition
,通過(guò)reloadArgs
對(duì)內(nèi)置過(guò)濾器進(jìn)行參數(shù)的配置。
這里我們以一個(gè)校驗(yàn)驗(yàn)證碼的內(nèi)置過(guò)濾器為例。
首先,我們實(shí)現(xiàn)一個(gè)ValidateImageCodeGatewayFilterFactory
類,代碼如下:
public class ValidateImageCodeGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
? ?private CacheTemplate cacheTemplate;
? ?public ValidateImageCodeGatewayFilterFactory(CacheTemplate cacheTemplate) {
? ? ? ?this.cacheTemplate = cacheTemplate;
? ?}
? ?@Override
? ?public GatewayFilter apply(NameValueConfig config) {
? ? ? ?return new ValidateImageCodeGatewayFilter(config, cacheTemplate);
? ?}
}
這個(gè)其實(shí)是一個(gè)工廠類,繼承了AbstractNameValueGatewayFilterFactory
類,AbstractNameValueGatewayFilterFactory
指定了參數(shù)的解析類型:NameValueConfig
,即我們?cè)跀?shù)據(jù)庫(kù)的對(duì)于自定義過(guò)濾器的參數(shù)配置,最后會(huì)通過(guò)NameValueConfig
實(shí)例傳遞進(jìn)來(lái)。
然后,我們?cè)賹?shí)現(xiàn)一個(gè)ValidateImageCodeGatewayFilter
類,代碼如下:
public class ValidateImageCodeGatewayFilter implements GatewayFilter {
? ?private AbstractNameValueGatewayFilterFactory.NameValueConfig config;
? ?private static final String DEFAULT_SSO_LOGIN = "/sso/app/login";
? ?private CacheTemplate cacheTemplate;
? ?public ValidateImageCodeGatewayFilter(AbstractNameValueGatewayFilterFactory.NameValueConfig config,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?CacheTemplate cacheTemplate) {
? ? ? ?this.config = config;
? ? ? ?this.cacheTemplate = cacheTemplate;
? ?}
? ?@Override
? ?public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
? ? ? ?String originPath = this.getOriginPath(exchange);
? ? ? ?String validatePath = StringUtil.isEmpty(this.config.getValue()) ? DEFAULT_SSO_LOGIN : this.config.getValue();
? ? ? ?if(!validatePath.equals(originPath)){
? ? ? ? ? ?return chain.filter(exchange);
? ? ? ?}
? ? ? ?try {
? ? ? ? ? ?this.check(exchange.getRequest());
? ? ? ?}catch (CommonBusinessException ex){
? ? ? ? ? ?ApiBaseResponse resp = new ApiBaseResponse();
? ? ? ? ? ?resp.setResponseMessage(ex.getErrorMessage());
? ? ? ? ? ?resp.setResponseCode(ex.getErrorCode());
? ? ? ? ? ?// 設(shè)置響應(yīng)值
? ? ? ? ? ?return Mono.defer(() -> Mono.just(exchange.getResponse()))
? ? ? ? ? ? ? ? ? ?.flatMap((response) -> {
? ? ? ? ? ? ? ? ? ? ? ?response.setStatusCode(HttpStatus.OK);
? ? ? ? ? ? ? ? ? ? ? ?response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
? ? ? ? ? ? ? ? ? ? ? ?DataBufferFactory dataBufferFactory = response.bufferFactory();
? ? ? ? ? ? ? ? ? ? ? ?DataBuffer buffer = dataBufferFactory.wrap(JsonUtil.toJson(resp).getBytes(Charset.defaultCharset()));
? ? ? ? ? ? ? ? ? ? ? ?return response.writeWith(Mono.just(buffer)).doOnError((error) -> DataBufferUtils.release(buffer));
? ? ? ? ? ? ? ? ? ?}
? ? ? ? ? ?);
? ? ? ?}
? ? ? ?return chain.filter(exchange);
? ?}
? ?/**
? ? * 驗(yàn)證碼校驗(yàn)
? ? * @param request
? ? */
? ?private void check(ServerHttpRequest request){
? ? ? ?String code = request.getQueryParams().getFirst("code");
? ? ? ?if(StringUtil.isEmpty(code)){
? ? ? ? ? ?throw new CommonBusinessException("-1", "驗(yàn)證碼不能為空");
? ? ? ?}
? ? ? ?String randomStr = request.getQueryParams().getFirst("randomStr");
? ? ? ?if(StringUtil.isEmpty(randomStr)){
? ? ? ? ? ?throw new CommonBusinessException("-1", "隨機(jī)數(shù)不能為空");
? ? ? ?}
? ? ? ?String key = CommonConstants.SYS_GATEWAY_CAPTCHA + randomStr;
? ? ? ?String text = cacheTemplate.valueGet(key, String.class);
? ? ? ?if(!code.equals(text)){
? ? ? ? ? ?throw new CommonBusinessException("-1", "驗(yàn)證碼錯(cuò)誤");
? ? ? ?}
? ? ? ?cacheTemplate.keyRemove(key);
? ?}
? ?/**
? ? * 獲取實(shí)際路徑
? ? * @param exchange 上下文
? ? * @return
? ? */
? ?private String getOriginPath(ServerWebExchange exchange){
? ? ? ?LinkedHashSet<URI> set = (LinkedHashSet<URI>)exchange.getAttributes().get(ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
? ? ? ?if(CollectionUtils.isEmpty(set)){
? ? ? ? ? ?return "";
? ? ? ?}
? ? ? ?String originPath = "";
? ? ? ?for(URI uri : set){
? ? ? ? ? ?originPath = ?uri.getPath();
? ? ? ? ? ?break;
? ? ? ?}
? ? ? ?return originPath;
? ?}
}
該過(guò)濾器,通過(guò)check
方法校驗(yàn)了驗(yàn)證碼的正確性,如果異常則拋出,然后返回給前端。getOriginPath
方法是獲取真實(shí)的URL,因?yàn)檫^(guò)濾器是掛載到具體的某個(gè)路由,在我們這個(gè)場(chǎng)景是掛載到單點(diǎn)登錄SSO服務(wù)并且只有登錄的接口才需要進(jìn)行驗(yàn)證碼的驗(yàn)證,其他接口不需要,所以這里需要獲取當(dāng)前請(qǐng)求的真實(shí)URL。
接著,我們?cè)跀?shù)據(jù)庫(kù)對(duì)具體某個(gè)路由filters字段配置該內(nèi)置過(guò)濾器的信息,如下:
[{"name": "ValidateImageCode", "args": { "path":"/sso/app/login" } }]
name字段就是指定了過(guò)濾器的名稱,即完整的類名
ValidateImageCodeGatewayFilter
去掉GatewayFilter即可args是一個(gè)map, key是path,value是需要驗(yàn)證的路徑
這時(shí)候我們運(yùn)行Spring Cloud Gateway會(huì)發(fā)現(xiàn)報(bào)錯(cuò),原因是
NameValueConfig
實(shí)例無(wú)法獲取到正確的配置信息。
經(jīng)過(guò)再次閱讀Spring Cloud Gateway會(huì)發(fā)現(xiàn),我們需要配置成如下格式才可以正確的加載配置:
[{"name": "ValidateImageCode", "args": { "_genkey_0":"path", "_genkey_1": "/sso/app/login" } }]
其實(shí)在加載動(dòng)態(tài)路由的時(shí)候,reloadArgs
方法就是做這個(gè)處理,代碼如下:
private void reloadArgs(List<FilterDefinition> filterDefinitions){
? ?if(CollectionUtils.isEmpty(filterDefinitions)){
? ? ? ?return;
? ?}
? ?for(FilterDefinition definition : filterDefinitions){
? ? ? ?Map<String, String> args = new HashMap<>();
? ? ? ?int i = 0;
? ? ? ?for(Map.Entry<String, String> entry : definition.getArgs().entrySet()){
? ? ? ? ? ?args.put(NameUtils.generateName(i), entry.getKey());
? ? ? ? ? ?args.put(NameUtils.generateName(i+1), entry.getValue());
? ? ? ? ? ?i += 2;
? ? ? ?}
? ? ? ?definition.setArgs(args);
? ?}
}
最后
經(jīng)過(guò)以上說(shuō)明,您學(xué)會(huì)了如何處理Spring Cloud Gateway的動(dòng)態(tài)路由和內(nèi)置過(guò)濾器了不?
相關(guān)源碼地址:https://gitee.com/anyin/anyin-cloud/tree/master/anyin-center-modules/anyin-center-gateway