使用SpringCloud Gateway遇到的一些问题
记录使用SCG过程中遇到的问题
解决gw和后端服务Cors配置重复问题
现象:
请求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;
}
}
更多推荐
所有评论(0)