請(qǐng)先閱讀之前的內(nèi)容:
- Spring Cloud 學(xué)習(xí)筆記 - No.1 服務(wù)注冊(cè)發(fā)現(xiàn)
- Spring Cloud 學(xué)習(xí)筆記 - No.2 服務(wù)消費(fèi) Ribbon & Feign
- Spring Cloud 學(xué)習(xí)筆記 - No.3 分布式配置 Config
- Spring Cloud 學(xué)習(xí)筆記 - No.4 斷路器 Hystrix
什么是服務(wù)網(wǎng)關(guān)
在之前的例子中汤锨,我們啟動(dòng)了一個(gè)外部服務(wù) eureka-consumer
,端口 3001
厌小。
同時(shí)我們也啟動(dòng)了兩個(gè)內(nèi)部服務(wù) eureka-client
筛欢,端口 2001
和 2002
浸锨,該外部服務(wù)通過 Ribbon 或 Feign 來在客戶端負(fù)載均衡地調(diào)用內(nèi)部服務(wù)。
之前我們都是通過 http://127.0.0.1:3001/consumer 來調(diào)用外部服務(wù) eureka-consumer
提供的服務(wù) /consumer
悴能。
問題來了:
假設(shè)我們啟動(dòng)了另外一個(gè)外部服務(wù) eureka-consumer
揣钦,端口 3002
。此時(shí)外部用戶只能通過 http://127.0.0.1:3002/consumer 來訪問漠酿,但是外部用戶可能并不知道 3002 這個(gè)端口冯凹。
服務(wù)網(wǎng)關(guān)是微服務(wù)架構(gòu)中一個(gè)不可或缺的部分。
通過服務(wù)網(wǎng)關(guān)統(tǒng)一向外系統(tǒng)提供 REST API 的過程中,除了具備服務(wù)路由宇姚、均衡負(fù)載功能之外匈庭,它還具備了權(quán)限控制等功能。
Spring Cloud Netflix 中的 Zuul 就擔(dān)任了這樣的一個(gè)角色浑劳,為微服務(wù)架構(gòu)提供了前門保護(hù)的作用阱持,同時(shí)將權(quán)限控制這些較重的非業(yè)務(wù)邏輯內(nèi)容遷移到服務(wù)路由層面,使得服務(wù)集群主體能夠具備更高的可復(fù)用性和可測(cè)試性魔熏。
構(gòu)建服務(wù)網(wǎng)關(guān) api-gateway
可以通過如下的 Spring Assistant
插件來創(chuàng)建項(xiàng)目 api-gateway
衷咽,添加 Zuul
等作為依賴。
在 pom.xml
中自動(dòng)導(dǎo)入了如下的依賴:
<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>
注意蒜绽,如果是 Finchley 版本的 Spring Cloud镶骗,需要再添加如下依賴:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
否則,啟動(dòng)時(shí)會(huì)報(bào)如下的錯(cuò)誤:
ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.0.3.RELEASE:run (default-cli) on project eureka-consumer: An exception occurred while running. null: InvocationTargetException: Error creating bean with name 'hystrixCommandAspect' defined in class path resource [org/springframework/cloud/netflix/hystrix/HystrixCircuitBreakerConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect]: Factory method 'hystrixCommandAspect' threw exception; nested exception is java.lang.NoClassDefFoundError: org/aspectj/lang/JoinPoint: org.aspectj.lang.JoinPoint -> [Help 1]
在主程序中通過 @EnableZuulProxy
注解開啟 Zuul 的功能:
@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
在 application.properties
躲雅,配置服務(wù)名鼎姊,端口及 Eureka 服務(wù)注冊(cè)中心的地址:
spring.application.name=api-gateway
server.port=7001
eureka.client.serviceUrl.defaultZone=http://localhost:1234/eureka/
最后通過 mvn spring-boot:run
啟動(dòng)該項(xiàng)目,它自己也作為一個(gè)服務(wù)注冊(cè)到 Eureka 服務(wù)注冊(cè)中心相赁。它除了會(huì)將自己注冊(cè)到 Eureka 服務(wù)注冊(cè)中心上之外相寇,也會(huì)從注冊(cè)中心獲取所有服務(wù)以及它們的實(shí)例清單。
因此服務(wù)網(wǎng)關(guān) Zuul 本身就已經(jīng)維護(hù)了系統(tǒng)中所有 serviceId
與實(shí)例地址的映射關(guān)系钮科,例如唤衫,它知道 eureka-consumer
這個(gè) serviceId
對(duì)應(yīng)到兩個(gè)地址:
當(dāng)有外部請(qǐng)求到達(dá)服務(wù)網(wǎng)關(guān) Zuul 的時(shí)候,根據(jù)請(qǐng)求的 URL 路徑找到最佳匹配的 path
規(guī)則跺嗽,將該請(qǐng)求路由到哪個(gè)具體的serviceId
上去战授,并且通過 Ribbon 來實(shí)現(xiàn)負(fù)載均衡策略。
一個(gè)默認(rèn)的服務(wù)網(wǎng)關(guān)就構(gòu)建完畢了桨嫁。由于 Spring Cloud Zuul 在整合了 Eureka 之后,具備默認(rèn)的服務(wù)路由功能份帐,即:當(dāng)我們這里構(gòu)建的 api-gateway
應(yīng)用啟動(dòng)并注冊(cè)到 Eureka 之后璃吧,服務(wù)網(wǎng)關(guān) Zull 會(huì)發(fā)現(xiàn)上面我們啟動(dòng)的兩個(gè)服務(wù) eureka-client
和 eureka-consumer
,這時(shí)候 Zuul 就會(huì)創(chuàng)建路由規(guī)則废境。
每個(gè)路由規(guī)則都包含兩部分畜挨,一部分是外部請(qǐng)求的匹配規(guī)則,另一部分是路由的服務(wù) ID噩凹。針對(duì)當(dāng)前示例的情況巴元,Zuul 會(huì)創(chuàng)建下面的四個(gè)路由規(guī)則,其中:
- 轉(zhuǎn)發(fā)到
eureka-client
服務(wù)的請(qǐng)求規(guī)則為:/eureka-client/**
- 轉(zhuǎn)發(fā)到
eureka-consumer
服務(wù)的請(qǐng)求規(guī)則為:/eureka-consumer/**
在之前的示例中驮宴,我們都是通過 http://127.0.0.1:3001/consumer 或者 http://127.0.0.1:3002/consumer 來調(diào)用 eureka-consumer
提供的服務(wù) /consumer
逮刨。
在啟動(dòng)了服務(wù)網(wǎng)關(guān)后,我們就可以通過 http://127.0.0.1:7001/eureka-consumer/consumer 來實(shí)現(xiàn)同樣的效果堵泽,該請(qǐng)求將最終被路由到 eureka-consumer
的/consumer
接口上修己。
傳統(tǒng)路由配置
所謂的傳統(tǒng)路由配置方式就是在不依賴于服務(wù)發(fā)現(xiàn)機(jī)制的情況下恢总,通過在配置文件中具體指定每個(gè)路由表達(dá)式與服務(wù)實(shí)例的映射關(guān)系來實(shí)現(xiàn) API 網(wǎng)關(guān)對(duì)外部請(qǐng)求的路由。
沒有 Eureka 服務(wù)治理框架幫助的時(shí)候睬愤,我們需要根據(jù)服務(wù)實(shí)例的數(shù)量采用不同方式的配置來實(shí)現(xiàn)路由規(guī)則片仿。
單實(shí)例配置:
zuul.routes.eureka-consumer.path=/eureka-consumer/**
zuul.routes.eureka-consumer.url=http://127.0.0.1:3001/
多實(shí)例配置:由于存在多個(gè)實(shí)例,API 網(wǎng)關(guān)在進(jìn)行路由轉(zhuǎn)發(fā)時(shí)需要實(shí)現(xiàn)負(fù)載均衡策略尤辱,于是這里還需要 Spring Cloud Ribbon 的配合砂豌。由于在 Spring Cloud Zuul 中自帶了對(duì) Ribbon 的依賴,所以我們只需要做一些配置即可光督。
zuul.routes.eureka-consumer.path=/eureka-consumer/**
zuul.routes.eureka-consumer.serviceId=eureka-consumer
ribbon.eureka.enabled=false
eureka-consumer.ribbon.listOfServers=http://127.0.0.1:3001/, http://127.0.0.1:3002/
不論是單實(shí)例還是多實(shí)例的配置方式奸鸯,我們都需要為每一對(duì)映射關(guān)系指定一個(gè)名稱,也就是上面配置中的 <route>
可帽,每一個(gè) <route>
就對(duì)應(yīng)了一條路由規(guī)則娄涩。
每條路由規(guī)則都需要通過 path
屬性來定義一個(gè)用來匹配客戶端請(qǐng)求的路徑表達(dá)式,并通過 url
或 serviceId
屬性來指定請(qǐng)求表達(dá)式映射具體實(shí)例地址或服務(wù)名映跟。
服務(wù)路由配置
Spring Cloud Zuul 通過與 Spring Cloud Eureka 的整合蓄拣,實(shí)現(xiàn)了對(duì)服務(wù)實(shí)例的自動(dòng)化維護(hù),所以在使用服務(wù)路由配置的時(shí)候努隙,我們不需要向傳統(tǒng)路由配置方式那樣為 serviceId
去指定具體的服務(wù)實(shí)例地址球恤,只需要通過一組 zuul.routes.<route>.path
與 zuul.routes.<route>.serviceId
參數(shù)對(duì)的方式配置即可,例如:
zuul.routes.eureka-consumer.path=/eureka-consumer/**
zuul.routes.eureka-consumer.serviceId=eureka-consumer
對(duì)于面向服務(wù)的路由配置荸镊,除了使用 path
與 serviceId
映射的配置方式之外咽斧,還有一種更簡(jiǎn)潔的配置方式:zuul.routes.<serviceId>=<path>
,其中 <serviceId>
用來指定路由的具體服務(wù)名躬存,<path>
用來配置匹配的請(qǐng)求表達(dá)式张惹,例如:
zuul.routes.eureka-consumer=/eureka-consumer/**
過濾器
思考這么一個(gè)問題:每個(gè)客戶端用戶請(qǐng)求微服務(wù)應(yīng)用提供的接口時(shí),它們的訪問權(quán)限往往都需要有一定的限制岭洲,系統(tǒng)并不會(huì)將所有的微服務(wù)接口都對(duì)它們開放宛逗。為了實(shí)現(xiàn)對(duì)客戶端請(qǐng)求的安全校驗(yàn)和權(quán)限控制,最簡(jiǎn)單和粗暴的方法就是為每個(gè)微服務(wù)應(yīng)用都實(shí)現(xiàn)一套用于校驗(yàn)簽名和鑒別權(quán)限的過濾器或攔截器盾剩。不過雷激,這樣的做法并不可取,它會(huì)增加日后的系統(tǒng)維護(hù)難度告私,因?yàn)橥粋€(gè)系統(tǒng)中的各種校驗(yàn)邏輯很多情況下都是大致相同或類似的屎暇,這樣的實(shí)現(xiàn)方式會(huì)使得相似的校驗(yàn)邏輯代碼被分散到了各個(gè)微服務(wù)中去,冗余代碼的出現(xiàn)是我們不希望看到的驻粟。
對(duì)于這樣的問題根悼,更好的做法是通過前置的網(wǎng)關(guān)服務(wù)來完成這些非業(yè)務(wù)性質(zhì)的校驗(yàn)。由于網(wǎng)關(guān)服務(wù)的加入,外部客戶端訪問我們的系統(tǒng)已經(jīng)有了統(tǒng)一入口番挺,既然這些校驗(yàn)與具體業(yè)務(wù)無關(guān)唠帝,那何不在請(qǐng)求到達(dá)的時(shí)候就完成校驗(yàn)和過濾,而不是轉(zhuǎn)發(fā)后再過濾而導(dǎo)致更長(zhǎng)的請(qǐng)求延遲玄柏。同時(shí)襟衰,通過在網(wǎng)關(guān)中完成校驗(yàn)和過濾佑力,微服務(wù)應(yīng)用端就可以去除各種復(fù)雜的過濾器和攔截器了仲智,這使得微服務(wù)應(yīng)用的接口開發(fā)和測(cè)試復(fù)雜度也得到了相應(yīng)的降低叉钥。
Zuul 允許開發(fā)者在 API 網(wǎng)關(guān)上通過定義過濾器來實(shí)現(xiàn)對(duì)請(qǐng)求的攔截與過濾茄厘,實(shí)現(xiàn)的方法非常簡(jiǎn)單,我們只需要繼承 ZuulFilter
抽象類并實(shí)現(xiàn)它定義的四個(gè)抽象函數(shù)就可以完成對(duì)請(qǐng)求的攔截和過濾了:
-
過濾類型
String filterType();
在 Zuul 中默認(rèn)定義了四種不同生命周期的過濾器類型爹谭,具體如下:-
pre
:可以在請(qǐng)求被路由之前調(diào)用介时。 -
routing
:在路由請(qǐng)求時(shí)候被調(diào)用报咳。 -
post
:在routing和error過濾器之后被調(diào)用椎咧。 -
error
:處理請(qǐng)求時(shí)發(fā)生錯(cuò)誤時(shí)被調(diào)用玖详。
-
-
執(zhí)行順序
int filterOrder();
通過int
值來定義過濾器的執(zhí)行順序,數(shù)值越小優(yōu)先級(jí)越高勤讽。 -
執(zhí)行條件
boolean shouldFilter();
返回一個(gè)boolean
類型來判斷該過濾器是否要執(zhí)行蟋座。我們可以通過此方法來指定過濾器的有效范圍。 -
具體操作
Object run();
過濾器的具體邏輯脚牍。在該函數(shù)中向臀,我們可以實(shí)現(xiàn)自定義的過濾邏輯,來確定是否要攔截當(dāng)前的請(qǐng)求诸狭,不對(duì)其進(jìn)行后續(xù)的路由券膀,或是在請(qǐng)求路由返回結(jié)果之后,對(duì)處理結(jié)果做一些加工等驯遇。
路由功能在真正運(yùn)行時(shí)芹彬,它的路由映射和請(qǐng)求轉(zhuǎn)發(fā)都是由幾個(gè)不同的過濾器完成的:
-
路由映射主要通過
pre
類型的過濾器完成,它將請(qǐng)求路徑與配置的路由規(guī)則進(jìn)行匹配妹懒,以找到需要轉(zhuǎn)發(fā)的目標(biāo)地址雀监; -
請(qǐng)求轉(zhuǎn)發(fā)由
route
類型的過濾器來完成,對(duì)pre
類型過濾器獲得的路由地址進(jìn)行轉(zhuǎn)發(fā)眨唬。
所以,過濾器可以說是 Zuul 實(shí)現(xiàn)服務(wù)網(wǎng)關(guān)功能最為核心的部件好乐,每一個(gè)進(jìn)入 Zuul 的 HTTP 請(qǐng)求都會(huì)經(jīng)過一系列的過濾器處理鏈得到請(qǐng)求響應(yīng)并返回給客戶端匾竿。
圖片引自:http://blog.didispace.com/spring-cloud-source-zuul/
在服務(wù)網(wǎng)關(guān) api-gateway 中添加過濾器
我們?cè)谏厦娴捻?xiàng)目 api-gateway
中創(chuàng)建 AccessFilter.java
:
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
public class AccessFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(AccessFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
log.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString());
Object accessToken = request.getParameter("accessToken");
if (accessToken == null) {
log.warn("access token is empty");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody("unauthorized");
return null;
}
log.info("access token ok");
return null;
}
}
隨后在主程序中創(chuàng)建具體的 Bean:
@Bean
public AccessFilter accessFilter() {
return new AccessFilter();
}
重啟 api-gateway
,訪問 http://127.0.0.1:7001/eureka-consumer/consumer:
加上 accessToken
參數(shù)訪問 http://127.0.0.1:7001/eureka-consumer/consumer?accessToken=12345:
核心過濾器
圖片引用自:http://blog.didispace.com/spring-cloud-zuul-exception-3/
拓展閱讀
引用自:
- Spring Cloud實(shí)戰(zhàn)小貼士:Zuul處理Cookie和重定向
- Spring Cloud實(shí)戰(zhàn)小貼士:Zuul統(tǒng)一異常處理(一)
- Spring Cloud實(shí)戰(zhàn)小貼士:Zuul統(tǒng)一異常處理(二)
- Spring Cloud實(shí)戰(zhàn)小貼士:Zuul統(tǒng)一異常處理(三)【Dalston版】
引用:
程序猿DD Spring Cloud基礎(chǔ)教程
Spring Cloud構(gòu)建微服務(wù)架構(gòu):服務(wù)網(wǎng)關(guān)(基礎(chǔ))【Dalston版】
Spring Cloud構(gòu)建微服務(wù)架構(gòu):服務(wù)網(wǎng)關(guān)(路由配置)【Dalston版】
Spring Cloud構(gòu)建微服務(wù)架構(gòu):服務(wù)網(wǎng)關(guān)(過濾器)【Dalston版】
Spring Cloud Dalston中文文檔