架構圖:
一、搭建環境
<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做負載均衡。