解决gw和后端服务Cors配置重复问题

https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-deduperesponseheader-gatewayfilter-factory

现象:
请求response header中Access-Control-Allow-Credentials, Access-Control-Allow-Origin的值均是有逗号分隔的重复值
即gw的cors设置和后端服务的cors设置重复叠加了,
可以让后端服务去掉cors配置,仅在gw上保留cors配置,
在紧急情况又或者后端无法修改cors配置的时候,亦可以直接通过gw的DedupeResponseHeader来进行设置

spring:
  cloud:
    gateway:
      routes:
      - id: dedupe_response_header_route
        uri: https://example.org
        filters:
        - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin

注:
DedupeResponseHeader过滤器还接受一个可选的策略strategy参数,其值为:
RETAIN_FIRST (default) 保留第一个值
RETAIN_LAST 保留最后一个值
RETAIN_UNIQUE 保留所有唯一的值,以值第一次出现的位置进行排序
具体示例:

DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin, RETAIN_FIRST

解决CORS OPTIONS请求403

在集成gw后,在浏览器中控制台发现options请求都返回403,且在gw日志中也看到options请求的响应status均为403,
即gw拒绝了options请求,cors配置还未执行,所以不是cors配置的问题,而是gw处理options请求的策略问题,
查看gw官方文档 - CORS Configuration后发现如下配置add-to-simple-url-handler-mapping(默认值false),
开启该配置可以使不在gw路由route predicate配置下的请求也可以使用cors配置,
由于CORS preflight(预检)请求的method为options,gw通过路由配置无法匹配该options请求,所以默认拒绝options请求并返回403 FORBIDDEN,
将add-to-simple-url-handler-mapping设置为true后,则会发现options预检请求均可正常通过。

示例配置

spring:
  cloud:
    gateway:
      globalcors:
      	# 支持浏览器CORS preflight options请求
        add-to-simple-url-handler-mapping: true
        # 具体跨域配置
        cors-configurations:
          '[/**]':
            #allowedOrigins: '*'
            allowedOriginPatterns: '*'
            # 通过cors限制http method,亦可'*'
            allowedMethods: 
            - POST
            - OPTIONS
            allowedHeaders: '*'
            exposedHeaders: '*'
            allowCredentials: true
            maxAge: 86400

注:
最近接了个Spring Cloud Gateway 2.2.x.RELEASE版本的网关,上述配置没管用,
最后直接代码开整:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.util.pattern.PathPatternParser;

/**
 * 网关全局CORS配置<br/>
 * 注:通过spring.cloud.gateway.globalcors配置并未生效,故使用此代码配置。<br/>
 * 后续升级SCG版本可再测试下是否好用。<br/>
 * 该类仅用作临时开发,后续若使用此实现需将设置项提取成配置属性。
 *
 * @author luohq
 * @date 2022-06-10
 */
@Configuration
public class GwCorsFilter {

    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        //允许cookies跨域
        config.setAllowCredentials(true);
        //允许向该服务器提交请求的URI,*表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
        config.addAllowedOrigin("http://capp-spa:8094");
        //允许访问的头信息,*表示全部
        config.addAllowedHeader("*");
        //预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
        config.setMaxAge(18000L);
        //允许提交请求的方法类型,*表示全部允许
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("HEAD");
        config.addAllowedMethod("GET");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("DELETE");
        config.addAllowedMethod("PATCH");

        org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource source =
                new org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }
}

解析请求体中的数据并保留请求体

之前同事用netty写的网关,网关需要解析请求体的属性值作为后端服务路由依据(我更喜欢透传,不去动请求体,效率更高,可以用path来做路由),网关请求体示例:

//Content-Type: application/json
{
	//后端服务路由方法
	"method": "...",
	//后端服务业务参数
	"biz": { ... }
}

//Content-type: application/x-www-form-urlencoded | multipart/form-data
method=...&key1=val1&key2=val2&file1=...

所以就需要去解析请求体,并根据method(这个method是网关协议定义的method,不是http协议中的method)去设置后端服务路由,将biz作为后端业务参数进行传递,
而在用gw重构之前同事写的网关协议时,发现在gw中对请求体仅能读取一次,故可通过如下方式对请求体进行缓存后再解析

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mx.tsp2.beg.gateway.constant.Constants.WEB_EXCHANGE_ATTR;
import com.mx.tsp2.beg.gateway.util.CommonUtils;
import com.mx.tsp2.beg.gateway.util.LogUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.reactive.filter.OrderedWebFilter;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.multipart.FormFieldPart;
import org.springframework.http.codec.multipart.Part;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.Collections;
import java.util.List;
import java.util.Optional;


/**
 * 重写URL全局过滤器
 * 注:使用WebFilter体系,提高filter优先级,
 * 使其在security filter体系前执行,
 * 需要先解析出methodName,然后才能在security中判断该method是否需要验证
 *
 * @author luohq
 * @data 2021-08-20
 */
@Component
@Slf4j
public class BegExtractMethodWebFilter implements OrderedWebFilter {
    /**
     * 过滤器顺序 - 使其在security filter体系前执行,且在基础验证过滤器BegBaseValidatorWebFilter之后执行
     */
    public static final Integer ORDER = BegBaseValidatorWebFilter.ORDER + 1;

    /**
     * default HttpMessageReader.
     */
    private static final List<HttpMessageReader<?>> MESSAGE_READERS = HandlerStrategies.withDefaults().messageReaders();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        /** 无需验证options请求,跳过余下处理,继续处理后续逻辑  */
        if (CommonUtils.isOptionsRequest(exchange)) {
            return chain.filter(exchange);
        }

        return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
            /** 缓存body,防止读取后无法再获取 */
            DataBufferUtils.retain(dataBuffer);
            final Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
            //用缓存的body装饰新的request
            final ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
                @Override
                public Flux<DataBuffer> getBody() {
                    return cachedFlux;
                }
            };
            //使用缓存的body的request封装新的exchange
            final ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
            /** 解析body并转换服务请求URL */
            return resolvedBody(mutatedExchange, chain);
        });
    }

    /**
     * 解析请求Body提取methodName,然后根据methodName获取服务转发URL
     *
     * @param mutatedExchange
     * @param chain
     * @return
     */
    @SuppressWarnings("unchecked")
    private Mono<Void> resolvedBody(ServerWebExchange mutatedExchange, WebFilterChain chain) {
        final HttpHeaders headers = mutatedExchange.getRequest().getHeaders();
        if (headers.getContentLength() == 0) {
            return chain.filter(mutatedExchange);
        }

        //根据content-type设置对应的解析类型
        final ResolvableType resolvableType;
        if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(headers.getContentType())) {
            resolvableType = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class);
        } else if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(headers.getContentType())) {
            resolvableType = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
        } else {
            resolvableType = ResolvableType.forClass(String.class);
        }

        //解析请求体并重置请求URL
        return MESSAGE_READERS.stream()
                .filter(reader -> reader.canRead(resolvableType, mutatedExchange.getRequest().getHeaders().getContentType()))
                .findFirst()
                .orElseThrow(() -> new IllegalStateException("no suitable HttpMessageReader."))
                .readMono(resolvableType, mutatedExchange.getRequest(), Collections.emptyMap())
                /** 根据begMethod重置服务请求URL */
                .flatMap(resolvedBody -> this.extractBegMethod(resolvedBody, mutatedExchange, chain));
    }

    /**
     * 根据请求体重methodName参数重置请求URL
     *
     * @param resolvedBody
     * @param exchange
     * @param chain
     * @return
     */
    private Mono<Void> extractBegMethod(Object resolvedBody, ServerWebExchange exchange, WebFilterChain chain) {
        String originUrl = exchange.getRequest().getURI().toString();
        try {
            /** 提取请求体中的methodName参数值 */
            String begMethod = this.extractBegMethodFromBody(resolvedBody, exchange);
            /** 解析methodName失败则直接返回404 */
            if (!StringUtils.hasText(begMethod)) {
                log.info("{} extract methodName failed and resp With 404: uri={}", LogUtils.getLogPrefixSimple(exchange), originUrl);
                return CommonUtils.respStatus(exchange, HttpStatus.NOT_FOUND);
            }
            /** 记录当前methodName */
            exchange.getAttributes().put(WEB_EXCHANGE_ATTR.PARAM_METHOD_NAME, begMethod);
            /** 记录method日志前缀 */
            exchange.getAttributes().put(WEB_EXCHANGE_ATTR.LOG_PREFIX_METHOD,
                    CommonUtils.buildStr(LogUtils.getLogPrefixSimple(exchange), " [", begMethod, "]"));
            //继续执行如下流程
            return chain.filter(exchange);
        } catch (Exception e) {
            log.info("{} extract methodName failed and resp With 500: uri={}", LogUtils.getLogPrefixSimple(exchange), originUrl, e);
            return CommonUtils.respStatus(exchange, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * 提起请求体中的methodName参数对应的具体值
     *
     * @param resolvedBody
     * @param exchange
     * @return
     */
    private String extractBegMethodFromBody(Object resolvedBody, ServerWebExchange exchange) {
        String begMethod = null;
        /** 即form表单、form表单带文件 */
        //content-type为application/x-www-form-urlencoded、multipart/form-data
        if (resolvedBody instanceof MultiValueMap) {
            Object methodInput = ((MultiValueMap) resolvedBody).getFirst(WEB_EXCHANGE_ATTR.PARAM_METHOD_NAME);
            if (methodInput instanceof FormFieldPart) {
                begMethod = ((FormFieldPart) methodInput).value();

            } else if (methodInput instanceof String) {
                begMethod = (String) methodInput;
            }
        }
        /** 即json请求 */
        else {
            //content-type为application/json
            String bodyJson = (String) resolvedBody;
            JsonObject bodyJsonObj = new JsonParser().parse(bodyJson).getAsJsonObject();
            begMethod = Optional.ofNullable(bodyJsonObj)
                    .map(jsonObj -> jsonObj.get(WEB_EXCHANGE_ATTR.PARAM_METHOD_NAME))
                    .map(jsonElement -> jsonElement.getAsString())
                    .orElse(null);

            //记录请求body(后续modifyRequest使用此值)
            Optional.ofNullable(bodyJsonObj)
                    .map(jsonObj -> jsonObj.get(WEB_EXCHANGE_ATTR.PARAM_BIZ))
                    .map(jsonStr -> jsonStr.toString())
                    .ifPresent(bizJsonStr -> {
                        /** 记录JsonBody中biz数据,用于后续服务调用参数  */
                        exchange.getAttributes().put(WEB_EXCHANGE_ATTR.REQUEST_DATA, bizJsonStr);
                    });

        }
        return begMethod;
    }

    @Override
    public int getOrder() {
        return BegExtractMethodWebFilter.ORDER;
    }

}
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐