總結之SpringCloud之路由網關——Zuul

Zuul是Spring Cloud全家桶中的微服務API網關。

所有從設備或網站來的請求都會經過Zuul到達後端的Netflix應用程序。作爲一個邊界性質的應用程序,Zuul提供了動態路由、監控、彈性負載和安全功能。Zuul底層利用各種filter實現如下功能:
•認證和安全 識別每個需要認證的資源,拒絕不符合要求的請求。
•性能監測 在服務邊界追蹤並統計數據,提供精確的生產視圖。
•動態路由 根據需要將請求動態路由到後端集羣。
•壓力測試 逐漸增加對集羣的流量以瞭解其性能。
•負載卸載 預先爲每種類型的請求分配容量,當請求超過容量時自動丟棄。
•靜態資源處理 直接在邊界返回某些響應。

zuul入門

新建一個服務zuul-service作爲路由網關服務。
pom.xml中引入依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>

使用@EnableZuulProxy啓用Zuul(使用@EnableZuulServer也可以啓用Zuul,只是不會自動從Eureka中獲取並自動代理服務,也不會自動加載部分Zuul過濾器,但是可以選擇性地替換代理平臺的各個部分)。

@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulApplicationStarter {

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

路由配置

server:
  port: 9000
spring:
  application:
    name: zuul Service
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8771/eureka/
    fetch-registry: false
zuul:
  strip-prefix: false
  routes:
    users: #你自定義規則名字
      path: /user/** #url地址
      serviceId: one #路由到serviceid
  ignored-patterns: /user/one #url攔截 防止外面通過url請求內部通信路徑
  #簡便寫法
    #one: /user/**
ribbon:
  eureka:
    enabled: false
one:
  ribbon:
    listOfServers: http://localhost:8773
management:
  security:
    enabled: false

相關參數
Zuul會自動讀取註冊中心的已經註冊的服務。user-service服務會自動設置/user-service/**這樣的路由,即/user-service/users會被代理到user-service服務的/users請求。
zuul.ignoredServices可以指定忽略註冊中心獲取的服務
zuul.routes.=路由key使用一個服務名稱,對應一個路由路徑
zuul.routes.<key>.serviceId=<serviceId>指定一個服務對應路由路徑爲zuul.routes.<key>.path
zuul.routes.<key>.url=<url>指定一個服務的url或者使用forward轉向Zuul服務的接口,對應路由路徑爲zuul.routes.<key>.path
zuul.routes.<ribbon>=<path>使用自定義Ribbon實現路由

Zuul服務啓動完成後,可以訪問http://localhost:9000/routes獲取路由列表

動態路由

Zuul結合SpringCloud配置中心,在修改路由配置信息後刷新配置可立即生效,無需重啓Zuul服務,這樣就實現了動態路由。

Zuul Filter

Zuul進行代理時,會有一系列的Zuul Filter對Http請求的request和response進行封裝和操作。
一個Zuul Filter有下面四個要素:

Type:類型。Zuul Filter的類型包括pre,routing,post和error。routing過濾器是在路由階段執行的,負責尋找原服務、請求轉發和返回接收。pre和post分別在routing之前和之後執行。如果Zuul執行代理的過程中拋出ZuulException異常,則會被error過濾器捕獲並進行相應處理。
在這裏插入圖片描述
Execution Order:執行順序。通過一個整型的值從小到大依次執行(相同類型過濾器間互相比較)。
Criteria:執行條件。當滿足一定條件時,纔會執行該過濾器。
Action:執行動作。當執行條件滿足時,進行的操作。
實現一個過濾器只要繼承ZuulFilter,並實現filterType(),filterOrder(),shouldFilter()和run()四個方法。這些方法與上面的四個要素對應。
如果要禁用一個Zuul過濾器,只需要配置zuul.<SimpleClassName>.<filterType>.disable=true,比如需要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter需要配置zuul.SendResponseFilter.post.disable=true
下面我們使用一個pre過濾器實現token驗證,如果Http header裏面沒有一個固定的token,則禁止訪問。
禁用Zuul默認的error過濾器,設置固定的token和需要驗證的路由key名單

zuul:
 # 禁用SpringCloud自帶的error filter
 SendErrorFilter:
   error:
     disable: true

zuul-filter:
 token-filter:
   # 訪問時,需要進行認證的路由key
   un-auth-routes:
     - users
     - smsApi
   # 固定的token
   static-token: xF2fdi8M

讀取自定義token配置信息

@Component
@ConfigurationProperties("zuulFilter.tokenFilter")
public class TokenValidateConfiguration {

    // 在這個列表裏面存儲的routeId都是需要使用TokenValidateFilter過濾的
    private List<String> unAuthRoutes;

    // 給定的token
    private String staticToken;

    public List<String> getUnAuthRoutes() {
        return unAuthRoutes;
    }

    public void setUnAuthRoutes(List<String> unAuthRoutes) {
        this.unAuthRoutes = unAuthRoutes;
    }

    public String getStaticToken() {
        return staticToken;
    }

    public void setStaticToken(String staticToken) {
        this.staticToken = staticToken;
    }
}

自定義過濾器

@Component
public class TokenValidateFilter extends ZuulFilter {

    protected static final Logger logger = LoggerFactory.getLogger(TokenValidateFilter.class);

    @Autowired
    private TokenValidateConfiguration tvConfig;

    @Override
    public String filterType() {//類型
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {//執行順序
        return FilterConstants.PRE_DECORATION_FILTER_ORDER;
    }

    @Override
    public boolean shouldFilter() {//是否攔截
        RequestContext ctx = RequestContext.getCurrentContext();
        return tvConfig.getUnAuthRoutes().contains(ctx.get(FilterConstants.PROXY_KEY));
    }

    @Override
    public Object run() {//攔截後操作
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        String token = request.getHeader("Authorization");
        if (token == null) {
            logger.warn("Http Header Authorization is null");
            forbidden();
            return null;
        }

        String staticToken = tvConfig.getStaticToken();
        if (StringUtils.isBlank(staticToken)) {
            logger.warn("property zuulFilter.tokenFilter.staticToken was not set");
            forbidden();
        } else if (!staticToken.equals(token)) {
            logger.warn("token is not valid");
            forbidden();
        }
        return null;
    }

    /**
     * 設置response的狀態碼爲403
     */
    private void forbidden() {
        // zuul中,將請求附帶的信息存在線程變量中。
        RequestContext.getCurrentContext().setResponseStatusCode(HttpStatus.FORBIDDEN.value());
        ReflectionUtils.rethrowRuntimeException(new ZuulException("token is not valid", HttpStatus.FORBIDDEN.value(),
                "token校驗不通過"));
    }
} 

注意:如果使用zuul.routes.=方式配置的路由,則ctx.get(FilterConstants.PROXY_KEY)會得到去掉頭尾的url(/smsApi/**會得到smsApi,/smsApi/target/**會得到smsApi/target),而並非路由key。所以之前配置文件中的路由

測試:攜帶正確的token訪問成功

新建一個error過濾器,當捕獲到ZuulException時,返回一個JSON對象

@Component
public class SendErrorRestFilter extends ZuulFilter {

    private static final Logger logger = LoggerFactory.getLogger(SendErrorRestFilter.class);

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

    @Override
    public int filterOrder() {
        return FilterConstants.SEND_ERROR_FILTER_ORDER;
    }

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

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        Throwable throwable = getCause(context.getThrowable());
        // 獲取response狀態碼
        int status = context.getResponseStatusCode();
        JSONObject info = new JSONObject();
        info.put("code", "異常碼" + status);
        info.put("message", throwable.getMessage());
        // 記錄日誌
        logger.warn("請求異常,被error filter攔截", context.getClass());

        // 設置response
        context.setResponseBody(info.toJSONString());
        context.getResponse().setContentType("application/json;charset=UTF-8");
        context.getResponse().setStatus(HttpStatus.OK.value());

        // 處理了異常之後清空異常
        context.remove("throwable");
        return null;
    }

    private Throwable getCause(Throwable throwable) {
        while (throwable.getCause() != null) {
            throwable = throwable.getCause();
        }
        return throwable;
    }
}

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