簡介
服務網關
服務網關是微服務架構中一個不可或缺的部分。通過服務網關統一向外系統提供REST API的過程中,除了具備服務路由、均衡負載功能之外,它還具備了權限控制等功能。Spring Cloud Netflix中的Zuul就擔任了這樣的一個角色,爲微服務架構提供了前門保護的作用,同時將權限控制這些較重的非業務邏輯內容遷移到服務路由層面,使得服務集羣主體能夠具備更高的可複用性和可測試性。
服務網關 = 路由轉發 + 過濾器
- 路由轉發:接收一切外界請求,轉發到後端的微服務上去;例如,/可以映射到您的Web應用程序,/api/users映射到用戶服務,並將/api/shop映射到商店服務。
- 過濾器:在服務網關中可以完成一系列的橫切功能,例如權限校驗、限流以及監控等,這些都可以通過過濾器完成(其實路由轉發也是通過過濾器實現的)。
Zuul
Netflix使用Zuul進行以下操作:
- 認證
- 洞察
- 壓力測試
- 金絲雀測試
- 動態路由
- 服務遷移
- 負載脫落
- 安全
- 靜態響應處理
- 主動/主動流量管理
Zuul、Ribbon以及Eureka結合可以實現智能路由和負載均衡的功能;網關將所有服務的API接口統一聚合,統一對外暴露。外界調用API接口時,不需要知道微服務系統中各服務相互調用的複雜性,保護了內部微服務單元的API接口;網關可以做用戶身份認證和權限認證,防止非法請求操作API接口;網關可以實現監控功能,實時日誌輸出,對請求進行記錄;網關可以實現流量監控,在高流量的情況下,對服務降級;API接口從內部服務分離出來,方便做測試。
Zuul通過Servlet來實現,通過自定義的ZuulServlet來對請求進行控制。核心是一系列過濾器,可以在Http請求的發起和響應返回期間執行一系列過濾器。Zuul採取了動態讀取、編譯和運行這些過濾器。過濾器之間不能直接通信,而是通過RequestContext對象來共享數據,每個請求都會創建一個RequestContext對象。
Zuul生命週期如下圖。 當一個客戶端Request請求進入Zuul網關服務時,網關先進入”pre filter“,進行一系列的驗證、操作或者判斷。然後交給”routing filter“進行路由轉發,轉發到具體的服務實例進行邏輯處理、返回數據。當具體的服務處理完成後,最後由”post filter“進行處理,該類型的處理器處理完成之後,將Request信息返回客戶端。 在其他階段發生錯誤時執行”error filter“。 除了默認的過濾器類型,Zuul還允許我們創建自定義的過濾器類型。例如,我們可以定製一種STATIC類型的過濾器,直接在Zuul中生成響應,而不將請求轉發到後端的微服務。
創建服務網關
添加依賴包文件POM.xml
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.springcloud</groupId>
<artifactId>springcloud-root</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>springcloud-zuul</artifactId>
<name>springcloud-zuul</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置application.yml文件
- application.yml
spring:
application:
name: springcloud-zuul
freemarker:
prefer-file-system-access: false
security:
user:
name: admin
password: 123456
server:
port: 8120
eureka:
instance:
hostname: eureka-zuul.com
instance-id: eureka-zuul
client:
service-url:
defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eureka-peer1.com:8897/eureka/,http://${spring.security.user.name}:${spring.security.user.password}@eureka-peer2.com:8898/eureka/,http://${spring.security.user.name}:${spring.security.user.password}@eureka-peer3.com:8899/eureka/
zuul:
#接口前綴(v1作爲版本號)
prefix: /v1
routes:
hiapi:
path: /hiapi/**
serviceId: springcloud-eureka-provider
ribbonapi:
path: /ribbonapi/**
serviceId: springcloud-ribbon
feignapi:
path: /feignapi/**
serviceId: springcloud-feign
修改C:\Windows\System32\drivers\etc\hosts
127.0.0.1 eureka-zuul.com
禁用指定的Filter
可以在 application.yml 中配置需要禁用的 filter,格式爲
zuul.<SimpleClassName>.<filterType>.disable=true
比如要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter就設置
zuul:
SendResponseFilter:
post:
disable: true
添加服務網關啓動類
- ZuulApplication.java
加上註解@EnableZuulProxy
package org.springcloud.zuul;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
依次啓動項目
springcloud-eureka-cluster-peer1
springcloud-eureka-cluster-peer2
springcloud-eureka-cluster-peer3
springcloud-eureka-provider1
springcloud-eureka-provider2
springcloud-eureka-provider3
springcloud-ribbon
springcloud-feign
springcloud-zuul
-
瀏覽器多次訪問http://eureka-zuul.com:8120/v1/hiapi/hi?name=zhaojq
zuul在路由轉發做了負載均衡 -
瀏覽器多次訪問http://eureka-zuul.com:8120/v1/ribbonapi/hi?name=zhaojq
Zuul和Ribbon相結合,實現了負載均衡(隨機策略) -
瀏覽器多次訪問http://eureka-zuul.com:8120/v1/feignapi/hi?name=zhaojq
Zuul和Feign相結合,實現了負載均衡(隨機策略)
在服務網關Zuul上配置熔斷器
實現熔斷器功能需要實現FallbackProvider接口,實現該接口的兩個方法,一個是getRoute(),用於指定熔斷器功能應用於哪些路由的服務;另一個方法fallbackResponse()爲進入熔斷器功能時執行的邏輯。
- ZuulHystrix.java
如果需要所有的路由服務都加熔斷功能,需要在getRoute()方法上返回”*“的匹配符
package org.springcloud.zuul;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
@Component
public class ZuulHystrix implements FallbackProvider {
@Override
//指定熔斷器功能應用於哪些路由的服務
public String getRoute() {
//return "springcloud-eureka-provider";
return "*";
}
@Override
//進入熔斷器功能時執行的邏輯
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
System.out.println("route:"+route);
System.out.println("exception:"+cause.getMessage());
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "ok";
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("eureka-provider is down!! this is the fallback.".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
全部正常啓動後,停止 springcloud-eureka-provider2 提供者,端口爲:8002服務
訪問命令窗口curl http://eureka-zuul.com:8120/v1/hiapi/hi?name=zhaojq,斷路器已經生效,提示:提供者服務掛了
自定義過濾器Zuul Filter
Zuul包括以下4中過濾器
- PRE過濾器:是在請求路由到具體服務之前執行的,可以做安全驗證,如身份驗證,參數驗證。
- ROUTING過濾器:它用於將請求 路由到具體的微服務實例。默認使用Http Client進行網絡請求。
- POST過濾器:在請求已被路由到微服務後執行的。可用作收集統計信息、指標,以及將響應傳輸到客戶端。
- ERROR過濾器:在其他過濾器發生錯誤時執行。
Zuul中默認實現的filter
類型 | 順序 | 過濾器 | 功能 |
---|---|---|---|
pre | -3 | ServletDetectionFilter | 標記處理Servlet的類型 |
pre | -2 | Servlet30WrapperFilter | 包裝HttpServletRequest請求 |
pre | -1 | FormBodyWrapperFilter | 包裝請求體 |
route | 1 | DebugFilter | 標記調試標誌 |
route | 5 | PreDecorationFilter | 處理請求上下文供後續使用 |
route | 10 | RibbonRoutingFilter | serviceId請求轉發 |
route | 100 | SimpleHostRoutingFilter | url請求轉發 |
route | 500 | SendForwardFilter | forward請求轉發 |
post | 0 | SendErrorFilter | 處理有錯誤的請求響應 |
post | 1000 | SendResponseFilter | 處理正常的請求響應 |
實現自定義濾器需要繼承ZuulFilter,實現ZuulFilter中的抽象方法,包括filterType(),filterOrder()以及IZuulFilter的shouldFilter()和run()方法。
- filterType()爲過濾器類型,有4中類型:pre、post、routing和error。
- filterOrder()是過濾順序,它爲一個int類型的值,值越小,越早執行該過濾器。
- shouldFilter()表示是否需要執行該過濾器邏輯,true表示執行,false表示不執行,如果true則執行run()方法。
- run()方法是具體的過濾的邏輯。本例中檢查請求的參數中是否傳了token或password這個參數,如果沒有傳,則請求不被路由到具體的服務實例,直接返回響應,狀態碼爲401。
TokenFilter 過濾器
- TokenFilter.java
package org.springcloud.zuul;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
public class TokenFilter extends ZuulFilter {
private static Logger LOGGER=LoggerFactory.getLogger(TokenFilter.class);
@Override
//定義filter的類型,有pre、route、post、error四種
public String filterType() {
//可以在請求被路由之前調用
return "pre";
}
@Override
//定義filter的順序,數字越小表示順序越高,越先執行
public int filterOrder() {
return 0;
}
@Override
//表示是否需要執行該filter,true表示執行,false表示不執行
public boolean shouldFilter() {
return true;
}
@Override
//filter需要執行的具體操作
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
LOGGER.info("--->>> TokenFilter {},{}", request.getMethod(), request.getRequestURL().toString());
//獲取請求的參數
String token = request.getParameter("token");
if (StringUtils.isNotBlank(token)) {
//對請求進行路由
ctx.setSendZuulResponse(true);
ctx.setResponseStatusCode(200);
ctx.set("isSuccess", true);
return null;
} else {
LOGGER.warn("token is empty");
//不對請求進行路由
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
try {
ctx.getResponse().getWriter().write("token is empty");
} catch (IOException e) {
e.printStackTrace();
}
ctx.set("isSuccess", false);
return null;
}
}
}
開啓過濾器,在程序的啓動類 ZuulFilterApplication 添加 Bean
- ZuulApplication.java
package org.springcloud.zuul;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
@Bean
public TokenFilter TokenFilter() {
return new TokenFilter();
}
}
重啓項目後,訪問http://eureka-zuul.com:8120/v1/hiapi/hi?name=zhaojq
提示 token is empty。
訪問http://eureka-zuul.com:8120/v1/hiapi/hi?name=zhaojq&token=cc
可見,TokenFilter這個Bean注入IOC容器後,對請求進行了過濾,並在請求路由轉發之前進行了邏輯判斷。
提示 token is empty顯示兩遍錯誤
把自定義TokenFilter的@Component取消掉就可以了,不讓它被Spring容器管理。
PasswordFilter 過濾器
- PasswordFilter.java
package org.springcloud.zuul;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
public class PasswordFilter extends ZuulFilter {
private final Logger LOGGER = LoggerFactory.getLogger(PasswordFilter.class);
@Override
public String filterType() {
//請求已被路由到微服務後執行
return "post";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
//判斷上一個過濾器結果爲true,否則就不走下面過濾器,直接跳過後面的所有過濾器並返回 上一個過濾器不通過的結果
return (boolean) ctx.get("isSuccess");
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
LOGGER.info("--->>> PasswordFilter {},{}", request.getMethod(), request.getRequestURL().toString());
String username = request.getParameter("password");
if (null != username && username.equals("123456")) {
ctx.setSendZuulResponse(true);
ctx.setResponseStatusCode(200);
ctx.set("isSuccess", true);
return null;
} else {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
try {
ctx.getResponse().getWriter().write("The password cannot be empty");
} catch (IOException e) {
e.printStackTrace();
}
ctx.set("isSuccess", false);
return null;
}
}
}
開啓過濾器,在程序的啓動類 ZuulFilterApplication 添加 Bean
- ZuulApplication.java
package org.springcloud.zuul;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
@Bean
public TokenFilter TokenFilter() {
return new TokenFilter();
}
@Bean
public PasswordFilter PasswordFilter() {
return new PasswordFilter();
}
}
訪問http://eureka-zuul.com:8120/v1/hiapi/hi?name=zhaojq&token=cc&password=123456