1.閘道器是怎麼演化來的
- 單體應用拆分成多個服務後,對外需要一個統一入口,解耦客戶端與內部服務
2.閘道器的基本功能
- 閘道器核心功能是路由轉發,因此不要有耗時操作在閘道器上處理,讓請求快速轉發到後端服務上
- 閘道器還能做統一的熔斷、限流、認證、日誌監控等
3.關於Spring Cloud Gateway
Spring Cloud Gateway是由spring官方基於Spring5.0、Spring Boot2.0、Project Reactor等技術開發的閘道器,使用非阻塞API,Websockets得到支援,目的是代替原先版本中的Spring Cloud Netfilx Zuul,目前Netfilx已經開源了Zuul2.0,但Spring 沒有考慮整合,而是推出了自己開發的Spring Cloud GateWay。這裡需要注意一下gateway使用的netty+webflux實現,不要加入web依賴(不要引用webmvc),否則初始化會報錯 ,需要加入webflux依賴。
gateway與zuul的簡單比較:gateway使用的是非同步請求,zuul是同步請求,gateway的資料封裝在ServerWebExchange裡,zuul封裝在RequestContext裡,同步方便調式,可以把資料封裝在ThreadLocal中傳遞。
Spring Cloud Gateway有三個核心概念:路由、斷言、過濾器
過濾器:gateway有兩種filter:GlobalFilter、GatewayFilter,全域性過濾器預設對所有路由有效。
文件地址:cloud.spring.io/spring-clou…
閘道器作為所有請求流量的入口,在實際生產環境中為了保證高可靠和高可用,儘量避免重啟,需要用到動態路由配置,在閘道器執行過程中更改路由配置
4.程式碼實踐
需要用到3個專案,eureka-server、gateway、consumer-service
- 1.eureka-server 服務發現註冊,供gateway轉發請求時獲取服務例項 ip+port,使用前面部落格中的示例程式碼
- 2.新建 gateway 閘道器專案,專案引用如下:
<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-actuator</artifactId>
</dependency>
123456789101112複製程式碼
在主類上啟用服務發現註冊註解 @EnableDiscoveryClient
配置檔案內容如下:
server:
port: 9999
spring:
profiles:
active: dev
application:
name: gateway-service
cloud:
gateway:
discovery:
locator:
enabled: true
# 服務名小寫
lower-case-service-id: true
routes:
- id: consumer-service
# lb代表從註冊中心獲取服務,且已負載均衡方式轉發
uri: lb://consumer-service
predicates:
- Path=/consumer/**
# 加上StripPrefix=1,否則轉發到後端服務時會帶上consumer字首
filters:
- StripPrefix=1
# 註冊中心
eureka:
instance:
prefer-ip-address: true
client:
service-url:
defaultZone: http://zy:zy123@localhost:10025/eureka/
# 暴露監控端點
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
123456789101112131415161718192021222324252627282930313233343536373839404142複製程式碼
上面就完成了閘道器程式碼部分,下面新建consumer-service
- 3.consumer-service 消費者服務 ,通過閘道器路由轉發到消費者服務,並返回資訊回去,因此是個mvc的專案
專案引用如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
12345678複製程式碼
在主類上啟用服務發現註冊註解 @EnableDiscoveryClient
在配置檔案中新增配置:
server.port=9700
spring.application.name=consumer-service
eureka.instance.prefer-ip-address=true
# 配置eureka-server security的賬戶資訊
eureka.client.serviceUrl.defaultZone=http://zy:zy123@localhost:10025/eureka/
12345複製程式碼
新建 IndexController ,新增一個 hello 方法,傳入name引數,訪問後返回 hi + name 字串
@RestController
public class IndexController {
@RequestMapping("/hello")
public String hello(String name){
return "hi " + name;
}
}
12345678複製程式碼
- 4.分別啟動3個專案,訪問 http://localhost:10025 看eureka上gateway與consumer-service例項是否註冊了,可以看到已經註冊了,分別在9700、9999埠
通過閘道器訪問consumer-service的hello方法,http://localhost:9999/consumer/hello?name=zy ,效果如下,說明請求已經轉發到consumer-service服務上了
以上完成了閘道器的基本程式碼,下面繼續介紹一些常用的過濾器,通過過濾器實現統一認證鑑權、日誌、安全等檢驗
- 5.在閘道器專案中新增 GlobalFilter 全域性過濾器,列印每次請求的url,程式碼如下:
/**
* 全域性過濾器
* 所有請求都會執行
* 可攔截get、post等請求做邏輯處理
*/
@Component
public class RequestGlobalFilter implements GlobalFilter,Ordered {
//執行邏輯
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest serverHttpRequest= exchange.getRequest();
String uri = serverHttpRequest.getURI().toString();
System.out.println(" uri : " + uri);//列印每次請求的url
String method = serverHttpRequest.getMethodValue();
if ("POST".equals(method)) {
//從請求裡獲取Post請求體
String bodyStr = resolveBodyFromRequest(serverHttpRequest);
//TODO 得到Post請求的請求引數後,做你想做的事
//下面的將請求體再次封裝寫回到request裡,傳到下一級,否則,由於請求體已被消費,後續的服務將取不到值
URI uri = serverHttpRequest.getURI();
ServerHttpRequest request = serverHttpRequest.mutate().uri(uri).build();
DataBuffer bodyDataBuffer = stringBuffer(bodyStr);
Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
request = new ServerHttpRequestDecorator(request) {
@Override
public Flux<DataBuffer> getBody() {
return bodyFlux;
}
};
//封裝request,傳給下一級
return chain.filter(exchange.mutate().request(request).build());
} else if ("GET".equals(method)) {
Map requestQueryParams = serverHttpRequest.getQueryParams();
//TODO 得到Get請求的請求引數後,做你想做的事
return chain.filter(exchange);
}
return chain.filter(exchange);
}
/**
* 從Flux<DataBuffer>中獲取字串的方法
* @return 請求體
*/
private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) {
//獲取請求體
Flux<DataBuffer> body = serverHttpRequest.getBody();
AtomicReference<String> bodyRef = new AtomicReference<>();
body.subscribe(buffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
DataBufferUtils.release(buffer);
bodyRef.set(charBuffer.toString());
});
//獲取request body
return bodyRef.get();
}
private DataBuffer stringBuffer(String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
}
//執行順序
@Override
public int getOrder() {
return 1;
}
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475複製程式碼
重新執行閘道器專案,並訪問 http://localhost:9999/consumer/hello?name=zy ,檢視控制檯,可看到 uri 日誌被列印出來了
- 6.在閘道器專案中新增 GatewayFilter 過濾器 ,我們給consumer-service 新增 token 認證過濾器 ,和全域性過濾器u同的是,GatewayFilter需要在配置檔案中指定那個服務使用此過濾器,程式碼如下:
/**
* 可對客戶端header 中的 Authorization 資訊進行認證
*/
@Component
public class TokenAuthenticationFilter extends AbstractGatewayFilterFactory {
private static final String Bearer_ = "Bearer ";
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
ServerHttpResponse response = exchange.getResponse();
try {
//String token = exchange.getRequest().getQueryParams().getFirst("authToken");
//1.獲取header中的Authorization
String header = request.getHeaders().getFirst("Authorization");
if (header == null || !header.startsWith(Bearer_)) {
throw new RuntimeException("請求頭中Authorization資訊為空");
}
//2.擷取Authorization Bearer
String token = header.substring(7);
//可把token存到redis中,此時直接在redis中判斷是否有此key,有則校驗通過,否則校驗失敗
if(!StringUtils.isEmpty(token)){
System.out.println("驗證通過");
//3.有token,把token設定到header中,傳遞給後端服務
mutate.header("userDetails",token).build();
}else{
//4.token無效
System.out.println("token無效");
DataBuffer bodyDataBuffer = responseErrorInfo(response , HttpStatus.UNAUTHORIZED.toString() ,"無效的請求");
return response.writeWith(Mono.just(bodyDataBuffer));
}
}catch (Exception e){
//沒有token
DataBuffer bodyDataBuffer = responseErrorInfo(response , HttpStatus.UNAUTHORIZED.toString() ,e.getMessage());
return response.writeWith(Mono.just(bodyDataBuffer));
}
ServerHttpRequest build = mutate.build();
return chain.filter(exchange.mutate().request(build).build());
};
}
/**
* 自定義返回錯誤資訊
* @param response
* @param status
* @param message
* @return
*/
public DataBuffer responseErrorInfo(ServerHttpResponse response , String status ,String message){
HttpHeaders httpHeaders = response.getHeaders();
httpHeaders.add("Content-Type", "application/json; charset=UTF-8");
httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
response.setStatusCode(HttpStatus.UNAUTHORIZED);
Map<String,String> map = new HashMap<>();
map.put("status",status);
map.put("message",message);
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(map.toString().getBytes());
return bodyDataBuffer;
}
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364複製程式碼
在配置檔案中指定consumer-service服務使用 TokenAuthenticationFilter ,配置如下:
routes:
- id: consumer-service
uri: lb://consumer-service
predicates:
- Path=/consumer/**
filters:
# 進行token過濾
- TokenAuthenticationFilter
- StripPrefix=1
123456789複製程式碼
執行專案,再次訪問 http://localhost:9999/consumer/hello?name=zy
- 7.前後端分離專案解決閘道器跨域問題,在閘道器主類中新增以下程式碼:
@Bean
public WebFilter corsFilter() {
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
if (!CorsUtils.isCorsRequest(request)) {
return chain.filter(ctx);
}
HttpHeaders requestHeaders = request.getHeaders();
ServerHttpResponse response = ctx.getResponse();
HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
HttpHeaders headers = response.getHeaders();
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
if (requestMethod != null) {
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
}
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "all");
headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600");
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
return chain.filter(ctx);
};
}
123456789101112131415161718192021222324252627複製程式碼
程式碼已上傳至碼雲,原始碼,專案使用的版本資訊如下:
- SpringBoot 2.0.6.RELEASE
- SpringCloud Finchley.SR2複製程式碼