SpringCloud-Gateway接入兼容旧业务踩坑记录

前言

随着公司业务的发展,目前公司的服务数已达到了21个,旧业务一直都是一个业务一个域名,再加上我们有n套测试环境,因此运维要维护的域名为21n个(这个数只多不少),大大增加了运维的维护成本,包括新起一套测试环境域名问题也十分困扰人。

同时微服务之前的鉴权原本也是通过nginx塞请求头的方式来处理,每次新起服务都需要重新配置。

跨域也是在各个服务里自己配置,有时候开发会忘记在各自服务里配置跨域,导致时间成本的增加。

综上所述在服务数日益增加的需求下,接入网关刻不容缓。


系统配置及版本

1
2
# 线上服务器配置
双节点8核心CPU 16G内存
依赖 版本
SpringCloud Hoxton.SR10
SpringGateway 2.2.7.RELEASE
SpringBoot 2.3.8.RELEASE
SA-TOKEN 1.25.0
***

全局过滤器

全局过滤器不需要配置在每个路由规则下就可以生效可以用来配置网关日志、白名单、鉴权等功能,通过实现Odered接口来实现过滤器优先级,优先级如下

1
2
3
4
5
6
7
8
9
10
11
/**
* Useful constant for the highest precedence value.
* @see java.lang.Integer#MIN_VALUE 0x80000000;
*/
int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;

/**
* Useful constant for the lowest precedence value.
* @see java.lang.Integer#MAX_VALUE 0x7fffffff
*/
int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Slf4j
@Component
public class GlobalLogFilter implements GlobalFilter, Ordered {
@Resource
private GatewaySecurityProperties gatewaySecurityProperties;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

if (Boolean.TRUE.equals(gatewaySecurityProperties.getLogEnable())) {
ServerHttpRequest request = exchange.getRequest();
log.info("====================REQUEST INFO START====================");
String hostName = Optional.of(exchange).map(ServerWebExchange::getRequest).map(ServerHttpRequest::getRemoteAddress).map(InetSocketAddress::getHostName).orElse(CharSequenceUtil.EMPTY);
String clientIp = Optional.of(exchange).map(ServerWebExchange::getRequest).map(ServerHttpRequest::getRemoteAddress).map(InetSocketAddress::getAddress).map(InetAddress::getHostAddress).orElse(CharSequenceUtil.EMPTY);
log.info("==REMOTE ADDRESS : [{}]{}", hostName, clientIp);
log.info("==REQUEST URL : [{}]{}", request.getMethod(), Optional.of(exchange).map(ServerWebExchange::getRequest).map(HttpRequest::getURI).map(URI::getPath).orElse(CharSequenceUtil.EMPTY));
log.info("==REQUEST TOKEN : [{}]", Optional.of(exchange)
.map(ServerWebExchange::getRequest)
.map(HttpMessage::getHeaders)
.map(x -> x.get(GlobalConstant.TOKEN_HEADER))
.map(y -> y.get(0))
.orElse(CharSequenceUtil.EMPTY));

log.info("=====================REQUEST INFO END======================");
}
return chain.filter(exchange);
}

@Override
public int getOrder() {
return FilterOrder.GLOBAL_LOG_FILTER_ORDER;
}

统一接口权限

统一接口权限在项目中采用的是SA-TOKEN,因此按照SA-TOKEN接口权限文档编码即可。可以集成nacos配置中心,变成实时可控的权限。

这边需要注意先注册SA-TOKEN全局过滤器

1
2
3
4
5
6
7
8
@Configuration
public class SaTokenConfigure {
// 注册 Sa-Token全局过滤器
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter();
}
}
1
2
3
4
5
// 全局鉴权开关
private Boolean urlPermissionsEnable;

// 权限映射关系
private Map<String, List<String>> urlPermissions;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Slf4j
@Component
public class GlobalPermissionFilter implements GlobalFilter, Ordered {
@Resource
private GatewaySecurityProperties gatewaySecurityProperties;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

if (Boolean.TRUE.equals(gatewaySecurityProperties.getUrlPermissionsEnable())) {
Map<String, List<String>> urlPermissions = gatewaySecurityProperties.getUrlPermissions();
try {
// 判断用户是否有该权限
urlPermissions.keySet().forEach(perm -> SaRouter.match(urlPermissions.get(perm), () -> StpUtil.checkPermission(perm)));
} catch (Exception e) {
throw new BusinessException(ResultStatus.UNAUTHORIZED);
}
}

return chain.filter(exchange);
}

@Override
public int getOrder() {
return FilterOrder.GLOBAL_PERMISSION_FILTER_ORDER;
}
}

统一APIGateway knife4j接入

网关侧配置

首先引入knife4j的依赖

1
2
3
4
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Slf4j
@Component
@Primary
public class SwaggerProvider implements SwaggerResourcesProvider {

public static final String API_URI = "/v2/api-docs";

/**
* 网关应用名称
*/
@Value("${spring.application.name}")
private String self;

private final RouteLocator routeLocator;

@Autowired
public SwaggerProvider(RouteLocator routeLocator) {
this.routeLocator = routeLocator;
}

@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routes = new ArrayList<>();
routeLocator.getRoutes()
.filter(route -> route.getUri().getHost() != null && Objects.equals(route.getUri().getScheme(), "lb") && !self.equalsIgnoreCase(route.getUri().getHost()))
.subscribe(route ->
routes.add(route.getUri().getHost()));

// 记录已经添加过的server,存在同一个应用注册了多个服务在注册中心上
Set<String> processed = new HashSet<>();
routes.forEach(service -> {
// 拼接url ,请求swagger的url
String url = "/" + service.toLowerCase() + API_URI;
if (!processed.contains(url)) {
processed.add(url);
resources.add(createSwaggerResource(service, url));
}
});
return resources;
}

private SwaggerResource createSwaggerResource(String name, String url) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setUrl(url);
swaggerResource.setLocation(url);
swaggerResource.setSwaggerVersion("3.0");
return swaggerResource;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@RestController
public class SwaggerHandler {

@Autowired(required = false)
private SecurityConfiguration securityConfiguration;
@Autowired(required = false)
private UiConfiguration uiConfiguration;

private final SwaggerProvider swaggerProvider;

@Autowired
public SwaggerHandler(SwaggerProvider swaggerProvider) {
this.swaggerProvider = swaggerProvider;
}

@GetMapping("/swagger-resources/configuration/security")
public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
}

@GetMapping("/swagger-resources/configuration/ui")
public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
}

@GetMapping("/swagger-resources")
public Mono<ResponseEntity<List<SwaggerResource>>> swaggerResources() {
return Mono.just((new ResponseEntity<>(swaggerProvider.get(), HttpStatus.OK)));
}
}

如此之后,访问http://${网关域名}/doc.html,能看到统一swagger文档,网关会根据服务名去业务服务去自动收集swagger的json展示在页面上,因此子服务也需要集成swagger框架,即访问${业务服务}/v2/api-docs能拿到业务服务的接口json文档即可。

业务服务侧配置

1
2
3
4
5
6
7
8
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* <p> Description : SwaggerConfiguration </p>
* <p>
* @date : 2020/3/31 11:54
*/
@Slf4j
@Configuration
@EnableSwagger2
public class SwaggerConfiguration {

/**
* 创建API应用
* apiInfo() 增加API相关信息
* 通过select()函数返回一个ApiSelectorBuilder实例,用来控制哪些接口暴露给Swagger来展现,
* 本例采用指定扫描的包路径来定义指定要建立API的目录。
*
* @return
*/
@Bean
public Docket createRestApi() {


//版本类型是swagger2
return new Docket(DocumentationType.SWAGGER_2)
//通过调用自定义方法apiInfo,获得文档的主要信息
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("xyz.molzhao.controller"))//扫描该包下面的API注解
.paths(PathSelectors.any())
.build().globalOperationParameters(getGlobalParameter());

}

/**
* 创建该API的基本信息(这些基本信息会展现在文档页面中)
* 访问地址:http://项目实际地址/swagger-ui.html
*
* @return
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("使用Swagger2 构建RESTful APIS - zy") //接口管理文档首页显示
.description("zy - Swagger使用演示")//API的描述
.termsOfServiceUrl("www.footmark.top")//网站url等
.version("1.0")
.build();
}

// 设置全局参数
private List<Parameter> getGlobalParameter() {
ParameterBuilder ticketPar = new ParameterBuilder();
List<Parameter> pars = new ArrayList<>();
ticketPar.name("token").description("token")
.modelRef(new ModelRef("string")).parameterType("query")
.required(false).build();
pars.add(ticketPar.build());

ParameterBuilder model = new ParameterBuilder();
model.name("model").description("设备")
.modelRef(new ModelRef("String")).parameterType("query")
.required(true).build();
pars.add(model.build());
return pars;
}

}

业务服务集成swaager文档即可,这边可以继续进行优化,将swagger封装成一个starter供给另外的服务引用


动态路由

由于网关是一个比较基础的服务,他的重启几乎会影响所有后端服务的访问,因此不能经常重启,此时我们就非常需要能实时改换路由规则的一个功能。本文将结合nacos实现网关动态路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 网关路由仓库配置(本项目采用nacos配置中心)
*/
@Configuration
public class GatewayConfig {
public static final long DEFAULT_TIMEOUT = 30000;

public static String NACOS_SERVER_ADDR;

public static String NACOS_NAMESPACE;

public static String NACOS_ROUTE_DATA_ID;

public static String NACOS_ROUTE_GROUP;

@Value("${spring.cloud.nacos.discovery.server-addr}")
public void setNacosServerAddr(String nacosServerAddr) {
NACOS_SERVER_ADDR = nacosServerAddr;
}

@Value("${spring.cloud.nacos.discovery.namespace:#{null}}")
public void setNacosNamespace(String nacosNamespace) {
NACOS_NAMESPACE = nacosNamespace;
}

@Value("${nacos.gateway.route.config.data-id}")
public void setNacosRouteDataId(String nacosRouteDataId) {
NACOS_ROUTE_DATA_ID = nacosRouteDataId;
}

@Value("${nacos.gateway.route.config.group}")
public void setNacosRouteGroup(String nacosRouteGroup) {
NACOS_ROUTE_GROUP = nacosRouteGroup;
}

}

动态路由实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* 通过nacos下发动态路由配置,监听Nacos中gateway-route配置
*/
@Component
@Slf4j
@DependsOn({"gatewayConfig"}) // 依赖于gatewayConfig bean
public class DynamicRouteServiceImplByNacos {

@Autowired
private DynamicRouteServiceImpl dynamicRouteService;

private ConfigService configService;

@PostConstruct
public void init() {
try {
configService = initConfigService();
if (configService == null) {
log.warn("initConfigService fail");
return;
}
String configInfo = configService.getConfig(GatewayConfig.NACOS_ROUTE_DATA_ID, GatewayConfig.NACOS_ROUTE_GROUP, GatewayConfig.DEFAULT_TIMEOUT);
log.info("获取网关当前配置:\r\n{}", configInfo);
List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
for (RouteDefinition definition : definitionList) {
log.info("update route : {}", definition.toString());
dynamicRouteService.add(definition);
}
} catch (Exception e) {
log.error("初始化网关路由时发生错误", e);
}
dynamicRouteByNacosListener(GatewayConfig.NACOS_ROUTE_DATA_ID, GatewayConfig.NACOS_ROUTE_GROUP);
}

/**
* 监听Nacos下发的动态路由配置
*
* @param dataId
* @param group
*/
public void dynamicRouteByNacosListener(String dataId, String group) {
try {
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
log.info("进行网关更新:\n\r{}", configInfo);
List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
log.info("update route : {}", definitionList.toString());
dynamicRouteService.updateList(definitionList);
}

@Override
public Executor getExecutor() {
log.info("getExecutor\n\r");
return null;
}
});
} catch (NacosException e) {
log.error("从nacos接收动态路由配置出错!!!", e);
}
}

/**
* 初始化网关路由 nacos config
*
* @return
*/
private ConfigService initConfigService() {
try {
Properties properties = new Properties();
properties.setProperty("serverAddr", GatewayConfig.NACOS_SERVER_ADDR);
if (CharSequenceUtil.isNotEmpty(GatewayConfig.NACOS_NAMESPACE)) {
properties.setProperty("namespace", GatewayConfig.NACOS_NAMESPACE);
}
return configService = NacosFactory.createConfigService(properties);
} catch (Exception e) {
log.error("初始化网关路由时发生错误", e);
return null;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
* 动态更新路由网关service
* 1)实现一个Spring提供的事件推送接口ApplicationEventPublisherAware
* 2)提供动态路由的基础方法,可通过获取bean操作该类的方法。该类提供新增路由、更新路由、删除路由,然后实现发布的功能。
*/
@Slf4j
@Service
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
@Autowired
private RouteDefinitionLocator routeDefinitionLocator;

/**
* 发布事件
*/
@Autowired
private ApplicationEventPublisher publisher;

@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}

/**
* 删除路由
* @param id
* @return
*/
public Result<Void> delete(String id) {
try {
log.info("网关删除路由,路由ID: {}",id);
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return Result.success();
} catch (Exception e) {
return Result.failed(GatewayResultEnum.DELETE_ROUTE_FAIL);
}
}

/**
* 更新路由
* @param definitions
* @return
*/
public Result<Void> updateList(List<RouteDefinition> definitions) {
log.info("网关更新路由:{}",definitions);
// 删除缓存routerDefinition
List<RouteDefinition> routeDefinitionsExits = routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();
if (!CollectionUtils.isEmpty(routeDefinitionsExits)) {
routeDefinitionsExits.forEach(routeDefinition -> {
log.info("delete routeDefinition:{}", routeDefinition);
delete(routeDefinition.getId());
});
}
definitions.forEach(this::updateById);
return Result.success();
}

/**
* 更新路由
* @param definition
* @return
*/
public Result<Void> updateById(RouteDefinition definition) {
try {
log.info("gateway update route {}",definition);
this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
} catch (Exception e) {
log.error("更新失败未找到路由,routeId: {}", definition.getId());
return Result.failed(GatewayResultEnum.UPDATE_ROUTE_FAIL);
}
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return Result.success();
} catch (Exception e) {
return Result.failed(GatewayResultEnum.UPDATE_ROUTE_FAIL);
}
}

/**
* 增加路由
* @param definition
* @return
*/
public Result<Void> add(RouteDefinition definition) {
log.info("gateway add route {}",definition);
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return Result.success();
}
}

由此在nacos中新增配置文件gateway-router即可实现动态路由,可以根据自己需要新增过滤器或者更改路由规则

1
2
3
4
5
6
nacos:
gateway:
route:
config:
data-id: gateway-router
group: DEFAULT_GROUP
1
2
3
4
5
6
7
8
9
10
11
12
[{
"id": "xyz",
"order": 0,
"predicates": [{
"args": {
"pattern":"/xyz/**"
},
"name": "Path"
}],
"filters": [{"name":"StripPrefix","args":{"_genkey_0":"1"}}],
"uri": "lb://xyz"
}]

一般情况下,我们为了方便可能在网关转发的时候直接使用gateway自带的根据服务名进行路由转发,但这不影响动态路由的配置,两者可以共存。

1
2
3
4
5
6
7
8
9
gateway:
discovery:
locator:
enabled: true #开启根据微服务名称自动转发
lower-case-service-id: true
filters:
## 当开启服务名转发配置后想用另外的过滤器,需要先加上StripPrefix
- StripPrefix=1
- CorsFilterFactory

跨域及重复跨域问题解决

1
Access to XMLHttpRequest at 'http://192.168.0.144:8762/ctginms_main2019/api/traceRoute/getMapData' from origin 'http://localhost:8080' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values '*, *', but only one is allowed.

由于之前的老项目每个服务对应一个域名,且存在很多h5页面直接根据老域名进行调用,前端修改域名起来十分困难,因此后端需要兼容这些老域名。

之前的技术架构中,每个业务服务都有自己的跨域配置,且都开启了(为了兼容老h5页面访问),现在需要接入统一网关,新服务的跨域配置理当在网关中统一配置,不然后续服务多起来一个配置要重复n次,不太合理,由此造成了当经过网关访问到某个旧服务(自己开启跨域配置)的时候会出现以上错误。意思就是重复跨域了。

什么是跨域

何谓同源:URL由协议、域名、端口和路径组成,如果两个URL的协议、域名和端口相同,则表示它们同源。浏览器的同源策略,从一个域上加载的脚本不允许访问另外一个域的文档属性 ,是浏览器上为安全性考虑实施的非常重要的安全策略。违反了这个同源策略我们就认为是跨域访问了,浏览器会拒绝这项请求。

以上看起来十分拗口,想要理解上面的内容也有点困难,其实可以简单的理解,后端返回的resonse中跨域的配置比如配置了允许的请求头,当前请求是否带有其他的非法请求头访问,如果是非法的即不能访问。

1
2
3
4
5
6
7
8
public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers";
public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age";
public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";

重复跨域解决

之前说过网关和业务服务都开启了跨域配置因此我们可以理解成一个请求链经过网关和业务服务都给他的ResonseHeader加上了以Access-Control-Allow-Origin为Key,*为Value的键值对,这就是重复跨域。

我们可以看下HttpHeaders的数据结构,他实现了名为MultiValueMap的数据结构,而这个底层的数据结构是Map<K, List<V>>因此我们可以知道这个数据结构一个key可以存放多个值,由此之前的*,*也可以理解了

1
2
3
4
5
6
7
8
9
10
public class HttpHeaders implements MultiValueMap<String, String>, Serializable {
private static final long serialVersionUID = -8578554704772377436L;
public static final String ACCEPT = "Accept";
public static final String ACCEPT_CHARSET = "Accept-Charset";
public static final String ACCEPT_ENCODING = "Accept-Encoding";
public static final String ACCEPT_LANGUAGE = "Accept-Language";
public static final String ACCEPT_RANGES = "Accept-Ranges";
public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
...
}

我们只需要在网关侧拦截到请求链的response,对ResonseHeader进行处理即可,如果有重复的value则给他去重在放行给浏览器这样问题也就迎刃而解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* 处理重复跨域问题, 清除重复的response-header
*/
@Component
@Slf4j
public class CorsFilterFactory extends ModifyResponseBodyGatewayFilterFactory {
private final Set<String> corsHeaderSet;

{
corsHeaderSet = new HashSet<>();
corsHeaderSet.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS);
corsHeaderSet.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS);
corsHeaderSet.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS);
corsHeaderSet.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN);
corsHeaderSet.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE);
corsHeaderSet.add(HttpHeaders.VARY);
}

@Override
public GatewayFilter apply(Config config) {
return new ModifyResponseGatewayFilter(this.getConfig());
}

private Config getConfig() {
Config cf = new Config();
cf.setRewriteFunction(byte[].class, byte[].class, getRewriteFunction());
return cf;
}

/**
* 重写 Response 响应头
* <p>
* 如果跨域响应头重复则去除
*
* @return 重写方法
*/
private RewriteFunction<byte[], byte[]> getRewriteFunction() {
return (exchange, resp) -> {
HttpHeaders oldHeaders = exchange.getResponse().getHeaders();
HttpHeaders newHeaders = new HttpHeaders();
oldHeaders.forEach((key, value) -> {
if (judgeCorsHeader(key)) {
List<String> responseValue = value.stream().distinct().collect(Collectors.toList());
// 当去重后的集合大小小于去重前的集合大小,说明有重复响应头
if (value.size() > responseValue.size()) {
newHeaders.addAll(key, responseValue);
}
}
});

newHeaders.forEach((key, value) -> {
oldHeaders.remove(key);
oldHeaders.addAll(key, value);
});
return Mono.just(resp);
};
}

private boolean judgeCorsHeader(String header) {
return corsHeaderSet.contains(header);
}
}

最后我们只需要在对应的路由规则上配置该路由器即可

1
2
3
4
5
6
7
8
9
gateway:
discovery:
locator:
enabled: true #开启根据微服务名称自动转发
lower-case-service-id: true
filters:
## 当开启服务名转发配置后想用另外的过滤器,需要先加上StripPrefix
- StripPrefix=1
- CorsFilterFactory

Exceeded limit on max bytes to buffer : 262144

接入完统一网关之后,上线的第一天晚上,监听到网关侧有大量Exceeded limit on max bytes to buffer : 262144该类型错误。经过排查发现是因为业务服务返回的json串大于了256k导致的。

配置文件配置缓冲区大小

经过参考网上的解决方案之后,按照如下配置即可放大缓冲区的大小,但是在SpringBoot版本2.3.8.RELEASE中配置并没有起作用。

1
2
3
spring:
codec:
max-in-memory-size: 2MB

配置类配置缓冲区大小

于是采用了通过配置来进行配置缓冲区大小,但是在当前SpringBoot版本中这种方法也没有起到作用。

1
2
3
4
5
6
7
8
@Configuration
@EnableWebFlux
public class WebFluxWebConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024);
}
}

重写Spring原文件

我们可以看到在SpringBoot2.3.8.RELEASE中这个缓存区大小被硬编码了,因此我们如何更改都无效,这边采用了一个不怎么优雅的办法,就是在自己项目文件中建一个和Spring源码相同的包名类名的文件并将新建的AbstractDataBufferDecoder<T>这个类的缓存区调大即可,最后采用了这种方案解决了问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package org.springframework.core.codec;

import org.reactivestreams.Publisher;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.MimeType;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.Map;

/**
* Spring硬编码maxInMemorySize,未找到修改该值的方法,所以直接覆盖
*
* Abstract base class for {@code Decoder} implementations that can decode
* a {@code DataBuffer} directly to the target element type.
*
* <p>Sub-classes must implement {@link #decodeDataBuffer} to provide a way to
* transform a {@code DataBuffer} to the target data type. The default
* {@link #decode} implementation transforms each individual data buffer while
* {@link #decodeToMono} applies "reduce" and transforms the aggregated buffer.
*
* <p>Sub-classes can override {@link #decode} in order to split the input stream
* along different boundaries (e.g. on new line characters for {@code String})
* or always reduce to a single data buffer (e.g. {@code Resource}).
*
* @author Rossen Stoyanchev
* @since 5.0
* @param <T> the element type
*/
@SuppressWarnings("deprecation")
public abstract class AbstractDataBufferDecoder<T> extends AbstractDecoder<T> {
// 原Spring中的该值为262144
private int maxInMemorySize = 10 * 1024 * 1024;


protected AbstractDataBufferDecoder(MimeType... supportedMimeTypes) {
super(supportedMimeTypes);
}


/**
* Configure a limit on the number of bytes that can be buffered whenever
* the input stream needs to be aggregated. This can be a result of
* decoding to a single {@code DataBuffer},
* {@link java.nio.ByteBuffer ByteBuffer}, {@code byte[]},
* {@link org.springframework.core.io.Resource Resource}, {@code String}, etc.
* It can also occur when splitting the input stream, e.g. delimited text,
* in which case the limit applies to data buffered between delimiters.
* <p>By default this is set to 256K.
* @param byteCount the max number of bytes to buffer, or -1 for unlimited
* @since 5.1.11
*/
public void setMaxInMemorySize(int byteCount) {
this.maxInMemorySize = byteCount;
}

/**
* Return the {@link #setMaxInMemorySize configured} byte count limit.
* @since 5.1.11
*/
public int getMaxInMemorySize() {
return this.maxInMemorySize;
}


@Override
public Flux<T> decode(Publisher<DataBuffer> input, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {

return Flux.from(input).map(buffer -> decodeDataBuffer(buffer, elementType, mimeType, hints));
}

@Override
public Mono<T> decodeToMono(Publisher<DataBuffer> input, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {

return DataBufferUtils.join(input, this.maxInMemorySize)
.map(buffer -> decodeDataBuffer(buffer, elementType, mimeType, hints));
}

/**
* How to decode a {@code DataBuffer} to the target element type.
* @deprecated as of 5.2, please implement
* {@link #decode(DataBuffer, ResolvableType, MimeType, Map)} instead
*/
@Deprecated
@Nullable
protected T decodeDataBuffer(DataBuffer buffer, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {

return decode(buffer, elementType, mimeType, hints);
}

}

升级SpringBoot版本

按照网上大量资料的说法在SpringBoot高版本中这个问题得到了解决,但是在一个稳定运行的线上项目,想要升级SpringBoot版本风险实在太大,因为有很多未知的坑。因此这种方法一开始就被摒弃了。有兴趣的可以自己尝试下高版本中是否开启这项配置有效。