Zuul是Spring Cloud全家桶中的微服務API網關。
所有從設備或網站來的請求都會經過Zuul到達後端的Netflix應用程序。作爲一個邊界性質的應用程序,Zuul提供了動態路由、監控、彈性負載和安全功能。Zuul底層利用各種filter實現如下功能:
•認證和安全 識別每個需要認證的資源,拒絕不符合要求的請求。
•性能監測 在服務邊界追蹤並統計數據,提供精確的生產視圖。
•動態路由 根據需要將請求動態路由到後端集羣。
•壓力測試 逐漸增加對集羣的流量以瞭解其性能。
•負載卸載 預先爲每種類型的請求分配容量,當請求超過容量時自動丟棄。
•靜態資源處理 直接在邊界返回某些響應。
zuul入門
新建一個服務zuul-service作爲路由網關服務。
pom.xml中引入依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
使用@EnableZuulProxy啓用Zuul(使用@EnableZuulServer也可以啓用Zuul,只是不會自動從Eureka中獲取並自動代理服務,也不會自動加載部分Zuul過濾器,但是可以選擇性地替換代理平臺的各個部分)。
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulApplicationStarter {
public static void main(String[] args) {
SpringApplication.run(ZuulApplicationStarter.class, args);
}
}
路由配置
server:
port: 9000
spring:
application:
name: zuul Service
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8771/eureka/
fetch-registry: false
zuul:
strip-prefix: false
routes:
users: #你自定義規則名字
path: /user/** #url地址
serviceId: one #路由到serviceid
ignored-patterns: /user/one #url攔截 防止外面通過url請求內部通信路徑
#簡便寫法
#one: /user/**
ribbon:
eureka:
enabled: false
one:
ribbon:
listOfServers: http://localhost:8773
management:
security:
enabled: false
相關參數
Zuul會自動讀取註冊中心的已經註冊的服務。user-service服務會自動設置/user-service/**這樣的路由,即/user-service/users會被代理到user-service服務的/users請求。
zuul.ignoredServices
可以指定忽略註冊中心獲取的服務
zuul.routes.=
zuul.routes.<key>.serviceId=<serviceId>
指定一個服務對應路由路徑爲zuul.routes.<key>.path
zuul.routes.<key>.url=<url>
指定一個服務的url或者使用forward轉向Zuul服務的接口,對應路由路徑爲zuul.routes.<key>.path
zuul.routes.<ribbon>=<path>
使用自定義Ribbon實現路由
Zuul服務啓動完成後,可以訪問http://localhost:9000/routes獲取路由列表
動態路由
Zuul結合SpringCloud配置中心,在修改路由配置信息後刷新配置可立即生效,無需重啓Zuul服務,這樣就實現了動態路由。
Zuul Filter
Zuul進行代理時,會有一系列的Zuul Filter對Http請求的request和response進行封裝和操作。
一個Zuul Filter有下面四個要素:
Type:類型。Zuul Filter的類型包括pre,routing,post和error。routing過濾器是在路由階段執行的,負責尋找原服務、請求轉發和返回接收。pre和post分別在routing之前和之後執行。如果Zuul執行代理的過程中拋出ZuulException異常,則會被error過濾器捕獲並進行相應處理。
Execution Order:執行順序。通過一個整型的值從小到大依次執行(相同類型過濾器間互相比較)。
Criteria:執行條件。當滿足一定條件時,纔會執行該過濾器。
Action:執行動作。當執行條件滿足時,進行的操作。
實現一個過濾器只要繼承ZuulFilter,並實現filterType(),filterOrder(),shouldFilter()和run()四個方法。這些方法與上面的四個要素對應。
如果要禁用一個Zuul過濾器,只需要配置zuul.<SimpleClassName>.<filterType>.disable=true
,比如需要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter需要配置zuul.SendResponseFilter.post.disable=true
。
下面我們使用一個pre過濾器實現token驗證,如果Http header裏面沒有一個固定的token,則禁止訪問。
禁用Zuul默認的error過濾器,設置固定的token和需要驗證的路由key名單
zuul:
# 禁用SpringCloud自帶的error filter
SendErrorFilter:
error:
disable: true
zuul-filter:
token-filter:
# 訪問時,需要進行認證的路由key
un-auth-routes:
- users
- smsApi
# 固定的token
static-token: xF2fdi8M
讀取自定義token配置信息
@Component
@ConfigurationProperties("zuulFilter.tokenFilter")
public class TokenValidateConfiguration {
// 在這個列表裏面存儲的routeId都是需要使用TokenValidateFilter過濾的
private List<String> unAuthRoutes;
// 給定的token
private String staticToken;
public List<String> getUnAuthRoutes() {
return unAuthRoutes;
}
public void setUnAuthRoutes(List<String> unAuthRoutes) {
this.unAuthRoutes = unAuthRoutes;
}
public String getStaticToken() {
return staticToken;
}
public void setStaticToken(String staticToken) {
this.staticToken = staticToken;
}
}
自定義過濾器
@Component
public class TokenValidateFilter extends ZuulFilter {
protected static final Logger logger = LoggerFactory.getLogger(TokenValidateFilter.class);
@Autowired
private TokenValidateConfiguration tvConfig;
@Override
public String filterType() {//類型
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {//執行順序
return FilterConstants.PRE_DECORATION_FILTER_ORDER;
}
@Override
public boolean shouldFilter() {//是否攔截
RequestContext ctx = RequestContext.getCurrentContext();
return tvConfig.getUnAuthRoutes().contains(ctx.get(FilterConstants.PROXY_KEY));
}
@Override
public Object run() {//攔截後操作
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = request.getHeader("Authorization");
if (token == null) {
logger.warn("Http Header Authorization is null");
forbidden();
return null;
}
String staticToken = tvConfig.getStaticToken();
if (StringUtils.isBlank(staticToken)) {
logger.warn("property zuulFilter.tokenFilter.staticToken was not set");
forbidden();
} else if (!staticToken.equals(token)) {
logger.warn("token is not valid");
forbidden();
}
return null;
}
/**
* 設置response的狀態碼爲403
*/
private void forbidden() {
// zuul中,將請求附帶的信息存在線程變量中。
RequestContext.getCurrentContext().setResponseStatusCode(HttpStatus.FORBIDDEN.value());
ReflectionUtils.rethrowRuntimeException(new ZuulException("token is not valid", HttpStatus.FORBIDDEN.value(),
"token校驗不通過"));
}
}
注意:如果使用zuul.routes.=方式配置的路由,則ctx.get(FilterConstants.PROXY_KEY)會得到去掉頭尾的url(/smsApi/**會得到smsApi,/smsApi/target/**會得到smsApi/target),而並非路由key。所以之前配置文件中的路由
測試:攜帶正確的token訪問成功
新建一個error過濾器,當捕獲到ZuulException時,返回一個JSON對象
@Component
public class SendErrorRestFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(SendErrorRestFilter.class);
@Override
public String filterType() {
return FilterConstants.ERROR_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.SEND_ERROR_FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
Throwable throwable = getCause(context.getThrowable());
// 獲取response狀態碼
int status = context.getResponseStatusCode();
JSONObject info = new JSONObject();
info.put("code", "異常碼" + status);
info.put("message", throwable.getMessage());
// 記錄日誌
logger.warn("請求異常,被error filter攔截", context.getClass());
// 設置response
context.setResponseBody(info.toJSONString());
context.getResponse().setContentType("application/json;charset=UTF-8");
context.getResponse().setStatus(HttpStatus.OK.value());
// 處理了異常之後清空異常
context.remove("throwable");
return null;
}
private Throwable getCause(Throwable throwable) {
while (throwable.getCause() != null) {
throwable = throwable.getCause();
}
return throwable;
}
}