一、服務網關與Zuul
客戶端和服務端直接連接,存在一定的弊端。客戶端如果要請求不同的微服務,會增加客戶端的複雜性;存在跨域請求時,還需要額外的處理,並且UI端和服務端存在耦合。
服務網關,就是路由轉發加上過濾器。路由轉發是接收一切外部的請求,然後將其轉發到後端的微服務上;過濾器是在服務網關中實現橫切功能,例如權限校驗、限流以及監控等,都可以通過過濾器完成。
在技術層面上,用戶端口首先要經過負載均衡器,之後再經過API網關,然後連接微服務。服務網關和微服務啓動時都要註冊到註冊中心上,當用戶請求時直接對網關進行請求,網管會進行服務發現和複雜均衡,之後會經過權限校驗、監控、限流等操作,與服務的響應進行聚合,返回給用戶。
Zuul是Netflix開源的微服務網關,可以和Eureka、Ribbon、Hystrix配合使用,Spring Cloud對Zuul進行了整合和增強,主要功能是路由轉發和過濾器。Zuul的功能有
- 身份認證與安全:識別每個資源的驗證要求、拒絕那些與要求不符的請求
- 審查與監控:在邊緣位置追蹤有意義的數據和統計結果,從而帶來精確的生產視圖
- 動態路由:動態地將請求路由轉達到不同的集羣
- 壓力測試:逐漸增加指向集羣的流量
- 負載分配:爲每一種負載類型分配對應容量,並啓用超出限定值的請求
- 靜態響應處理:在邊緣位置直接建立部分響應、從而避免其轉發到內部集羣
- 多區域彈性:跨越AWS Region進行請求路由,,旨在實現Elastic Load Balancing使用的多樣化,以及讓系統的邊緣更貼近系統的使用者
二、Zuul環境搭建
首先我們新建一個SpringBoot應用,並在Maven中添加依賴
<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>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
之後我們來配置application.yml
spring:
application:
name: zuul-service
server:
port: 9000
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
最後在啓動類Application中添加註解@EnableZuulProxy即可開啓Zuul服務
三、Zuul路由
上面的例子可以將Zuul註冊到Eureka註冊中心,然而我們怎麼通過Zuul來配置路由呢?
Zuul路由有兩種模式,一種是不依賴服務發現,比如Nginx,另一種方式是通過微服務的以來發現,結合Eureka來進行路由轉發,我們要使用的是第二種。我們需要一組zuul.routes.<route>.path與zuul.routes.<route>.serviceId參數對的方式進行配置
# 自定義路由
zuul:
routes:
server-provide:
path: /server-api/**
servideId: server-provide
# 開啓Eureka支持
ribbon:
eureka:
enabled: true
例如
zuul:
routes:
api-a:
path: /api-a/**
servideId: server-ribbon
api-b:
path: /api-b/**
servideId: server-feign
這樣,以/api-a/開頭的請求都會轉發給service-ribbon服務,以/api-b/開頭的請求都轉發給service-feign服務
我們還可以使用URL來進行路由的轉發
zuul:
routes:
test:
path: /test/**
url: http://localhost:8081
四、Zuul過濾器
在進行路由轉發的時候,可能會涉及到一些公共操作,例如權限判斷或認證,如果沒有過濾器,就需要在每個微服務中單獨實現,比較麻煩。因此我們使用過濾器在每次路由之前對請求進行過濾。
過濾器有四個類型:
- PRE: 可以在請求被路由之前調用,我們可利用這種過濾器實現身份驗證、在集羣中選擇請求的微服務、記錄調試信息等,可實現日誌監控、身份認證、黑名單等功能
- ROUTING:在路由請求時候被調用。這種過濾器用於構建發送給微服務的請求,並使用Apache HttpClient或者Netflix Ribbon請求微服務
- POST:在routing和error過濾器之後被調用,這種過濾器可用來爲響應添加標準HTTP Header、手機統計信息和指標、將響應從微服務發送給客戶端,可實現審計、統計等功能
- ERROR:處理請求時發生錯誤調用,可實現統一異常處理等功能
Http請求會進入一個pre-filter,之後可以路由到自己的個性化路由器,否則會繼續到下一個routing-filter,在之後回到post-filter,最後會將應答返回客戶端。如果報錯了,會經過error-filters,再到post-filters,最終將應答返回。
要編寫一個過濾器,我們需要實現一個抽象類ZuulFilter並實現它的抽象函數,有以下四個
- String filterType():該函數需要返回一個字符串來代表過濾器的類型,而這個類型就是在HTTP請求過程中定義的各個階段。在Zuul中默認定義了四種不同生命週期的過濾器類型,有:pre、routing、post、error
- int filterOrder():通過int值來定義過濾器的執行順序,數值越小優先級越高
- boolean shouldFilter():返回一個boolean類型來判斷該過濾器是否要執行,我們可以通過此方法來指定過濾器的有效範圍
- Object run():過濾器的具體邏輯,在該函數中,我們可以實現自定義的過濾邏輯,來確定是否要攔截當前的請求,不對其進行後續的路由,或是在請求路由返回結果之後,對處理結果做一些加工等
下面我們新建一個過濾器
@Component
public class PreFilter extends ZuulFilter{
@Override
public String filterType(){
return "pre";
}
@Override
public int filterOrder(){
return 0;
}
@Override
public boolean shouldFilter(){
return true;
}
@Override
public Object run() throws ZuulException(){
System.out.println("PreFilter");
RequestContext ctx = RequestContext.getCurrentContext();
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody("Not Authenticated");
return null;
}
}
需要注意的是,記得要將自定義的過濾器添加@Component註解,添加到Spring容器中。在上面的代碼中,過濾器會攔截請求,並返回401碼,如果是要繼續轉發請求,則是下面的代碼
@Override
public Object run() throws ZuulException(){
RequestContext ctx = RequestContext.getCurrentContext();
ctx.setSendZuulResponse(true);
ctx.setResponseStatusCode(200);
return null;
}
這樣即可應答成功。而要判斷是否要正確應答,只需要根據一個簡單的判斷即可實現。
下面我們來實現這樣的代碼,如果有Token,則正確應答,否則因權限問題而轉發失敗
@Override
public Object run() throws ZuulException(){
RequestContext ctx = RequestContext.getCurrentContext();
Strint token = ctx.getRequest().getParameter("token");
if(token != null){
ctx.setSendZuulResponse(true);
ctx.setResponseStatusCode(200);
}else {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody("Not Authenticated");
}
return null;
}
此時如果我的URL爲http://localhost:9000/hello?token=123456即可正常訪問,如果URL中沒有token,則會因爲權限不足而轉發失敗
五、Zuul熔斷器
在前面我們講到Hystrix提供了異常熔斷的方法,Zuul也同樣提供了服務降級的方法。在Zuul中我們通過實現FallbackProvider接口即可
@Component
public class MyFallbackProvider implements FallbackProvider{
@Override
public String getRoute(){ return "*";}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause){
return new ClientHttpResponse(){
@Override
public HttpStatus getStatusCode() throws IOException{
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException{
return this.getStatusCode().value();
}
@Override
public String getStatusText() throws IOException{
return this.getStatusCode().getReasonPhrase();
}
@Override
public void close(){}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("Service不可用".getBytes());
}
@Override
public HttpHeaders getHeaders(){
HttpHeaders headers = new HttpHeaders();
MediaType mt = new MediaType("application", "json", Charset.forName("UTF-8"));
headers.setContentType(mt);
return headers;
}
};
}
}
六、Zuul實戰
下面我們根據一個實例來練習一下前面Zuul的內容。我們通過一個Token來確認用戶是否有權限訪問服務,要驗證用戶是否合法的,且操作是否已經失效了。
要實現這些內容,需要使用JWT。整個的操作流程是:
- 用戶打開客戶端以後,客戶端要求用戶給予授權
- 用戶同意給予客戶端授權
- 客戶端使用上一步獲得的授權,向認證服務器申請令牌
- 認證服務器對客戶端進行認證以後,確認無誤,同意發放令牌
- 客戶端使用令牌,向資源服務器申請獲取資源
- 資源服務器確認令牌無誤,同意向客戶端開放資源
Json Web Token (JWT)是一個非常輕巧的規範,允許我們使用JWT在用戶和服務器之間傳遞安全可靠的信息。它是基於RFC 7519標準定義的一種可以安全傳輸和自包含的JSON對象。HTTP的Post方法會用戶名和密碼,然後再服務器端會生成一個Token,並將Token返回給用戶。之後客戶端會發送受保護的API請求,並在Authorization Header中攜帶Token,服務器端會檢驗JWT是否合法,如果合法會取得相關信息,並返回請求的數據。
JWT有及部分進行組成:Header、Payload和Signature:Header中有聲明的加密算法(alg)和聲明類型(typ);Payload中包含了JWT的簽發者(iss)、JWT面向的用戶(sub)、接收JWT的一方(aud)、過期時間(exp)、什麼時候簽發的(iat)、最開始時間,如果當前時間在其之前,則不被接收(nbf)、最後還有簽名Signature,signature = 加密算法(header + “.” + payload,密鑰)
要使用JWT我們需要在Maven中配置
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
下面我們來編寫一個JWTUtil類來使用JWT
public class JwtUtil{
// 過期時間
private static final long EXPIRE_TIME = 15 * 60 * 1000;
// 私鑰
private static final String TOKEN_SECRET = "privateKey";
/**
* 生成簽名,15分鐘過期
*/
public static String sign(Long userId){
try{
// 設置過期時間
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
// 私鑰和加密算法
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
// 設置頭部消息
Map<String, Object> header = new HashMap<>(2);
header.put("Type","Jwt");
header.put("alg","HS256");
// 返回token字符串
return JWT.create()
.withHeader(header)
.withClaim("userId",userId)
.withExpiresAt(date)
.sign(algorithm);
}catch (Exception e){
e.printStackTrace();
return null;
}
}
}
/**
* 檢驗token是否正確
*/
public static Long verify(String token){
try{
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
Long userId = jwt.getClaim("userId").asLong();
return userId;
} catch(Exception e){
return 0L;
}
}
}
之後我們可以新建一個Controller,
@RestController
public class JwtController{
@RequestMapping("/getToken")
public Map getToken(){
// 前面傳用戶信息
Long userId = 111L;
// 返回令牌
Map token = new HashMap<>();
String sign = JwtUtil.sign(userId);
token.put("sign",sign);
token.put("userId",userId);
return token;
}
}
下面我們要實現,在網關的時候驗證是否sign是有效的,也就是要在過濾器中編寫run方法
@Override
public Object run() throws ZuulException(){
RequestContext ctx = RequestContext.getCurrentContext();
Strint token = ctx.getRequest().getParameter("token");
token = token == null?"":token;
String userId = ctx.getRequest().getParameter("userId");
long token_userid = JwtUtil.verify(token);
if(token_userid == Long.valueof(userId)){
ctx.setSendZuulResponse(true);
ctx.setResponseStatusCode(200);
}else {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody("Not Authenticated");
}
return null;
}
歡迎加入交流羣QQ1107710098