在spring boot中整合微服務閘道器係統Spring Cloud Zuul

蘇小林發表於2019-05-08

spring cloud zuul由大名鼎鼎的netflix公司開發,已經超越spring cloud gateway微服務閘道器係統,成為了Spring Cloud全家桶裡排名第一的微服務閘道器係統了

閘道器作為所有應用系統的最前端,可以提供以下的價值

  1. 為後端微服務系統提供統一的入口
  2. 為後端微服務系統提供統一的授權機制
  3. 為後端微服務系統提供統一的認證機制
  4. 為後端微服務系統api提供統一簽名校驗機制
  5. 為流量入口新增日誌記錄
  6. qps統計
  7. 限流

完整程式碼已上傳GITHUB,參考:github.com/neatlife/my…

建立閘道器專案

可以在https://start.spring.io/建立新的spring boot專案作為閘道器的骨架,比如

在spring boot中整合微服務閘道器係統Spring Cloud Zuul

注意需要把Zuul閘道器元件新增進來,也可以手動新增,pom依賴如下:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
複製程式碼

代理後端服務

編輯Spring Boot main方法所在的主類,新增EnableZuulProxy註解,核心程式碼如下

@SpringBootApplication
@EnableZuulProxy
public class MyzuulApplication {

	public static void main(String[] args) {
		SpringApplication.run(MyzuulApplication.class, args);
	}

}
複製程式碼

然後在application.properties配置檔案中配置代理的後端服務,比如代理這個服務 jsonplaceholder.typicode.com/todos/

在spring boot中整合微服務閘道器係統Spring Cloud Zuul
這個服務可以直接訪問效果如下:jsonplaceholder.typicode.com/todos/3
在spring boot中整合微服務閘道器係統Spring Cloud Zuul
使用zuul代理訪問效果如下
在spring boot中整合微服務閘道器係統Spring Cloud Zuul
可以看到資料是完全一樣的,閘道器只是起到了一個代理的作用

可以結合apollo配置中心實現自動重新整理後端路由列表,參考: ZuulPropertiesRefresher.java

核心程式碼如下:

  @ApolloConfigChangeListener(interestedKeyPrefixes = "zuul.")
 public void onChange(ConfigChangeEvent changeEvent) {
   refreshZuulProperties(changeEvent);
 }

 private void refreshZuulProperties(ConfigChangeEvent changeEvent) {
   logger.info("Refreshing zuul properties!");

   /**
    * rebind configuration beans, e.g. ZuulProperties
    * @see org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#onApplicationEvent
    */
   this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));

   /**
    * refresh routes
    * @see org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration.ZuulRefreshListener#onApplicationEvent
    */
   this.applicationContext.publishEvent(new RoutesRefreshedEvent(routeLocator));

   logger.info("Zuul properties refreshed!");
 }
複製程式碼

過濾器系統

過濾器型別 過濾器執行時機 常見用法
PRE 在請求被路由之前呼叫 比如記錄請求引數,認證
ROUTING 將請求路由到後端 比如請求dubbo搭建的後端服務
POST 在請求被路由之後呼叫 比如記錄響應資料
ERROR 發生錯誤時執行該過濾器 比如給前端返回統一的報錯json格式

web 系統

這個閘道器係統本身也是一個完整spring boot專案 可以編寫控制器api,呼叫hibernate運算元據庫等 比如在閘道器裡編寫登陸功能實現統一的授權功能

授權與認證

授權一般在閘道器裡的控制器裡實現授權的邏輯,授權一般可以指代登陸操作 認證一般在閘道器裡的前置過濾器裡實現,一般是檢查是否登陸來決定是否允許訪問閘道器後面的服務

閘道器登陸介面編寫思路

流程程式碼如下

@PostMapping("/login")
public String login(@RequestParam(name = "username") String username,
                    @RequestParam(name = "password") String password
) {

    String token;

    // 到資料中檢查使用者名稱和密碼是否合法
    if ("admin".equals(username) && "admin".equals(password)) {
        // 生成token,儲存到redis中
        token = "21232f297a57a5a743894a0e4a801fc3";
    } else {
        throw new RuntimeException("使用者名稱或密碼錯誤");
    }

    // 返回token給前端,用來認證使用
    return token;
}

複製程式碼

認證流程

使用pre型別的過濾器 流程程式碼如下

/**
 * 認證
 */
@Slf4j
public class CertificationFilter extends ZuulFilter {

    @Override
    public Object run() {
        HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
        // 到redis中檢查token是否存在
        if  ("21232f297a57a5a743894a0e4a801fc3".equals(request.getParameter("token"))) {
            return null;
        }
        throw new ZuulRuntimeException(new ZuulException("未授權使用者禁止訪問", 403, "token校驗失敗"));
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }
}
複製程式碼

就是普通的登陸介面,不過是把這個邏輯放到了閘道器裡面

將請求的引數記錄到日誌

這樣做的目的是在異常時可以通過日誌找到請求引數 編寫一個過濾器,獲取請求引數並記錄到日誌中,核心程式碼如下

@Override
public Object run() {
    HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
    Map<String, Object> parameters = getParametersFromRequest(request);
    log.info("閘道器有新的訪問, url: {}, method: {}, parameters: {}", request.getRequestURL(), request.getMethod(), parameters);
    return null;
}

private Map<String, Object> getParametersFromRequest(HttpServletRequest request) {
    Enumeration<?> parameterNames = request.getParameterNames();
    Map<String, Object> parameters = new HashMap<>(16);
    while (parameterNames.hasMoreElements()) {
        String pName = (String) parameterNames.nextElement();
        Object pValue = request.getParameter(pName);
        parameters.put(pName, pValue);
    }
    return parameters;
}
複製程式碼

完整程式碼參考:

統一錯誤返回格式

使用error過濾器可以實現 核心程式碼如下

@Slf4j
public class CustomErrorFilter extends ZuulFilter {

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        // Remove error code to prevent further error handling in follow up filters
        ctx.remove("error.status_code");
        // block the SendErrorFilter from running
        ctx.set("sendErrorFilter.ran");

        ctx.setSendZuulResponse(false);
        ctx.getResponse().setContentType("application/json");
        ctx.getResponse().setCharacterEncoding("utf-8");
        ctx.getResponse().setHeader("Access-Control-Allow-Origin", "*");
        ctx.getResponse().setHeader("Access-Control-Allow-Methods", "*");
        ctx.getResponse().setHeader("Access-Control-Allow-Age", "86400");
        ctx.getResponse().setHeader("Access-Control-Allow-Headers", "*");
        ctx.setResponseStatusCode(200);
        StringWriter sw = new StringWriter();
        ctx.getThrowable().printStackTrace(new PrintWriter(sw, true));
        try {
            ZuulException zuulException = (ZuulException) ctx.getThrowable().getCause().getCause();

            ctx.getResponse().getWriter().write(
            "{\"message\": \"" +
                    zuulException.getMessage() +
                    "\", \"code\" :" +
                    zuulException.nStatusCode
                    + "}"
            );
        } catch (Exception e) {
            log.error("寫入異常到客戶端異常, estring: {}", e.toString());
        }
        return null;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public int filterOrder() {
        return -1;
    }

    @Override
    public String filterType() {
        return FilterConstants.ERROR_TYPE;
    }

}
複製程式碼

注意需要加上以下設定

// 阻止別的錯誤處理過濾器再次對錯誤進行處理
ctx.remove("error.status_code");
// 阻止別的傳送錯誤的過濾器對錯誤響應結果再次進行處理
ctx.set("sendErrorFilter.ran");
複製程式碼

在瀏覽器中檢視效果

在spring boot中整合微服務閘道器係統Spring Cloud Zuul

zuul設定與調優

常用的調優引數如下:

連線池最大連線,預設是200 zuul.host.maxTotalConnections=1000

每個route可用的最大連線數,預設值是20 zuul.host.maxPerRouteConnections=1000

Hystrix最大的併發請求 預設值是100 zuul.semaphore.maxSemaphores=1000

hystrix熔斷設定與調優

閘道器屬於整個系統最前端的應用,同時又屬於基礎服務,和redis, mysql等基礎服務一樣,一般是不允許當機的,可用性應該得到保證,保證可用性常用的技術就是使用降級熔斷技術hystrix了

Hystrix 超時時間配置 配置預設的hystrix超時時間

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000
複製程式碼

配置特定方法的超時時間

hystrix.command.<hystrixcommandkey>.execution.isolation.thread.timeoutInMilliseconds=10000
複製程式碼

的format為FeignClassName#methodSignature,下面是示例配置

hystrix.command.PressureService#getBalance(int).execution.isolation.thread.timeoutInMilliseconds=10000
複製程式碼

一些注意的點

可以在前置過濾器中校驗介面的簽名,參考:簡單API介面簽名驗證

閘道器裡面登陸的使用者需要和其它服務共享使用者的登陸資訊,可以把使用者的資訊存放到redis中進行共享

如果在開發過程中遇到問題,可加作者微信探討

wx

相關文章