网关功能
- 流量转发
- 用户认证
- 服务限流
- 服务降级
- 灰度发布
1 概述
基于springboot和spring webflux,基于netty运行,的http网关服务。
它的目标是替换奈飞的zuul。其实它和zuul2的性能差不多。选择其一即可。
考虑到spring的生态,使用spring-cloud-gateway更加有优势
2 核心概念
Route 网关的基础(断言为真时匹配到路由)
- id
- 目标uri
- 断言
- 过滤器
Predicate java8的函数,输入类型是webflux的ServerWebExchange,允许开发人员处理http请求
Filter是gateway上的过滤器,可以在请求发出前后做一些业务上的处理
整体流程
maven配置
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>3.0.4</version>
</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-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
</dependencies>
服务配置
spring:
application:
name: zuul-gateway-static
cloud:
gateway:
routes:
- id: test1
uri: "http://www.example.com/"
predicates:
- Path=/abc/**
server:
port: 10011
address: 127.0.0.1
debug: true
management:
endpoint:
gateway:
enabled: true
endpoints:
web:
exposure:
include: "*"
logging:
config: classpath:logback-spring.xml
#eureka.instance.ip-address=127.0.0.1
#eureka.client.serviceUrl.defaultZone=http://tom:123456@localhost:9010/eureka/
#eureka.instance.preferIpAddress=true
#eureka.instance.instance-id=${spring.application.name}:${server.address}:${server.port}
#eureka.client.healthcheck.enabled=true
#eureka.instance.lease-expiration-duration-in-seconds=20
#eureka.instance.lease-renewal-interval-in-seconds=15
访问页面就可以看到转发的数据 http://127.0.0.1:10011/abc
spring-cloud-gateway 接入consul转发服务信息
注册consul
consul:
discovery:
service-name: zuul-gateway-static
health-check-path: /health/status
prefer-ip-address: true
ip-address: 127.0.0.1
host: localhost
port: 8500
配置consul的转发信息
- id: lb-info
uri: lb://consul-server-producer
predicates:
- Path=/info/**
访问转发后的地址: http://127.0.0.1:10011/info/1231211
各类断言工厂(路由判断)
path路由断言工厂
- 配置
- id: pathInfo
uri: http://www.example.com/
predicates:
- Path=/abcd/{segment}
query路由断言工厂
- 配置
- id: queryInfo
uri: http://www.example.com/
predicates:
- Query= foo,bb
method路由断言工厂`
- 配置
- id: methodnfo
uri: http://www.example.com/
predicates:
- Method= DELETE
- 访问地址:
curl --location --request DELETE 'http://127.0.0.1:10011/adfasfsdfdsd'
head 路由断言工厂
- 配置
- id: headinfo
uri: http://www.example.com/
predicates:
- Header=x-ragnar-traceid,[\w\d]+
- 访问地址
curl --location --request GET 'http://127.0.0.1:10011/adfasfsdfdsd12312' \
--header 'x-ragnar-traceid: 123213123'
自定义路由断言工厂
自定义断言工厂代码
@Slf4j
@Component
public class GrayRoutePredicateFactory extends AbstractRoutePredicateFactory<GrayCfg> {
public GrayRoutePredicateFactory() {
super(GrayCfg.class);
}
@Override
public Predicate<ServerWebExchange> apply(GrayCfg cfg) {
return serverWebExchange -> {
log.info("enter GrayRoutePredicateFactory"+cfg.isGrayStatus());
if (cfg.isGrayStatus()) {
log.info(" GrayRoutePredicateFactory hit start gray");
return true;
}
return false;
};
}
}
自定义断言工厂配置
- id: grayinfo
uri: http://www.baidu.com/
predicates:
- Path=/eee/**
- name: Gray
args:
grayStatus: true
过滤器工厂(修改请求)
AddRequestHeader过滤器工厂
- AddRequestHeader=from,abc
RemoveRequestHeader过滤器工厂
- RemoveRequestHeader=from2
SetStatus过滤器工厂
- id: statusFilter
uri: http://www.example.com/
predicates:
- Query=foo,b1
filters:
- SetStatus=401
RedirectTo过滤器工厂
- id: redictFilter
uri: http://www.example.com/
predicates:
- Query=foo,b2
filters:
- RedirectTo=302,http://weibo.com
自定义过滤器工厂
- 自定义过滤器配置
- id: lb-info
uri: lb://consul-server-producer
predicates:
- Path=/info/**
filters:
- RemoveRequestHeader=from2
- AddRequestHeader=from,abc
- name: Login
args:
checkLogin: true
- 自定义过滤器代码@Slf4j
@Component
public class LoginGatewayFilterFactory extends AbstractGatewayFilterFactory<LoginCfg> {
public LoginGatewayFilterFactory() {
super(LoginCfg.class);
}
@Override
public GatewayFilter apply(LoginCfg loginCfg) {
return (exchange, chain) -> {
log.info("if us check login:"+ loginCfg.isCheckLogin());
ServerHttpRequest request = exchange.getRequest().mutate()
.build();
if (loginCfg.isCheckLogin()) {
log.info("do login checking.......");
}
return chain.filter(exchange.mutate().request(request).build());
};
}
}
全局过滤器
[图片上传失败...(image-f49665-1664766893624)]
自定义全局过滤器
package cn.beckbi.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import reactor.core.publisher.Mono;
/**
* @program: spring-cloud
* @description:
* @author: bikang
* @create: 2022-08-21 20:21
*/
@Slf4j
@Configuration
public class GlobalFilterConfig {
@Bean
@Order(-2)
public GlobalFilter filter1() {
return (exchange, chain) -> {
log.info("filter1 pre");
return chain.filter(
exchange
).then(
Mono.fromRunnable(()->{
log.info("filter1 post");
})
);
};
}
@Bean
@Order(0)
public GlobalFilter filter2() {
return (exchange, chain) -> {
log.info("filter2 pre");
return chain.filter(
exchange
).then(
Mono.fromRunnable(()->{
log.info("filter2 post");
})
);
};
}
@Bean
@Order(2)
public GlobalFilter filter3() {
return (exchange, chain) -> {
log.info("filter3 pre");
return chain.filter(
exchange
).then(
Mono.fromRunnable(()->{
log.info("filter3 post");
})
);
};
}
}
自定义拦截器
package cn.beckbi.filter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.loadbalancer.ResponseData;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.Objects;
/**
* @program: spring-cloud
* @description:
* @author: bikang
* @create: 2022-08-21 20:29
*/
@Slf4j
@Component
public class UserFilter implements GlobalFilter, Ordered {
@Builder
@Data
static class Resp {
private int code;
private String msg;
}
private static final String BAD_CID = "123";
private ObjectMapper mapper = new ObjectMapper();
@Override
public int getOrder(){
return 0;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
boolean matchFilter = false;
if (Objects.nonNull(queryParams) && Objects.nonNull(queryParams.get("cid"))) {
String cid = queryParams.get("cid").get(0);
if (Objects.nonNull(cid) && BAD_CID.equals(cid)) {
matchFilter = true;
}
}
if (matchFilter) {
ServerHttpResponse serverHttpResponse = exchange.getResponse();
Resp resp = Resp.builder()
.code(401)
.msg("非法请求")
.build();
DataBuffer dataBuffer = serverHttpResponse.bufferFactory().wrap(
this.getJsonBytes(resp)
);
serverHttpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
serverHttpResponse.getHeaders().add("Content-Type", "application/json; charset=utf-8");
return serverHttpResponse.writeWith(Mono.just(dataBuffer));
}
return chain.filter(exchange);
}
private byte[] getJsonBytes(Object o) {
try {
return mapper.writeValueAsBytes(o);
}catch (JsonProcessingException e) {
log.error("json error", e);
}
return "".getBytes();
}
}
http://127.0.0.1:10011/info/1231212312?cid=123
限流器代码:基于redis做限流
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
package cn.beckbi.limiter;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Mono;
import java.util.Objects;
/**
* @program: spring-cloud
* @description:
* @author: bikang
* @create: 2022-08-21 21:02
*/
@Configuration
public class LimiterConfig {
@Bean("ipKeyResolver")
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getHostName()
);
}
@Bean("cidKeyResolver")
public KeyResolver cidKeyResolver() {
return exchange -> Mono.just(
Objects.requireNonNull(exchange.getRequest().getQueryParams().getFirst("cid"))
);
}
@Primary
@Bean("apiKeyResolver")
public KeyResolver apiKeyResolver() {
return exchange -> {
Route route = (Route) exchange.getAttributes().get(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
return Mono.just(
route.getId()+"#"+exchange.getRequest().getPath().value()
);
};
}
}
限流器配置
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 2
key-resolver: "#{@apiKeyResolver}"
全局路由处理器
package cn.beckbi.errorhandler;
import cn.beckbi.util.JsonUtil;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.util.annotation.Nullable;
/**
* @program: spring-cloud
* @description:
* @author: bikang
* @create: 2022-08-21 21:36
*/
@Component
@Slf4j
@Order(-1)
public class JsonHandler implements ErrorWebExceptionHandler {
@Builder
@Data
static class Msg {
int code;
String msg;
}
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
return Mono.error(ex);
}
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
ServerHttpRequest request = exchange.getRequest();
String rawQuery = request.getURI().getRawQuery();
String query = StringUtils.hasText(rawQuery) ? "?" + rawQuery : "";
String path = request.getPath() + query ;
String message ;
HttpStatus status = null;
if (ex instanceof ResponseStatusException) {
status = ((ResponseStatusException) ex).getStatus();
}
if (status == null){
status = HttpStatus.INTERNAL_SERVER_ERROR;
}
// 通过状态码自定义异常信息
if (status.value() >= 400 && status.value() < 500){
message = "路由服务不可达或禁止访问!";
}else {
message = "路由服务异常!";
}
message += " path:" + path;
Msg msg = Msg.builder().code(status.value())
.msg(message)
.build();
return response
.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory();
return bufferFactory.wrap(JsonUtil.getJsonBytes(msg));
}));
}
}
代码路径: https://github.com/beckbikang/spring-cloud/tree/main/kgateway
spring-cloud-gateway动态路由
实现了spring-cloud的动态路由,一个真正可用的网关就成型了,从应用的角度来看,这就是spring-cloud的最后的一课了
动态路由其实不难实现 RouteDefinitionRepository 接口即可。
配置
spring:
application:
name: zuul-gateway-dynamic
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
consul:
discovery:
service-name: zuul-gateway-dynamic
health-check-path: /health/status
prefer-ip-address: true
ip-address: 127.0.0.1
host: localhost
port: 8500
redis:
database: 0
host: 127.0.0.1
port: 6379
connect-timeout: 100ms
timeout: 500ms
jedis:
pool:
max-active: 200
min-idle: 10
max-idle: 20
max-wait: 10000ms
server:
port: 10013
address: 127.0.0.1
debug: true
management:
endpoint:
gateway:
enabled: true
endpoints:
web:
exposure:
include: "*"
logging:
config: classpath:logback-spring.xml
代码
package cn.beckbi.route;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
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.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
/**
* @program: spring-cloud
* @description:
* @author: bikang
* @create: 2022-08-22 07:47
*/
@Slf4j
@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
private static final String GATEWAY_ROUTE = "SPRING_CLOUD_GATEWAY_ROUTE_LIST";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
List<RouteDefinition> routeDefinitionList = new ArrayList<>();
stringRedisTemplate.opsForHash().values(GATEWAY_ROUTE).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 -> {
stringRedisTemplate.opsForHash().put(GATEWAY_ROUTE, routeDefinition.getId(), JSON.toJSONString(routeDefinition));
return Mono.empty();
});
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return routeId.flatMap(id->{
if(stringRedisTemplate.opsForHash().hasKey(GATEWAY_ROUTE, id)){
stringRedisTemplate.opsForHash().delete(GATEWAY_ROUTE, id);
return Mono.empty();
}else {
return Mono.defer(() -> Mono.error(new Exception("routeDefinition not found:" + routeId)));
}
});
}
}
动态增加路由
curl --location --request POST 'http://localhost:10013/actuator/gateway/routes/test1' \
--header 'Content-Type: application/json' \
--data-raw '{"uri":"http://www.example.com/","predicates":[{"name":"Path","args":{"pattern":"/abc/**"}}],"filters":[{"name":"StripPrefix","args":{"value":1}}],"order":0}'