Spring Cloud -- 4 服務閘道器
我們使用Spring Cloud Netflix中的Eureka實現了服務註冊中心以及服務註冊與發現;而服務間通過Ribbon或Feign實現服務的消費以及均衡負載;通過Spring Cloud Config實現了應用多環境的外部化配置以及版本管理。為了使得服務叢集更為健壯,使用Hystrix的融斷機制來避免在微服務架構中個別服務出現異常時引起的故障蔓延。
在該架構中,我們的服務叢集包含:內部服務Service A和Service B,他們都會註冊與訂閱服務至Eureka Server,而Open Service是一個對外的服務,通過均衡負載公開至服務呼叫方。本文我們把焦點聚集在對外服務這塊,這樣的實現是否合理,或者是否有更好的實現方式呢?
先來說說這樣架構需要做的一些事兒以及存在的不足:
- 首先,破壞了服務無狀態特點。為了保證對外服務的安全性,我們需要實現對服務訪問的許可權控制,而開放服務的許可權控制機制將會貫穿並汙染整個開放服務的業務邏輯,這會帶來的最直接問題是,破壞了服務叢集中REST API無狀態的特點。從具體開發和測試的角度來說,在工作中除了要考慮實際的業務邏輯之外,還需要額外可續對介面訪問的控制處理。
- 其次,無法直接複用既有介面。當我們需要對一個即有的叢集內訪問介面,實現外部服務訪問時,我們不得不通過在原有介面上增加校驗邏輯,或增加一個代理呼叫來實現許可權控制,無法直接複用原有的介面。
面對類似上面的問題,我們要如何解決呢?下面進入本文的正題:服務閘道器!
為了解決上面這些問題,我們需要將許可權控制這樣的東西從我們的服務單元中抽離出去,而最適合這些邏輯的地方就是處於對外訪問最前端的地方,我們需要一個更強大一些的均衡負載器,它就是本文將來介紹的:服務閘道器。
服務閘道器是微服務架構中一個不可或缺的部分。通過服務閘道器統一向外系統提供REST API的過程中,除了具備服務路由、均衡負載功能之外,它還具備了許可權控制等功能。Spring Cloud Netflix中的Zuul就擔任了這樣的一個角色,為微服務架構提供了前門保護的作用,同時將許可權控制這些較重的非業務邏輯內容遷移到服務路由層面,使得服務叢集主體能夠具備更高的可複用性和可測試性。
準備工作
我們會看到:
然後開始寫我們的服務閘道器:
新建一個springBoot專案,pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.sun</groupId> <artifactId>api-gateway</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <name>api-gateway</name> <description>Spring Cloud project</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.3.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Brixton.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> |
啟動類上加上@EnableZuulProxy
註解開啟Zuul,
並且把我們的服務過濾器載入進來,後期我們要用過濾器來攔截進行業務操作。
application.properties:
配置檔案中除了基本的配置外,需要將我們的服務註冊到註冊中心去發現其他服務,這樣我們就可以我們就可以實現對serviceId的對映,如果是通過指定serviceId的方式,eureka依賴是必須要的。我們也可以用url的方式去對映,但這樣不夠透明友好。
該配置定義的規則是:當我們訪問http://localhost:5555/api-a/add?a=1&b=2
的時候,Zuul會將該請求路由到:http://localhost:2222/add?a=1&b=2
上。當我們訪問http://localhost:5555/api-b/add?a=1&b=2
的時候,Zuul會將該請求路由到:http://localhost:3333/add?a=1&b=2
上。
服務過濾器:
過濾器只需要繼承ZuulFilter實現其四個抽象函式就可以對所有請求進行攔截和過濾,書寫業務邏輯。
如下我們攔截檢查請求中是否有accesstoken,若有就進行路由,若沒有就拒絕訪問,返回401錯誤。
package com.didispace.filter; import javax.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; public class AccessFilter extends ZuulFilter { private static Logger log = LoggerFactory.getLogger(AccessFilter.class); /*** * filterType:返回一個字串代表過濾器的型別,在zuul中定義了四種不同生命週期的過濾器型別,具體如下: * pre:可以在請求被路由之前呼叫 * routing:在路由請求時候被呼叫 * post:在routing和error過濾器之後被呼叫 * error:處理請求時發生錯誤時被呼叫 */ @Override public String filterType() { return "pre"; } //通過int值來定義過濾器的執行順序 @Override public int filterOrder() { return 0; } //返回一個boolean型別來判斷該過濾器是否要執行,所以通過此函式可實現過濾器的開關.true為開false為關。 @Override public boolean shouldFilter() { return true; } /*** * 過濾器的具體邏輯。需要注意,這裡我們通過ctx.setSendZuulResponse(false)令zuul過濾該請求, * 不對其進行路由,然後通過ctx.setResponseStatusCode(401)設定了其返回的錯誤碼,當然我們也可以 * 進一步優化我們的返回,比如,通過ctx.setResponseBody(body)對返回body內容進行編輯等。 */ @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); log.info(String.format("%s request to %s", 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); log.info("省缺accessToken已攔截"); return null; } log.info("access token ok"); return null; } } |
啟動服務後我們訪問
http://localhost:5555/api-a/add?a=1&b=2:返回401錯誤;
訪問http://localhost:5555/api-a/add?a=1&b=2&accessToken=token:正確路由到server-A,並返回計算內容
還有很多其他的過濾型別就不一一展開了。
filterType
生命週期介紹:
Springcloud核心元件:
參考自:碼雲程式猿DD。感謝他的分享!