架构图:
一、搭建环境
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
server:
port: 8080 #端口
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
-id: service-order
uri: http://127.0.0.1:9001
predicates:
-path=/order/**
package com.springcloud.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class,args);
}
}
注意这个url和之前zuul的url不同,留给小伙伴自己去对比哦!
动态路由:
引入eureka依赖和配置,懒得写了。
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: service-order
# uri: http://127.0.0.1:9001
uri: lb://service-order
predicates:
- Path=/order/**
一个骚操作:
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: service-order
uri: lb://service-order
predicates:
- Path=/service-order/**
filters:
- RewritePath=/service-order/(?<segment>.*), /$\{segment}
我先给大家解释一下,这个是什么含义:- Path=/service-order/** 说明想要访问我们的路由,url必须是:localhost:8080/service-order/开头的路径,同时呢下面有一个过滤器(正则表达式),比如:http://localhost:8080/service-order/order/buy/1你访问的url就是这个,那么在过滤器将这个访问编程http://localhost:8080/order/buy/1,然后调用 http://service-order/order/buy/1。
有人这个时候可能就要喷我了,你这不闲的蛋疼吗?直接用上面最开始写的,就是这么搞的,难道不香吗?
首先大家要明确一个事,就是考虑安全的问题,如果按照刚开始这样做,不用正则表达式这种方法,看看会出现什么问题,如果你前端调用接口的代码,让人搞到了。是http:192.168.1.33:8080/order/buy/1(瞎写一个IP端口)。那么整个后面的/order/buy/1就完全暴露了给别人,如果别人知道了你消费者的ip和端口,就可以直接调用你的接口了。这样非常不安全!!!
如果采取刚才的正则替换的方式,别人只能得到/service-order/order/buy/1,那么他即使知道了你消费者的ip和端口,也不知道具体调用的路径,因为他调用这个/service-order/order/buy/1是不正确的。争取的是:/order/buy/1这个路径。这回清楚了吧,这不是蛋疼的操作,是安全的操作。
开启自动配置
spring:
application:
name: gateway-server
cloud:
gateway:
# 开启自动配置的根据服务名称进行路由转发
discovery:
locator:
enabled: true # 开启服务名称自动转发
lower-case-service-id: true # 服务名称以小写形式呈现
如果输入localhost:8080/service-order/order/buy/1,那么就会根据service-order匹配对应的服务(从注册中心得到),然后访问/order/buy/1路径。
二、过滤器
Spring Cloud Gateway 的 Filter 的生命周期不像 Zuul 的那么丰富,它只有两个:“pre” 和 “post”。
-
PRE : 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
-
POST :这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
- GatewayFilter:应用到单个路由或者一个分组的路由上。
- GlobalFilter:应用到所有的路由上。
局部过滤器
全局过滤器
自定义全局过滤器
package com.springcloud.demo.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public class LoginFilter implements GlobalFilter, Ordered {
/**
* 真正执行逻辑的代码
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("进入过滤器");
String token = exchange.getRequest().getQueryParams().getFirst("access-token");
if(token == null){
System.out.println("没有登陆!");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
/**
* 过滤器链:返回值越小,越靠前执行。
*/
@Override
public int getOrder() {
return 0;
}
}
三、限流算法
1、计数器
2、漏桶算法
在网关内部存在一个一定容量的队列,之后又rate程序从队列中取出请求,这个是有要求的,比如每秒调用两次,其余的请求都在队列中,如果突然请求暴增,那么也必须先到队列里,多余的直接丢弃。这种算法主要是保护微服务的安全。
3、令牌桶算法
当网关服务启动的时候,一个令牌队列(不一定非得是队列)中,请求到来的时候,需要获得令牌才能够访问对应的服务接口,初始化的时候有100个令牌,当这个时候有一千个请求过来的时候,可以抵挡钱100个请求,其余请求全部丢弃,这个时候,生成令牌的程序会每秒生成一个令牌,所以请求只能在有令牌的时候才能访问微服务,其实这样是保证了网关服务的安全。
四、基于Filter的限流
<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>
修改配置文件
server:
port: 8080 #端口
spring:
application:
name: api-gateway-server #服务名称
redis:
host: localhost
pool: 6379
database: 0
cloud: #配置SpringCloudGateway的路由
gateway:
routes:
- id: product-service
uri: lb://service-product
predicates:
- Path=/product-service/**
filters:
- name: RequestRateLimiter
args:
# 使用SpEL从容器中获取对象
key-resolver: '#{@pathKeyResolver}'
# 令牌桶每秒填充平均速率
redis-rate-limiter.replenishRate: 1
# 令牌桶的上限
redis-rate-limiter.burstCapacity: 3
- RewritePath=/product-service/(?<segment>.*), /$\{segment}
#eureka注册中心
eureka:
client:
service-url:
defaultZone: http://localhost:9000/eureka/
instance:
prefer-ip-address: true #使用ip地址注册
filters就是我们新增的配置。key-resolver的value需要我们增加一个注入的对象(需要配置)。
package com.springcloud.demo.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @ClassName KeyResolverConfiguration
* @Description
* @Author
* @Date 2020/5/29 10:54
* @Version 1.0
**/
@Configuration
public class KeyResolverConfiguration {
/**
* 基于路径的限流规则
*/
@Bean
public KeyResolver pathKeyResolver(){
//自定义KeyResolver
return new KeyResolver() {
/**
* @param exchange 上下文参数。
*/
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getPath().toString());
}
};
}
}
启动redis
开启客户端监控。
我们一直访问就会出现:
配置不同的限流规则:
/**
* 基于ip地址的限流
*/
@Bean
public KeyResolver ipKeyResolver(){
return exchange -> Mono.just(
exchange.getRequest().getHeaders().getFirst("X-Forwarded-For")
);
}
/**
* 基于参数的限流
*/
@Bean
public KeyResolver userKeyResolver(){
return exchange -> Mono.just(
exchange.getRequest().getQueryParams().getFirst("user")
);
}
在配置文件修改即可。
5、基于sentinel的限流
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>1.6.3</version>
</dependency>
注释掉配置文件中的一些信息
添加新的配置类
package com.springcloud.demo.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayParamFlowItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* sentinel配置
*/
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
/**
* 配置限流的异常处理器:SentinelGatewayBlockExceptionHandler
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
/**
* 配置限流过滤器
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
/**
* 配置初始化的限流参数
* 用于指定资源的限流规则
* 1、资源名称(路由id)
* 2、统计时间 setIntervalSec
* 3、配置限流阈值 setCount
*/
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
//每秒访问一次
rules.add(new GatewayFlowRule("service-order")
.setCount(1)
.setIntervalSec(1)
);
GatewayRuleManager.loadRules(rules);
}
}
当一秒中访问多次 就会限流。
注意:这个不需要启动sentinel的项目jar包,当然也可以启动之后动态配置。
自定义异常提示
-
setBlockHandler :注册函数用于实现自定义的逻辑处理被限流的请求,对应接口为BlockRequestHandler 。默认实现为 DefaultBlockRequestHandler ,当被限流时会返回类似于下面的错误信息: Blocked by Sentinel: FlowException 。
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 001);
map.put("message", "对不起,接口限流了");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON_UTF8).
body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
参数限流
rules.add(new GatewayFlowRule("order-service")
.setCount(1)
.setIntervalSec(1)
.setParamItem(new GatewayParamFlowItem()
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM)
.setFieldName("id")
)
);
自定义api分组
/**
* 自定义api限流分组
* 1、定义分组
* 2、对小组配置限流规则
*/
@PostConstruct
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("order_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/service-order/order/**").
setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
ApiDefinition api2 = new ApiDefinition("person_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/service-person/person"));
}});
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
添加到rules
四、高可用
我们现在是单点的网关,如果挂掉咋整?要保证高可用那么就必须使用集群这种方式,所以我们可以搞一个网关集群,那么问题来了,如果前端咋调用接口?不同的ip,所以呢要用一个nginx做负载均衡。