快速入門全棧 - 14 SpringCloud 服務網關

一、服務網關與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。整個的操作流程是:

  1. 用戶打開客戶端以後,客戶端要求用戶給予授權
  2. 用戶同意給予客戶端授權
  3. 客戶端使用上一步獲得的授權,向認證服務器申請令牌
  4. 認證服務器對客戶端進行認證以後,確認無誤,同意發放令牌
  5. 客戶端使用令牌,向資源服務器申請獲取資源
  6. 資源服務器確認令牌無誤,同意向客戶端開放資源

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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章