一、spring cloud gateway微服务网关简介
为什么需要网关? 在微服务架构中,每个服务是一个独立运行的组件,每个服务都会完成特定的功能,例如订单服务、评论服务、库存服务。假设客户端发起一个请求,我们所有的服务端都需要一个认证的程序,认证客户端来的请求是否是认证过得,例如登录。这样我们的各个微服务就会做重复的工作,所以我们为了解决类似的重复工作问题,我们就引入一个微服务网关。
网关可以做些什么呢?网关可以做:授权、日志、限流、路由等工作。网关服务有:openResty(Nginx+lua)、Kong、Tyk、Zuul、Spring cloud gateway。
我们该如何选择网关服务呢?通常网关服务需要有这些特性: 高稳定性、高性能、高安全性、可扩展性。
spring cloud gateway是spring官方提供的组件,目的是为了取代zuul。它的核心是:spring webflux、Reactor。
二、spring cloud gateway的使用
1、pom.xml依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
引入gateway依赖后我们就可以使用网关了。gateway的核心概念其实就是路由,路由(Route)又包含了:ID、Predicate、Filter。请求会先进入断言,断言拦截判断是否可以进入路由(true/false),true才继续往下进入filter。
2、断言(Predicate)
具体大家可以查询java8里面的predicate(断言、函数式接口)、Consumer/BiConsumer。关于断言此处就不多做赘述了,大家可以看Predicate深入介绍,这位博主写的比较清楚。
3、filter——拦截器
filter有两类:GlobalFilter、RouteFilter,全局拦截器不需要我们在配置文件中配置,它是针对全局生效的;RouteFilter需要我们在配置文件中配置,如以下配置:
spring:
cloud:
gateway:
routes:
- predicates:
- Path=/gateway/**
filters:
- StripPrefix=1 #设置跳过前缀gateway,如果需要跳过多个可以设置2/3对应数字就行
uri: http://localhost:7070/
Filter拦截有两种方式:PreFilter前置过滤、PostFilter后置过滤。针对请求进入到服务端的请求进行过滤,是PreFilter前置过滤;针对服务端处理完毕的返回的报文进行过滤是PostFilter后置过滤。
filter里面可以做授权认证、限流、添加标记报文和一些自定义的操作,更详细的大家可以看:方志朋博客、方志朋CSDN博客
gateway实现限流
网关的限流是通过redis来实现的,所以需要引入redis依赖。引入redis依赖后,我们在配置文件中添加相应的配置,再实现“keyResolver”接口,自定义针对哪一个资源来进行限流的判断,具体如下:
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-data-redis-reactive</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring:
cloud:
gateway:
routes:
- id: ratelimiter-route #配置限流路由
predicates:
- Path=/ratelimiter/**
filters:
- StripPrefix=1
- name: RequestRateLimiter #配置限流name属性:RequestRateLimiter
args: #以下是限流里面配置的参数
deny-empty-key: true #配置限流基于某个标记判断,true为基于
keyResolver: '#{@ipAddressKeyResolver}' #配置限流bean的名称,bean名称默认首字母小写
redis-rate-limiter.replenishRate: 1 #设置每秒允许请求数是1个
redis-rate-limiter.burstCapacity: 2 #设置并发容量(令牌桶容量)是2个
uri: lb://order-service #配置lb(loadBalance)负载均衡分发,从eureka发现服务,分发服务。
package com.example.gatewayservice8085;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class IpAddressKeyResolver implements KeyResolver {
//设置针对IP作为一个进行限流请求的key
public Mono<String> resolve(ServerWebExchange exchange) {
//通过just生成一个Mono对象
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()) ;
}
}
gateway实现负载均衡
gateway的负载均衡是通过eureka来实现的,我们的网关程序中需要引入eureka依赖,添加相应的配置,具体如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
spring:
application:
name: gateway-service
cloud:
gateway:
routes:
- id: lb-route
predicates:
- Path=/lb/**
filters:
- StripPrefix=1
uri: lb://order-service #需要转发的话,有一个叫lb的协议,既loadBalance-负载均衡分发,从eureka发现服务,分发服务,这就是一个全局路由。
discovery:
locator: #配置路由解析器locator
enabled: true
lower-case-service-id: true #配置以小写的方式进行匹配
eureka:
client:
service-url:
defaultZone: http://localhost:8081/eureka
gateway动态路由及持久化
实现动态路由及路由的持久化,我们需要依赖redis和actuator,具体实现如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
spring:
redis:
host: 127.0.0.1
port: 6379
password: 123456
timeout: 6000ms
jedis:
pool:
max-active: 1000
max-wait: -1ms
max-idle: 10
min-idle: 5
management:
endpoints:
web:
exposure:
include: "*"
package com.example.gatewayservice8085;
import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
//通过redis实现动态路由持久化
@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
private static final String GATEWAY_ROUTE_KEY = "gateway_dynamic_route";
@Autowired
RedisTemplate<String,String> redisTemplate;
@Override
public Flux<RouteDefinition> getRouteDefinitions() {//从redis中获取全部路由信息
List<RouteDefinition> routeDefinitionList = new ArrayList<>();
redisTemplate.opsForHash().values(GATEWAY_ROUTE_KEY).stream().forEach(route->{
routeDefinitionList.add(JSON.parseObject(route.toString(),RouteDefinition.class));
});
return Flux.fromIterable(routeDefinitionList);
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {//存储动态路由信息
return route.flatMap(routeDefinition -> {//定义一个routeDefinition用flatMap进行数据的转化
redisTemplate.opsForHash().put(GATEWAY_ROUTE_KEY,routeDefinition.getId(),JSON.toJSONString(routeDefinition));//存入redis
return Mono.empty();
});
}
@Override
public Mono<Void> delete(Mono<String> routeId) {//通过路由id删除指定路由
return routeId.flatMap(id->{
if (redisTemplate.opsForHash().hasKey(GATEWAY_ROUTE_KEY,id)){
redisTemplate.opsForHash().delete(GATEWAY_ROUTE_KEY,id);
return Mono.empty();
}
//如果没有数据,需要通过Mono.defer定义一个Mono返回。
return Mono.defer(()->Mono.error(new Exception("routeDefinition not found"+routeId)));
});
}
}
动态添加路由:比如我们动态添加一个id是baidu_route的路由,我们可以发送POST请求到http://localhost:8085/actuator/gateway/routes/baidu_route,同时携带下面的JSON请求参数(注意我们请求链接最后的“baidu_route”和我们请求参数中的id要一致):
{
"id": "baidu_route",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/baidu"}
}],
"filters": [{
"args":{
"_genkey_0":1
},
"name":"StripPrefix"
}],
"uri": "https://www.baidu.com",
"order": 0
}
通过http://localhost:8085/actuator/gateway/refresh我们可以刷新路由信息
通过http://localhost:8085/actuator/gateway/routes我们可以查看我们的路由信息
通过发送DELETE请求到http://localhost:8085/actuator/gateway/routes/baidu_route我们可以删除持久化的路由(同样请求链接最后的“baidu_toute”要和我们要删除的路由id保持一致)
三、通过demo来学习
本项目建立在上一篇文章spring cloud config配置中心的基础上,同样在springcloudnetflix项目下新建spring项目。
1、pom.xml依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>spring-cloud-netflix</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.example</groupId>
<artifactId>gateway-service-8085</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gateway-service-8085</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-data-redis-reactive</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、application.yml配置文件
spring:
application:
name: gateway-service
cloud:
gateway:
routes:
- id: config-route
predicates:
- Path=/gateway/**
filters:
- StripPrefix=1 #设置跳过前缀gateway,如果需要跳过多个可以设置2/3对应数字就行
- Define=Hello Aron
uri: http://localhost:7070/
- id: cookie-route
predicates:
# - Cookie=name,aron
- Path=/define/** #Path 和Auth 是and的关系,如果Path通过,Auth不通过的话,不会进入filters.
- Auth=Authorization,token #Auth=key,value是key/value形式
filters:
- StripPrefix=1
uri: https://www.gupaoedu.com
- id: lb-route
predicates:
- Path=/lb/**
filters:
- StripPrefix=1
uri: lb://order-service #需要转发的话,有一个叫lb的协议,既loadBalance-负载均衡分发,从eureka发现服务,分发服务,这就是一个全局路由。
- id: ratelimiter-route #配置限流路由
predicates:
- Path=/ratelimiter/**
filters:
- StripPrefix=1
- name: RequestRateLimiter #配置限流name属性:RequestRateLimiter
args: #以下是限流里面配置的参数
deny-empty-key: true #配置限流基于某个标记判断,true为基于
keyResolver: '#{@ipAddressKeyResolver}' #配置限流bean的名称,bean名称默认首字母小写
redis-rate-limiter.replenishRate: 1 #设置每秒允许请求数是1个
redis-rate-limiter.burstCapacity: 2 #设置并发容量(令牌桶容量)是2个
uri: lb://order-service #配置lb(loadBalance)负载均衡分发,从eureka发现服务,分发服务。
discovery:
locator: #配置路由解析器locator
enabled: true
lower-case-service-id: true #配置以小写的方式进行匹配
redis:
host: 127.0.0.1
port: 6379
password: 123456
timeout: 6000ms
jedis:
pool:
max-active: 1000
max-wait: -1ms
max-idle: 10
min-idle: 5
server:
port: 8085
eureka:
client:
service-url:
defaultZone: http://localhost:8081/eureka
management:
endpoints:
web:
exposure:
include: "*"
3、项目代码
(1)、自定义Auth断言
package com.example.gatewayservice8085;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
//自定义断言类名命名规则必须是:name+'RoutePredicateFactory',既后缀必须是‘RoutePredicateFactory’,
// 它会去筛选、截取作为我们的predicate的key
@Component
public class AuthRoutePredicateFactory extends AbstractRoutePredicateFactory<AuthRoutePredicateFactory.Config> {
public AuthRoutePredicateFactory() {
super(Config.class);
}
private static final String NAME_KEY = "name";//name就是我们Config中的name,对应我们配置文件中的key,例如Auth=key,value 的key.
private static final String VALUE_KEY = "value";//value就是我们Config中的value,对应我们配置文件中的value,例如Auth=key,value 的value.
public List<String> shortcutFieldOrder() {
return Arrays.asList(NAME_KEY,VALUE_KEY);
}
public Predicate<ServerWebExchange> apply(final Config config) {
boolean hasValue = !StringUtils.isBlank(config.getValue());
//如果header中携带了某个值,进行header的判断
return (exchange->{
HttpHeaders httpHeaders = exchange.getRequest().getHeaders();
List<String> headerList = httpHeaders.get(config.getName());
if (headerList == null || headerList.isEmpty()){
return false;
}else {
return hasValue ? headerList.stream().anyMatch((value)->{
return value.equals(config.getValue());
}) : false;
}
});
}
//内部配置类
public static class Config {
private String name;
private String value;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}
(2)、自定义路由拦截器(RouteFilte)
package com.example.gatewayservice8085;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
//自定义RouteFilter
@Component
public class DefineGatewayFilterFactory extends AbstractGatewayFilterFactory<DefineGatewayFilterFactory.Config> {
private static final String NAME_KEY = "name";
Logger logger = LoggerFactory.getLogger(DefineGatewayFilterFactory.class);
public DefineGatewayFilterFactory() {
super(Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(NAME_KEY);
}
@Override
public GatewayFilter apply(Config config) {
//Filter 里面分为pre post。pre:客户端请求进来;post:服务端返回过去.
return ((exchange, chain) -> { //chain表示过滤器链
logger.info("[pre] Filter Request,请求进来写自己的业务逻辑,name:"+config.getName());
//then表示接受一个变量;filter表示前面的处理结束,then表示接受前面的处理结果继续处理
return chain.filter(exchange).then(Mono.fromRunnable(()->{ //Mono.fromRunnable表示链处理完成后的回调
logger.info("[Post]: Response Filter——链路处理完毕返回");
}));
});
}
//内部配置类
public static class Config {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
(3)、设置限流—自定义针对哪个资源进行限流判断,此处针对IP限流
package com.example.gatewayservice8085;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class IpAddressKeyResolver implements KeyResolver {
//设置针对IP作为一个进行限流请求的key
public Mono<String> resolve(ServerWebExchange exchange) {
//通过just生成一个Mono对象
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()) ;
}
}
(4)、通过redis实现动态路由的持久化
package com.example.gatewayservice8085;
import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
//通过redis实现动态路由持久化
@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
private static final String GATEWAY_ROUTE_KEY = "gateway_dynamic_route";
@Autowired
RedisTemplate<String,String> redisTemplate;
@Override
public Flux<RouteDefinition> getRouteDefinitions() {//从redis中获取全部路由信息
List<RouteDefinition> routeDefinitionList = new ArrayList<>();
redisTemplate.opsForHash().values(GATEWAY_ROUTE_KEY).stream().forEach(route->{
routeDefinitionList.add(JSON.parseObject(route.toString(),RouteDefinition.class));
});
return Flux.fromIterable(routeDefinitionList);
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {//存储动态路由信息
return route.flatMap(routeDefinition -> {//定义一个routeDefinition用flatMap进行数据的转化
redisTemplate.opsForHash().put(GATEWAY_ROUTE_KEY,routeDefinition.getId(),JSON.toJSONString(routeDefinition));//存入redis
return Mono.empty();
});
}
@Override
public Mono<Void> delete(Mono<String> routeId) {//通过路由id删除指定路由
return routeId.flatMap(id->{
if (redisTemplate.opsForHash().hasKey(GATEWAY_ROUTE_KEY,id)){
redisTemplate.opsForHash().delete(GATEWAY_ROUTE_KEY,id);
return Mono.empty();
}
//如果没有数据,需要通过Mono.defer定义一个Mono返回。
return Mono.defer(()->Mono.error(new Exception("routeDefinition not found"+routeId)));
});
}
}
四、总结
至此,spring-cloud-gateway就学习完毕了。