SpringCloud Gateway实战
SpringCloud Gateway
认识SpringCloud Gateway
-
SpringCloud Gateway 是 Spring 官方最新推出的一款基于 SpringFramework 5,Project Reactor和SpringBoot 2之上开发的网关。
-
它与第一代网关 Zuul 不同的是:gateway 是异步非阻塞的(netty + webflux 实现 )zuul是同步阻塞请求的。
-
Gateway三大组成部分
- Route路由:由ID和目标URI组成
- Predicate断言
- Filter过滤器
SpringCloud Gateway和Zuul最核心的区别
SpringCloud Gateway工作模型图及解读
SpringCloud Gateway三大核心概念
Route、Predicate、Filter
Spring Cloud Gateway 过滤器
全局过滤器和局部过滤器
- 全局过滤器作用于所有的路由不需要单独配置,通常用来实现统一-化处理的业务需求
- 局部过滤器实现并生效的三个步骤
-
- 需要实现 GatewayFilter, Ord注册到 Spring 容器中。
-
- 加入到过滤器工厂,并且将工厂注册到Spring容器中
-
- 在配置文件中进行配置,如果)配置则不启用此过滤器规则(路由规则)
-
SpringCloud Gateway 路由的配置
常见的三种配置方式
- 在代码中注入 RouteLocator Bean,并手工编写配置路由定义;
- 在 application.yml、bootstrap.ym| 等配置文件中配置 spring.cloud.gateway;
- 通过配置中心(Nacos)实现动态的路由配置
谓词Predicate的原理与应用
谓词Predicate是什么
由Java 8引入,位于Java.util.function包中,是一个FunctionInterface(函数式接口)
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class PredicateTest {
public static List<String> MICRO_SERVICE = Arrays
.asList("nacos", "authority", "gateway", "ribbon", "feign", "hystrix", "e-commerce");
/**
* 方法主要用于参数符不符合规则,返回值是boolean
*/
@Test
public void testPredicateTest() {
Predicate<String> letterLengthLimit = s -> s.length() > 5;
MICRO_SERVICE.stream().filter(letterLengthLimit).forEach(System.out::println);
}
/**
* and 方法等同于 逻辑与 & ,存在短路特性,需要所有的条件都满足
* 长度大于5 且 字符串以 gate 开头
*/
@Test
public void testPredicateAnd(){
Predicate<String> letterLengthLimit = s -> s.length() >5;
Predicate<String> letterStartWith = s -> s.startsWith("gate");
MICRO_SERVICE.stream().filter(
letterLengthLimit.and(letterStartWith)
).forEach(System.out::println);
}
/**
* or 等同于逻辑或||
*/
@Test
public void testPredicateOr(){
Predicate<String> letterLengthLimit = s -> s.length() >5;
Predicate<String> letterStartWith = s -> s.startsWith("gate");
//要么第一个能通过,要么第二个通过
MICRO_SERVICE.stream().filter(
letterLengthLimit.or(letterStartWith)
).forEach(System.out::println);
}
/**
* negate 等同于逻辑非 !
*/
@Test
public void testNegate(){
Predicate<String> letterStartWith = s -> s.startsWith("gate");
MICRO_SERVICE.stream().filter(
letterStartWith.negate()
).forEach(System.out::println);
}
/**
* isEqual 类似与Object中 equals(),区别在于先判断对象是否为null, 如果不为null,再使用equals进行比较
*/
@Test
public void testPredicateEqual(){
Predicate<String> equalGateway = s ->Predicate.isEqual("gateway").test(s);
MICRO_SERVICE.stream().filter(equalGateway).forEach(System.out::println);
}
}
比如可查看PathRoutePredicateFactory类。
集成Alibaba Nacos实现动态路由配置
静态路由配置
静态路由配置写在配置文件中(yml或者properties文件中),端点是:Spring.cloud.gateway
缺点非常明显,每次改动都需要网关模块重新部署。
动态路由配置
nacos:
gateway:
route:
config:
# 即为配置中心中的配置文件名称
data-id: e-commerce-gateway-router
group: e-commerce
GatewayConfig: 定于配置类,读取Nacos相关的配置项,用于监听配置监听器
/**
*
* 配置类, 读取nacos相关的配置项,用于配置监听器
*
*/
@Configuration
public class GatewayConfig {
/**
* 读取配置的超时时间
*/
public static final long DEFAULT_TIMEOUT = 30000;
/**
* nacos服务器地址
*/
public static String NACOS_SERVER_ADD;
/**
* 命名空间
*/
public static String NACOS_NAMESPACE;
/**
* data—id
*/
public static String NACOS_ROUTE_DATA_ID;
/**
* 分组id
*/
public static String NACOS_ROUTE_GROUP;
@Value("${spring.cloud.nacos.discovery.server-addr}")
public void setNacosServerAdd(String nacosServerAdd){
NACOS_SERVER_ADD = nacosServerAdd;
}
@Value("${spring.cloud.nacos.discovery.namespace}")
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 setNacosRouteGroupId(String group){
NACOS_ROUTE_GROUP = group;
}
}
实现动态更新路由网关Service,实现对路由信息的动态更新:添加路由定义和更新路由定义
/**
* 事件推送Aware : 动态更新路由网关 Service
*
*
*/
@Slf4j
@Service
@SuppressWarnings("all")
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {
/**
* 写路由定义
*/
private final RouteDefinitionWriter routeDefinitionWriter;
/**
* 获取路由定义
*/
private final RouteDefinitionLocator routeDefinitionLocator;
/**
* 事件发布
*/
private ApplicationEventPublisher publisher;
public DynamicRouteServiceImpl(RouteDefinitionWriter routeDefinitionWriter, RouteDefinitionLocator routeDefinitionLocator, ApplicationEventPublisher publisher) {
this.routeDefinitionWriter = routeDefinitionWriter;
this.routeDefinitionLocator = routeDefinitionLocator;
this.publisher = publisher;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
//完成事件推送句柄的初始化
this.publisher = applicationEventPublisher;
}
/**
* 增加路由定义
* @param routeDefinition
* @return
*/
public String addRouteDefination(RouteDefinition definition){
log.info("gateway add route:[{}]",definition);
//保存路由配置并发布
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
//发布事件通知给Gateway,同步新增的路由定义
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
}
/**
* 更新路由
* @param definitions
* @return
*/
public String updateList(List<RouteDefinition> definitions){
log.info("gateway update route : [{}]",definitions);
//先拿到当前Gateway中存储的路由定义
List<RouteDefinition> routeDefinitionsExists = routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();
if(!CollectionUtil.isEmpty(routeDefinitionsExists)){
//需要清除掉之前所有的 “旧的” 路由定义
routeDefinitionsExists.forEach(rd ->{
log.info("delete route defination: [{}]",rd);
deleteById(rd.getId());
});
}
//把更新的路由定义同步到 gateway中
definitions.forEach(definition ->updateByRouteDefination(definition));
return "success";
}
/**
* 根据路由id删除路由配置
* @param id
* @return
*/
private String deleteById(String id){
try{
log.info("gateway delete route id :[{}]",id);
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
//发布事件通知给gateway 更新路由定义
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "delte success";
}catch (Exception ex){
log.error("gateway delete route fail:[{}]",ex.getMessage(),ex);
return "delete fail";
}
}
/**
* 更新路由
* 更新的实现策略比较简单: 删除+新增=更新
* @param definition
* @return
*/
private String updateByRouteDefination(RouteDefinition definition){
try{
log.info("gateway update route :[{}]",definition);
this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
}catch (Exception ex){
return "update fail,not find route routeId "+definition.getId();
}
try{
this.routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
}catch (Exception ex){
return "update route fail";
}
}
}
连接到NacosConfigService,读取配置信息,增加及更改路由信息。
/**
*
* 通过Nacos 下发动态路由配置,监听nacos中路由配置的变更
* DependsOn是强调依赖类GatewayConfig Bean先注入,在注册该类
*
*/
@Slf4j
@Component
@DependsOn("gatewayConfig")
public class DynamicRouteServiceImplByNacos {
/**
* Nacos配置服务客户端
*/
private ConfigService configService;
private final DynamicRouteServiceImpl dynamicRouteService;
public DynamicRouteServiceImplByNacos(DynamicRouteServiceImpl dynamicRouteService) {
this.dynamicRouteService = dynamicRouteService;
}
/**
* 初始化 Nacos Config,填充 Nacos配置中心信息
*
*/
private ConfigService initConfigService() {
try {
Properties properties = new Properties();
properties.setProperty("serverAddr", GatewayConfig.NACOS_SERVER_ADD);
properties.setProperty("namespace", GatewayConfig.NACOS_NAMESPACE);
return configService = NacosFactory.createConfigService(properties);
} catch (NacosException ex) {
log.error("init gateway nacos config error :[{}]", ex.getMessage(), ex);
return null;
}
}
/**
* 监听 nacos下发的动态路由配置信息
*
* @param dataId nacos dataId
* @param group nacos Group
*/
private void dynamicRouteByNacosListener(String dataId, String group) {
try {
// 给nacos config客户端增加一个监听器
configService.addListener(dataId, group, new Listener() {
/**
* 自己提供线程池执行操作
*/
@Override
public Executor getExecutor() {
return null;
}
/**
* 监听器收到配置更新
* @param configInfo Nacos中最新的配置定义
*/
@Override
public void receiveConfigInfo(String configInfo) {
log.info("start to update config :[{}]", configInfo);
List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
log.info("update route :[{}]", definitionList.toString());
dynamicRouteService.updateList(definitionList);
}
});
} catch (NacosException ex) {
log.error("dynamic update gateway config error :[{}]", ex.getMessage(), ex);
}
}
/**
* Bean在容器中构造完成之后会执行 init 方法,读取初始化Nacos配置中心内容
*/
@PostConstruct
public void init() {
log.info("gateway route init...");
try {
// 初始化 Nacos 配置客户端
configService = initConfigService();
if (null == configService) {
log.error("init config service fail");
return;
}
// 通过Nacos Config 并指定路由配置路径去获取路由配置
String configInfo = configService.getConfig(
GatewayConfig.NACOS_ROUTE_DATA_ID,
GatewayConfig.NACOS_ROUTE_GROUP,
GatewayConfig.DEFAULT_TIMEOUT
);
log.info("get current gateway config:[{}]", configInfo);
List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
if (CollectionUtil.isNotEmpty(definitionList)) {
for (RouteDefinition definition : definitionList) {
log.info("init gateway config :[{}]", definition.toString());
dynamicRouteService.addRouteDefination(definition);
}
}
} catch (Exception ex) {
log.error("gateway route init has some error :[{}]", ex.getMessage(), ex);
}
// 设置监听器
dynamicRouteByNacosListener(GatewayConfig.NACOS_ROUTE_DATA_ID, GatewayConfig.NACOS_ROUTE_GROUP);
}
}
解读SpringCloud Gateway Filter
全局过滤器
RouteToRequestUrlFilter.java,实现路由url转换过程
局部过滤器
前缀过滤器
举例
spring:
cloud:
routes:
-id:qinyi
uri:http://example/org
filters:
-PrefixPath=/mypath
如果请求/hello命中了,就会变成http://example/org/mypath/hello
另一个局部过滤器StripPrefixGatewayFilterFactory.
spring:
cloud:
routes:
-id:qinyi
uri:http://example/org
filters:
- StripPrefix=2
举例:/name/bar/foo 会截断为 /foo
SpringCloud Gateway的执行流程
举例:自定义HeaderToken局部过滤器,用于对请求header中token进行校验
HeadTokenGatewayFilter
/**
*
* HTTP 请求头部携带 token验证过滤器
*
*/
public class HeadTokenGatewayFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 从Http Header中寻找key为token value为imooc的键值对
String name = exchange.getRequest().getHeaders().getFirst("token");
if("imooc".equals(name)){
return chain.filter(exchange);
}
//标记此次请求没有权限,并结束这次请求
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
/**
* 数字越大,优先级越低
* @return 优先级level
*/
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE+2;
}
}
加入到过滤器工厂
/**
* Head Token 局部过滤器
*
*/
@Component
public class HeaderTokenGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return new HeadTokenGatewayFilter();
}
}
添加到配置文件中
"filters": [
{
"name": "HeaderToken"
},
{
# 截断路由前缀第一个/之前的内容,即/imooc
"name": "StripPrefix",
"args": {
"parts": "1"
}
}
]
实现全局网关缓存请求Body数据
GlobalCacheRequestBodyFilter.java,用于缓存请求body
/**
*
* 缓存请求body的全局过滤器
* Spring webFlux
*
*/
@Slf4j
@Component
@SuppressWarnings("all")
public class GlobalCacheRequestBodyFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
boolean isloginOrRegister =
exchange.getRequest().getURI().getPath().contains(GatewayConstant.LOGIN_URI)
|| exchange.getRequest().getURI().getPath().contains(GatewayConstant.REGISTER_URI);
if (null == exchange.getRequest().getHeaders().getContentType()
|| !isloginOrRegister) {
return chain.filter(exchange);
}
// DataBufferUtils.join 拿到请求中的数据 --> DataBuffer
return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
// 确保数据缓冲区不被释放, 必须要 DataBufferUtils.retain
DataBufferUtils.retain(dataBuffer);
// defer、just 都是去创建数据源, 得到当前数据的副本
Flux<DataBuffer> cachedFlux = Flux.defer(() ->
Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
// 重新包装 ServerHttpRequest, 重写 getBody 方法, 能够返回请求数据
ServerHttpRequest mutatedRequest =
new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
// 将包装之后的 ServerHttpRequest 向下继续传递
return chain.filter(exchange.mutate().request(mutatedRequest).build());
});
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE+1;
}
}
由于请求只对注册或者登录失效。定义常量
/**
* 网关常量定义
*
*/
public class GatewayConstant {
/**
* 登录 uri
*/
public static final String LOGIN_URI = "/e-commerce/login";
/**
* 注册 uri
*/
public static final String REGISTER_URI = "/e-commerce/register";
/**
* 授权中心拿到token的uri 格式化接口
*/
public static final String AUTHORITY_CENTER_TOKEN_URL_FORMAT = "http://%s:%s/ecommerce-authority-center/authority/token";
/**
* 去授权中心注册并拿到 token的uri 格式化接口
*/
public static final String AUTHORITY_CENTER_REGISTER_URL_FORMAT = "http://%s:%s/ecommerce-authority-center/authority/register";
}
配合全局登录鉴权过滤器GlobalLoginOrRegisterFilter,通过CachedFilter获得数据
@Slf4j
@Component
public class GlobalLoginOrRegisterFilter implements GlobalFilter, Ordered {
/**
* 注册中心客户端,可以从注册中心获取服务实例信息
*/
private final LoadBalancerClient loadBalancerClient;
private final RestTemplate restTemplate;
public GlobalLoginOrRegisterFilter(LoadBalancerClient loadBalancerClient, RestTemplate restTemplate) {
this.loadBalancerClient = loadBalancerClient;
this.restTemplate = restTemplate;
}
/**
* 登录、注册、鉴权
* 1. 如果是登录或注册,则去授权中心拿到token,并返回给客户端
* 2. 如果是访问其他服务,则鉴权,没有权限,返回 401
*
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
if (request.getURI().getPath().contains(GatewayConstant.LOGIN_URI)) {
//去授权中心拿token
String token = getTokenFromAuthorityCenter(request, GatewayConstant.AUTHORITY_CENTER_TOKEN_URL_FORMAT);
//Header中不能设置null
response.getHeaders().add(CommonConstant.JWT_USER_INFO_KEY,
null == token ? "null": token);
response.setStatusCode(HttpStatus.OK);
return response.setComplete();
}
//如果是注册
if (request.getURI().getPath().contains(GatewayConstant.REGISTER_URI)) {
//去授权中心拿token,先创建用户,在返回token
String token = getTokenFromAuthorityCenter(request, GatewayConstant.AUTHORITY_CENTER_REGISTER_URL_FORMAT);
response.getHeaders().add(
CommonConstant.JWT_USER_INFO_KEY,
null == token ? "null" : token
);
response.setStatusCode(HttpStatus.OK);
return response.setComplete();
}
//3. 访问其他服务,则鉴权,校验是否能从token中解析用户信息
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(CommonConstant.JWT_USER_INFO_KEY);
LoginUserInfo loginUserInfo = null;
try{
loginUserInfo = TokenParseUtil.parseUserInfoFromToken(token);
}catch (Exception ex){
log.error("parse user info from token error :[{}]",ex.getMessage(),ex);
}
//获取不到登录用户信息,返回401
if(null == loginUserInfo){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//解析通过,则放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE + 2;
}
/**
* 从授权中心获取token
*
* @param request
* @param uriFormat
* @return
*/
private String getTokenFromAuthorityCenter(ServerHttpRequest request, String uriFormat) {
//service id 就是服务名字,负载均衡
ServiceInstance serviceInstance = loadBalancerClient.choose(
CommonConstant.AUTHORITY_CENTER_SERVICE_ID
);
log.info("Nacos Client Info :[{}],[{}],[{}]", serviceInstance.getServiceId(), serviceInstance.getInstanceId(), JSON.toJSONString(serviceInstance.getMetadata()));
String requestUrl = String.format(
uriFormat, serviceInstance.getHost(), serviceInstance.getPort()
);
UsernameAndPassword requestBody = JSON.parseObject(
parseBodyFromRequest(request), UsernameAndPassword.class
);
log.info("login request url and body :[{}],[{}]", requestUrl, JSON.toJSONString(requestBody));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
JwtToken token = restTemplate.postForObject(
requestUrl, new HttpEntity<>(JSON.toJSONString(requestBody), headers), JwtToken.class
);
if (null != token) {
return token.getToken();
}
return null;
}
/**
* 从post请求中获取到请求数据
*
* @param request
* @return
*/
private String parseBodyFromRequest(ServerHttpRequest request) {
//获取请求体
Flux<DataBuffer> body = request.getBody();
AtomicReference<String> bodyRef = new AtomicReference<>();
//订阅缓冲区去消费请求体中的数据
body.subscribe(buffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
//一定要使用DataBufferUtils.release释放掉,否则会出现内存泄漏
DataBufferUtils.release(buffer);
bodyRef.set(charBuffer.toString());
});
//获取请求体中的数据
return bodyRef.get();
}
}
举例
[
{
"id": "e-commerce-nacos-client",
"predicates": [
{
"args": {
"pattern": "/imooc/ecommerce-nacos-client/**"
},
"name": "Path"
}
],
"uri": "lb://e-commerce-nacos-client",
"filters": [
{
"name": "HeaderToken"
},
{
"name": "StripPrefix",
"args": {
"parts": "1"
}
}
]
}
]
-
id:路由配置主键,需要全局唯一
-
predicates:完成路由匹配
-
uri:从注册中心中获取到服务
-
filter
- StripPrefix:可配置跳过前缀,实现转发
配置登录请求转发规则(代码实现)
使用代码去定义路由规则,在网关层面拦截登录和注册接口,适合比较简单的路由规则。
/**
*
* 配置登录请求转发规则
*/
@Configuration
public class RouteLocatorConfig {
/**
* 使用代码去定义路由规则,在网关层面拦截登录和注册接口
*
* @param builder
* @return
*/
@Bean
public RouteLocator loginRouteLocator(RouteLocatorBuilder builder) {
//手动定义 Gateway路由规则需要指定 id、path和uri
return builder.routes().route("e_commerce_authority",
r -> r.path(
"/imooc/e-commerce/login",
"/imooc/e-commerce/register"
).uri("http://localhost:9000/")
).build();
}
}