提示:本整合方案基于springcloud alibaba的毕业版本推荐,即:
springboot version:2.6.3
springcloud version:2021.0.01
springcloud alibaba version: 2021.0.1.0
目录
前言
基于springcloud gateway和sentinel整合的配置,记录为主。
简陋的画了一个时序图

官方解释:面向分布式服务架构的高可用流量控制组件
二、使用步骤
请注意:以下配置由于部分原因不能展示编译、打包等构建相关的插件配置,如果您需要参考请自行按情况添加,此文档只记录和标题相关的配置项。
1.父pom内容
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.hx</groupId>
<artifactId>qp-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<spring-cloud.version>2021.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.1.0</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
</project>2.gateway工程pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>qp-demo</artifactId>
<groupId>com.hx</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<name>网关</name>
<artifactId>qp-gateway</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- sentinel nacos持久化 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!-- springcloud alibaba团队编写的sentinel start组件 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<!-- sentinel团队编写的scg适配器 -->
<!-- <dependency>-->
<!-- <groupId>com.alibaba.csp</groupId>-->
<!-- <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>-->
<!-- </dependency>-->
</dependencies>
</project>3.相关代码
3.1 gateway yml配置文件
spring:
application:
name: qp-gateway
cloud:
#sentinel配置
sentinel:
#sentinel控制台配置,本地端口不配置默认从8719开始尝试监听
transport:
dashboard: 127.0.0.1:8080
#nacos持久化配置
datasource:
ds1:
nacos:
server-addr: 127.0.0.1:8848
data-id: qp-gateway-gw-flow
group-id: qp
data-type: json
#网关限流规则
rule-type: gw-flow
ds2:
nacos:
server-addr: 127.0.0.1:8848
namespace: public
data-id: qp-gateway-degrade
group-id: qp
data-type: json
#熔断规则
rule-type: degrade
gateway:
#全局过滤器,此处做了交易请求的验签和签名
default-filters:
#验证签名
- NuccMsg=true
#签名
- name: ModifyResponseBody
args:
rewriteFunction: '#{@nuccSign}'
outClass: java.lang.String
inClass: java.lang.String
routes:
#灰度支付交易网关
- id: payment-gary
order: 1
uri: lb://qp-server-payment-gary
predicates:
#交易路由自定义断言,按请求中交易码字段进行判断
- Path=/main/**
- name: Nucc
args:
msgTps: ["tradecode01","tradecode02","test"]
#灰度发布自定义断言,按请求中某字段的正则表达式进行判断
- NuccGary=SgnNo, ^110120119.*
filters:
#请求地址重写
- RewritePath=/main/?(?<segment>.*), /payment/$\{segment}
#正常支付交易网关
- id: payment
order: 2
uri: lb://qp-server-payment
predicates:
- Path=/main/**
- name: Nucc
args:
msgTps: ["tradecode01","tradecode02","test"]
filters:
- RewritePath=/main/?(?<segment>.*), /payment/$\{segment}
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
fileName: dev
server:
port: 80883.2 请求交易码判断自定义断言
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.handler.AsyncPredicate;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
/**
* 某机构XML接口规范实现的路由断言工厂
*/
@Component
@Slf4j
public class NuccRoutePredicateFactory extends AbstractRoutePredicateFactory<NuccRoutePredicateFactory.Config> {
private final List<HttpMessageReader<?>> messageReaders;
public NuccRoutePredicateFactory() {
super(NuccRoutePredicateFactory.Config.class);
this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
}
public NuccRoutePredicateFactory(List<HttpMessageReader<?>> messageReaders) {
super(NuccRoutePredicateFactory.Config.class);
this.messageReaders = messageReaders;
}
@Override
public AsyncPredicate<ServerWebExchange> applyAsync(NuccRoutePredicateFactory.Config config) {
return new AsyncPredicate<ServerWebExchange>() {
@Override
public Publisher<Boolean> apply(ServerWebExchange exchange) {
Object cachedBody = exchange.getAttribute("bodyCache");
if (cachedBody != null) {
return Mono.just(checkMspTp(cachedBody, config));
} else {
log.debug("解析....");
return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange,
(serverHttpRequest) -> ServerRequest
.create(exchange.mutate().request(serverHttpRequest).build(), messageReaders)
.bodyToMono(String.class).doOnNext(objectValue -> exchange.getAttributes()
.put(GatewayCons.CACHE_REQUEST_BODY_OBJECT_KEY, objectValue))
.map(objectValue -> checkMspTp(objectValue, config)));
}
}
@Override
public Object getConfig() {
return config;
}
};
}
/**
* 校验报文编号
*
* @param requestBody 请求body正文
* @param config 配置
* @return
*/
private boolean checkMspTp(Object requestBody, NuccRoutePredicateFactory.Config config) {
String content = String.valueOf(requestBody);
if (content == null) {
log.warn("没有获取到请求body,请核实");
return false;
}
if (!content.contains("<root")) {
log.warn("收到非xml报文,请检查请求方是否正确发起交易");
return false;
}
String xmlStr = content.substring(content.indexOf("<root"), content.indexOf("</root>") + 7);// xml报文域
String sign = content.substring(content.indexOf("{S:") + 3, content.lastIndexOf("}"));// 签名域
String msgTp = content.substring(content.indexOf("<MsgTp") + 7, content.indexOf("</MsgTp>"));// 报文编号
return Arrays.asList(config.msgTps).contains(msgTp);
}
@Override
public Predicate<ServerWebExchange> apply(NuccRoutePredicateFactory.Config config) {
throw new UnsupportedOperationException("NuccRoutePredicateFactory is only async.");
}
public static class Config {
String[] msgTps; //报文编号
public String[] getMsgTps() {
return msgTps;
}
public NuccRoutePredicateFactory.Config setMsgTps(String[] msgTps) {
this.msgTps = msgTps;
return this;
}
}
}
3.3 请求灰度字段判断自定义断言
package com.yld.gateway.predicate;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.handler.AsyncPredicate;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
/**
* 灰度路由断言工厂
*/
@Component
@Slf4j
public class NuccGaryRoutePredicateFactory extends AbstractRoutePredicateFactory<NuccGaryRoutePredicateFactory.Config> {
private final List<HttpMessageReader<?>> messageReaders;
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("xmlTag", "regexp");
}
public NuccGaryRoutePredicateFactory() {
super(NuccGaryRoutePredicateFactory.Config.class);
this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
}
public NuccGaryRoutePredicateFactory(List<HttpMessageReader<?>> messageReaders) {
super(NuccGaryRoutePredicateFactory.Config.class);
this.messageReaders = messageReaders;
}
@Override
public AsyncPredicate<ServerWebExchange> applyAsync(NuccGaryRoutePredicateFactory.Config config) {
return new AsyncPredicate<ServerWebExchange>() {
@Override
public Publisher<Boolean> apply(ServerWebExchange exchange) {
Object cachedBody = exchange.getAttribute("bodyCache");
if (cachedBody != null) {
return Mono.just(checkGaryTag(cachedBody, config));
} else {
log.debug("解析....");
return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange,
(serverHttpRequest) -> ServerRequest
.create(exchange.mutate().request(serverHttpRequest).build(), messageReaders)
.bodyToMono(String.class).doOnNext(objectValue -> exchange.getAttributes()
.put(GatewayCons.CACHE_REQUEST_BODY_OBJECT_KEY, objectValue))
.map(objectValue -> checkGaryTag(objectValue, config)));
}
}
@Override
public Object getConfig() {
return config;
}
};
}
/**
* 判断是否符合灰度断言校验
* @param requestBody
* @param config
* @return
*/
private boolean checkGaryTag(Object requestBody, NuccGaryRoutePredicateFactory.Config config) {
String content = String.valueOf(requestBody);
if (content == null) {
log.warn("没有获取到请求body,请核实");
return false;
}
if (!content.contains("<root")) {
log.warn("收到非xml报文,请检查请求方是否正确发起交易,灰度断言无法判定");
return false;
}
if (!ObjectUtils.isEmpty(config.regexp)) {
// check if a header value matches
String tag = String.format("<%s>", config.xmlTag); //格式化标签开始
String tagEnd = String.format("</%s>", config.xmlTag); //格式化标签结束
String xmlTagValue = content.substring(content.indexOf(tag) + tag.length(), content.indexOf(tagEnd)); //xml标签截取
if (xmlTagValue.matches(config.regexp)) { //按正则判断
log.debug("断言匹配到了灰度标记:xmltag:{},regexp:{},报文匹配:{}",config.xmlTag, config.regexp, xmlTagValue);
return true;
}
return false;
}
return false;
}
@Override
public Predicate<ServerWebExchange> apply(NuccGaryRoutePredicateFactory.Config config) {
throw new UnsupportedOperationException("NuccGaryRoutePredicateFactory is only async.");
}
public static class Config {
private String xmlTag;// 报文的XML标签名
private String regexp;// 报文内容的正则
public String getXmlTag() {
return xmlTag;
}
public NuccGaryRoutePredicateFactory.Config setXmlTag(String xmlTag) {
this.xmlTag = xmlTag;
return this;
}
public String getRegexp() {
return regexp;
}
public NuccGaryRoutePredicateFactory.Config setRegexp(String regexp) {
this.regexp = regexp;
return this;
}
}
}
3.4 请求验签过滤器
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
/**
* 请求验签过滤器
*/
@Component
@Slf4j
public class NuccMsgGatewayFilterFactory extends AbstractGatewayFilterFactory<NuccMsgGatewayFilterFactory.Config> {
public NuccMsgGatewayFilterFactory() {
super(NuccMsgGatewayFilterFactory.Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
Object requestBody = exchange.getAttribute("bodyCache");
String content = String.valueOf(requestBody);
if (content == null) {
log.warn("没有获取到请求body,请核实");
}
if (!content.contains("<root")) {
log.warn("收到非xml报文,请检查请求方是否正确发起交易");
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete();
}
String xmlStr = content.substring(content.indexOf("<root"), content.indexOf("</root>") + 7);// xml报文域
String sign = content.substring(content.indexOf("{S:") + 3, content.lastIndexOf("}"));// 签名域
log.debug("mock验签......");
//..TODO
log.debug("请求正文:{},签名:{}", xmlStr, sign);
return chain.filter(exchange);
};
}
@Data
public static class Config {
boolean signCheckType; //签名校验类型,true校验,false不校验
}
}
3.5 响应加签过滤器方法
加签没有编写过滤器是因为scg已经实现了ModifyResponseBody,我们直接对接口RewriteFunction编写实现类即可,网关Yml配置文件可见配置关系。
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component("nuccSign")
@Slf4j
public class NuccSignFunction implements RewriteFunction<String, String> {
/**
* 响应进行签名
*
* @param serverWebExchange 下游web句柄
* @param resp 响应报文
* @return
*/
@Override
public Publisher<String> apply(ServerWebExchange serverWebExchange, String resp) {
log.debug("响应报文签名:{}", resp);
if (!resp.contains("root")) {
return Mono.just(resp);
}
String xmlStr = resp.substring(resp.indexOf("<root"), resp.indexOf("</root>") + 7);// xml报文域
resp = resp + "{S:mocksign}"; //MOCK
return Mono.just(resp);
}
}
3.6 sentinel BlockRequest实现类
此方法是通过对BlockRequestHandler接口进行实现完成的,对BlockRequestHandler官方的解释是:
您可以在 GatewayCallbackManager 注册回调进行定制:
setBlockHandler:注册函数用于实现自定义的逻辑处理被限流的请求,对应接口为 BlockRequestHandler。默认实现为 DefaultBlockRequestHandler,当被限流时会返回类似于下面的错误信息:Blocked by Sentinel: FlowException。
所以我先根据我的接口规范,编写了限流或熔断规则发生时,响应给客户端的规范报文
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.text.ParseException;
import java.util.Date;
/**
* sentinel拦截后实现类
*
* @author hx
* @Description 完善响应报文,按规范返回
* @date 2022/4/25 10:46
*/
@Slf4j
@Component
public class NuccSentinelBlockHandler implements BlockRequestHandler {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
log.error("NuccSentinelBlockHandler block throw ", throwable);
String responseXml;
if (throwable instanceof ParamFlowException) { //限流规则
responseXml = errorMsg("00000001", "系统达到最大流量限制");
} else if (throwable instanceof DegradeException) { //熔断规则
responseXml = errorMsg("00000002", "系统暂时忙");
} else {
responseXml = errorMsg("00000003", "系统错误请稍后重试");
}
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS).bodyValue(responseXml);
}
/**
* 生成通用响应报文
*
* @param sysRtnCd 系统返回代码
* @param sysRtnDesc 系统返回代码详细信息
* @return
*/
private String errorMsg(String sysRtnCd, String sysRtnDesc) {
return String.format("%s-%s", sysRtnCd, sysRtnDesc); //规范不方便公开,理解这么做的意义即可
}
}
GatewayCallbackManager注册可以通过CommandLineRunner容器启动后进行加载:
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.yld.gateway.callback.NuccSentinelBlockHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
/**
* 网关配置类
* @author hx
* @Description 加载配置类
* @date 2022/4/25 21:32
*/
@Slf4j
@Component
public class GatewayConfig implements CommandLineRunner {
@Autowired
NuccSentinelBlockHandler blockHandler;
@Override
public void run(String... args) throws Exception {
GatewayCallbackManager.setBlockHandler(blockHandler);
}
}
4.nacos持久化配置
首先我最初是通过sentinel的dashboard进行规则配置,但后来发现每次重启应用都需要重新配,这很不合理,翻阅了sentinel的官方文档,其中提到:
Sentinel 的理念是开发者只需要关注资源的定义,当资源定义成功后可以动态增加各种流控降级规则。Sentinel 提供两种方式修改规则:
- 通过 API 直接修改 (
loadRules)- 通过
DataSource适配不同数据源修改通过 API 修改比较直观,可以通过以下几个 API 修改不同的规则:
FlowRuleManager.loadRules(List<FlowRule> rules); // 修改流控规则 DegradeRuleManager.loadRules(List<DegradeRule> rules); // 修改降级规则
手动修改规则(硬编码方式)一般仅用于测试和演示,生产上一般通过动态规则源的方式来动态管理规则。
既然有这建议我就参考进行配置,按官方文档的意思,配置关系应该是这样的:

1.首先网关应用启动,通过nacos获取gateway.yml配置
2.sentinel数据源组件通过配置(配置样例在上面),获取对应的json配置文件
3.加载成功后,如果应用在sentinel dashboard注册了,那么可以打开dashboard进行监控(不可以修改或增加配置,因为重启应用后就失效了)
4.1限流规则
引用我的另外一篇文章
4.2熔断规则
引用我的另外一篇文章
springcloud gateway(scg)-sentinel nacos配置-熔断篇
https://blog.csdn.net/hanxu00920/article/details/124445362
4.3补充说明
我的网关接入sentinel是通过springcloud alibaba接入的,也就是引用了spring-cloud-alibaba-sentinel-gateway组件,sentinel官方文档中还提到了可以通过sentinel-spring-cloud-gateway-adapter适配器进行接入,我没有深入去研究二者的区别,但使用前者接入,dashboard是可以识别出网关资源(网关是按route id标注资源,而普通的应用则是通过url标注资源)。
总结
以上内容仅为个人记录,如有可以参考到的地方希望对你有帮助,如果有错误、或更好的方式实现以上功能可以留言。