提示:本整合方案基于springcloud alibaba的毕业版本推荐,即:
springboot version:2.6.3
springcloud version:2021.0.01
springcloud alibaba version: 2021.0.1.0
目录
前言
二、使用步骤
1.父pom内容
2.gateway工程pom
3.相关代码
3.1 gateway yml配置文件
3.2 请求交易码判断自定义断言
3.3 请求灰度字段判断自定义断言
3.4 请求验签过滤器
3.5 响应加签过滤器方法
3.6 sentinel BlockRequest实现类
4.nacos持久化配置
4.1限流规则
4.2熔断规则
4.3补充说明
总结
基于springcloud gateway和sentinel整合的配置,记录为主。
简陋的画了一个时序图
一、sentinel是什么
官方解释:面向分布式服务架构的高可用流量控制组件
请注意:以下配置由于部分原因不能展示编译、打包等构建相关的插件配置,如果您需要参考请自行按情况添加,此文档只记录和标题相关的配置项。
4.0.0
com.hx
qp-demo
1.0-SNAPSHOT
pom
org.springframework.boot
spring-boot-starter-parent
2.6.3
2021.0.1
2021.0.1.0
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
${spring-cloud-alibaba.version}
pom
import
qp-demo
com.hx
1.0-SNAPSHOT
4.0.0
网关
qp-gateway
org.springframework.cloud
spring-cloud-starter-bootstrap
org.springframework.cloud
spring-cloud-starter-gateway
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.cloud
spring-cloud-starter-loadbalancer
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
com.alibaba.csp
sentinel-datasource-nacos
com.alibaba.cloud
spring-cloud-alibaba-sentinel-gateway
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/?(?.*), /payment/$\{segment}
#正常支付交易网关
- id: payment
order: 2
uri: lb://qp-server-payment
predicates:
- Path=/main/**
- name: Nucc
args:
msgTps: ["tradecode01","tradecode02","test"]
filters:
- RewritePath=/main/?(?.*), /payment/$\{segment}
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
fileName: dev
server:
port: 8088
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 {
private final List> messageReaders;
public NuccRoutePredicateFactory() {
super(NuccRoutePredicateFactory.Config.class);
this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
}
public NuccRoutePredicateFactory(List> messageReaders) {
super(NuccRoutePredicateFactory.Config.class);
this.messageReaders = messageReaders;
}
@Override
public AsyncPredicate applyAsync(NuccRoutePredicateFactory.Config config) {
return new AsyncPredicate() {
@Override
public Publisher 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("") + 7);// xml报文域
String sign = content.substring(content.indexOf("{S:") + 3, content.lastIndexOf("}"));// 签名域
String msgTp = content.substring(content.indexOf(""));// 报文编号
return Arrays.asList(config.msgTps).contains(msgTp);
}
@Override
public Predicate 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;
}
}
}
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 {
private final List> messageReaders;
@Override
public List shortcutFieldOrder() {
return Arrays.asList("xmlTag", "regexp");
}
public NuccGaryRoutePredicateFactory() {
super(NuccGaryRoutePredicateFactory.Config.class);
this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
}
public NuccGaryRoutePredicateFactory(List> messageReaders) {
super(NuccGaryRoutePredicateFactory.Config.class);
this.messageReaders = messageReaders;
}
@Override
public AsyncPredicate applyAsync(NuccGaryRoutePredicateFactory.Config config) {
return new AsyncPredicate() {
@Override
public Publisher 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("", 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 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;
}
}
}
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 {
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("") + 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不校验
}
}
加签没有编写过滤器是因为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 {
/**
* 响应进行签名
*
* @param serverWebExchange 下游web句柄
* @param resp 响应报文
* @return
*/
@Override
public Publisher apply(ServerWebExchange serverWebExchange, String resp) {
log.debug("响应报文签名:{}", resp);
if (!resp.contains("root")) {
return Mono.just(resp);
}
String xmlStr = resp.substring(resp.indexOf("") + 7);// xml报文域
resp = resp + "{S:mocksign}"; //MOCK
return Mono.just(resp);
}
}
此方法是通过对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 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);
}
}
首先我最初是通过sentinel的dashboard进行规则配置,但后来发现每次重启应用都需要重新配,这很不合理,翻阅了sentinel的官方文档,其中提到:
Sentinel 的理念是开发者只需要关注资源的定义,当资源定义成功后可以动态增加各种流控降级规则。Sentinel 提供两种方式修改规则:
- 通过 API 直接修改 (
loadRules
)- 通过
DataSource
适配不同数据源修改通过 API 修改比较直观,可以通过以下几个 API 修改不同的规则:
FlowRuleManager.loadRules(List
rules); // 修改流控规则 DegradeRuleManager.loadRules(List rules); // 修改降级规则 手动修改规则(硬编码方式)一般仅用于测试和演示,生产上一般通过动态规则源的方式来动态管理规则。
既然有这建议我就参考进行配置,按官方文档的意思,配置关系应该是这样的:
1.首先网关应用启动,通过nacos获取gateway.yml配置
2.sentinel数据源组件通过配置(配置样例在上面),获取对应的json配置文件
3.加载成功后,如果应用在sentinel dashboard注册了,那么可以打开dashboard进行监控(不可以修改或增加配置,因为重启应用后就失效了)
引用我的另外一篇文章
springcloud gateway(scg)-sentinel nacos配置-流控篇https://blog.csdn.net/hanxu00920/article/details/124432569
引用我的另外一篇文章
我的网关接入sentinel是通过springcloud alibaba接入的,也就是引用了spring-cloud-alibaba-sentinel-gateway组件,sentinel官方文档中还提到了可以通过sentinel-spring-cloud-gateway-adapter适配器进行接入,我没有深入去研究二者的区别,但使用前者接入,dashboard是可以识别出网关资源(网关是按route id标注资源,而普通的应用则是通过url标注资源)。
以上内容仅为个人记录,如有可以参考到的地方希望对你有帮助,如果有错误、或更好的方式实现以上功能可以留言。