SpringCloud之Zuul

1.為什么需要網(wǎng)關(guān)

??想象這么一個場景,如果我們有成千上萬的微服務(wù),客戶端如果一個一個的去連接,顯然這么做是不現(xiàn)實(shí)的,這個時候網(wǎng)關(guān)就出現(xiàn)了~如下圖右邊所示,所有的請求都必須要經(jīng)過網(wǎng)關(guān),然后由網(wǎng)關(guān)將請求路由到其他的微服務(wù)。


網(wǎng)關(guān)的重要性

??既然網(wǎng)關(guān)作為所有請求的入口,那么它必須具備哪些要素呢?

2.網(wǎng)關(guān)的要素

??2.1.穩(wěn)定性,高可用

??服務(wù)網(wǎng)關(guān)作為請求的入口,它必須7*24小時可用,網(wǎng)關(guān)癱瘓,系統(tǒng)全掛,所以穩(wěn)定性和高可用性是網(wǎng)關(guān)的第一也是最重要的要素

??2.2.性能、并發(fā)性

??所有的請求都經(jīng)過網(wǎng)關(guān),所以壓力非常巨大,并發(fā)要求極高

??2.3.安全性

??防止外部的惡意訪問!

??2.4.擴(kuò)展性

??理論上網(wǎng)關(guān)適合處理所有的非業(yè)務(wù)場景處理,例如:協(xié)議轉(zhuǎn)發(fā),防刷、流量監(jiān)控、日志

3.常用的網(wǎng)關(guān)方案

??3.1Nginx + lua

??Nginx適合高并發(fā)的處理

??3.2Kong

??這款軟件是基于Nginx+Lua的,而且比Nginx的配置更簡單,但是這款軟件是要收費(fèi)的

??3.3Tyk

??開源的輕量級網(wǎng)關(guān),這是go語言開發(fā)的

??3.4Spring cloud zuul

??本篇文章的主角!雖然是主角,但是它的性能還是無法和Nginx比,但是據(jù)說zuul2.0提升了性能,我們用SpringCloud,用zuul會更加方便和簡單。

4.Zuul

??Zuul的本質(zhì)就是:路由+過濾器=Zuul,它的核心就是一系列的過濾器。Zuul提供了四種過濾器API他們是:
??前置(Pre)
??路由(Route)
??后置(Post)
??錯誤(Error)
??Zuul的架構(gòu)圖如下所示,我們可以看到過濾器之間沒有直接通信的,他們之間有一個RequestContext上下文。


Zuul的架構(gòu)圖

我們接下來看一下請求的生命周期:請求首先到達(dá)的是前置過濾器,對請求路由前的前置加工,比如對參數(shù)校驗我們就放在這個filter中。routing Filter就是將請求路由到Origin Server中去,當(dāng)請求處理完后會由post filter做處理。圖的右下角還有一個error filter,當(dāng)三種過濾器發(fā)生錯誤出現(xiàn)異常后,就會到達(dá)這個error filter。左下角還有一個custom filter,這個filter其實(shí)可以放到Prefilter這里,也可以放到post filter。


請求生命周期

4.1代碼與實(shí)戰(zhàn)

4.1.1pom.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.imooc</groupId>
    <artifactId>api-gateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>api-gateway</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Finchley.SR2</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</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-netflix-zuul</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</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>

4.1.2

main函數(shù)

@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
}

4.1.3關(guān)于路由的轉(zhuǎn)發(fā)排除和自定義

zuul默認(rèn)的路由就是:服務(wù)名/真正的訪問路徑
在yaml配置文件中的相關(guān)配置項

zuul: 
  routes:
  # /myProduct/product/list -> /product/product/list
    aaaaaa:
      path: /myProduct/**
      serviceId: product
      sensitiveHeaders:
  #簡潔寫法
#    product: /myProduct/**
  #排除某些路由
  ignored-patterns:
    - /**/product/listForOrder 
management:
  security:
    enabled: false  

4.1.4Cookie

當(dāng)zuul進(jìn)行路由轉(zhuǎn)發(fā)請求的時候,cookie默認(rèn)是不轉(zhuǎn)發(fā)的,所以需要配置sensitiveHeaders這個配置項。為什么就說要改這個配置項就能成功呢?我們可以通過源碼來分析,在zuulProperties源碼中,我們可以看到private Set<String> sensitiveHeaders = new LinkedHashSet(Arrays.asList("Cookie", "Set-Cookie", "Authorization"));這么一行代碼,也就是說請求頭中的這些信息,zuul是會全部過濾掉的。

zuul:
  #全部服務(wù)忽略敏感頭(全部服務(wù)都可以傳遞cookie)
  sensitive-headers: 
  routes:
#aaaa的名字可以隨便寫,但是path不要亂寫
    aaaaaa:
      path: /myProduct/**
      serviceId: product
      sensitiveHeaders:

4.1.5動態(tài)路由

動態(tài)路由需要兩個配置來配合,一個就是我上一篇文章寫的config,還有一個就是我們需要一個配置項:

@Component
public class ZuulConfig {

    @ConfigurationProperties("zuul")
    @RefreshScope
    public ZuulProperties zuulProperties() {
        return new ZuulProperties();
    }
}

??過濾器典型應(yīng)用場景有以下,對于前置過濾有限流、鑒權(quán)、參數(shù)校驗調(diào)整。對于后置過濾器可以做統(tǒng)計與日志記錄。

5.Zuul的綜合使用

先看一個圖:


系統(tǒng)架構(gòu)圖

5.1過濾器

??我們先通過Zuul的PreFilter和PostFilter實(shí)現(xiàn)兩個功能:如果用戶在訪問后臺微服務(wù)時沒有提供token值,我們則拒絕訪問。第二個功能時,當(dāng)我們處理完請求快要返回應(yīng)答時,在response中請求頭中放入一個信息。
我們先看看Prefilter的實(shí)現(xiàn):

@Component
public class TokenFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

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

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

    @Override
    public Object run() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        //這里從url參數(shù)里獲取, 也可以從cookie, header里獲取
        String token = request.getParameter("token");
        if (StringUtils.isEmpty(token)) {
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
        }
        return null;
    }
}

??注意代碼里面的PRE_DECORATION_FILTER_ORDER,這個ORDER是Zuul原生提供的,也是我們在自定義Filter時,官方比較建議使用的一個ORDER。在接下來看看RUN方法,我們看到里面使用了RequestContext來獲取request對象,并從request中獲取token值,如果token為null,我們就會設(shè)置ZuulResponse為false,同時設(shè)置應(yīng)答碼為401。
??接下來我們看看PostFilter的代碼:

@Component
public class addResponseHeaderFilter extends ZuulFilter{
    @Override
    public String filterType() {
        return POST_TYPE;
    }

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

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

    @Override
    public Object run() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletResponse response = requestContext.getResponse();
        response.setHeader("X-Foo", UUID.randomUUID().toString());
        return null;
    }
}

??我們在請求頭中放入了X-Foo的屬性。在filterOrder方法里面我們使用了另外一個官方比較推薦的Order。

5.2限流

??限流應(yīng)該在請求被轉(zhuǎn)發(fā)之前調(diào)用,而且可以說應(yīng)該是所用調(diào)用中最前端的邏輯。常用的限流算法就是令牌桶算法:


令牌桶算法

??就是說我們往一個桶里面按照一定的速率放令牌,如果桶滿了,就把多余的令牌丟掉,當(dāng)請求到達(dá)系統(tǒng)的時候,如果能夠從桶中獲得令牌,就讓請求被執(zhí)行,同時同種減少一個令牌,如果桶中沒有令牌了,那么就拒絕請求。我們看一下在Zuul中如何實(shí)現(xiàn)。

@Component
public class RateLimitFilter extends ZuulFilter{

    private static final RateLimiter RATE_LIMITER = RateLimiter.create(100);

    /**
     * to classify a filter by type. Standard types in Zuul are "pre" for pre-routing filtering,
     * "route" for routing to an origin, "post" for post-routing filters, "error" for error handling.
     * We also support a "static" type for static responses see  StaticResponseFilter.
     * Any filterType made be created or added and run by calling FilterProcessor.runFilters(type)
     *
     * @return A String representing that type
     */
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    /**
     * filterOrder() must also be defined for a filter. Filters may have the same  filterOrder if precedence is not
     * important for a filter. filterOrders do not need to be sequential.
     *
     * @return the int order of a filter
     */
    @Override
    public int filterOrder() {
        return SERVLET_DETECTION_FILTER_ORDER - 1;
    }

    /**
     * a "true" return from this method means that the run() method should be invoked
     *
     * @return true if the run() method should be invoked. false will not invoke the run() method
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * if shouldFilter() is true, this method will be invoked. this method is the core method of a ZuulFilter
     *
     * @return Some arbitrary artifact may be returned. Current implementation ignores it.
     */
    @Override
    public Object run() {
        if (!RATE_LIMITER.tryAcquire()) {
            throw new RateLimitException();
        }

        return null;
    }
}

??這里我們直接使用了google的令牌桶算法,當(dāng)然感興趣的同學(xué)可以試試網(wǎng)上配合redis等數(shù)據(jù)庫來完成限流的方案,這里我們就使用谷歌提供的算法。我們的filterOrder使用了最高優(yōu)先級的order,因為限流必須要在最前面使用。同時我們每秒鐘往桶里面放100個令牌,每次來一個請求,我們就拿走一個令牌。

5.3Zuul鑒權(quán)和添加用戶服務(wù)

??我們假設(shè)這樣一個場景,對于/order/create請求只能買家訪問,/order/finish 只能賣家訪問,/product/list 都可訪問,如何讓Zuul來做這樣的鑒權(quán)?也就是讓zuul判斷登錄用戶究竟是買家還是賣家,我們可以看看下面的AuthFilter:

@Component
public class AuthFilter extends ZuulFilter {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

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

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

    @Override
    public Object run() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        /**
         * /order/create 只能買家訪問(cookie里有openid)
         * /order/finish 只能賣家訪問(cookie里有token, 并且對應(yīng)的redis中值)
         * /product/list 都可訪問
         */
        if ("/order/order/create".equals(request.getRequestURI())) {
            Cookie cookie = CookieUtil.get(request, "openid");
            if (cookie == null || StringUtils.isEmpty(cookie.getValue())) {
                requestContext.setSendZuulResponse(false);
                requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
            }
        }

        if ("/order/order/finish".equals(request.getRequestURI())) {
            Cookie cookie = CookieUtil.get(request, "token");
            if (cookie == null
                    || StringUtils.isEmpty(cookie.getValue())
                    || StringUtils.isEmpty(stringRedisTemplate.opsForValue().get(String.format(RedisConstant.TOKEN_TEMPLATE, cookie.getValue())))) {
                requestContext.setSendZuulResponse(false);
                requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
            }
        }

        return null;
    }
}

??在run方法邏輯中我們判斷了用戶的請求路徑,做了相應(yīng)的鑒權(quán),但是我們可以看到這其中存在問題,如果以后我們要對買家的邏輯或者賣家的邏輯做更改,怎么辦?都在這一個Filter里面修改嗎?不現(xiàn)實(shí),所以我們決定將這個Filter拆分。
??對于賣家的Filter:

@Component
public class AuthSellerFilter extends ZuulFilter {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

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

    @Override
    public boolean shouldFilter() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        if ("/order/order/finish".equals(request.getRequestURI())) {
            return true;
        }

        return false;
    }

    @Override
    public Object run() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        /**
         * /order/finish 只能賣家訪問(cookie里有token, 并且對應(yīng)的redis中值)
         */

        Cookie cookie = CookieUtil.get(request, "token");
        if (cookie == null
                || StringUtils.isEmpty(cookie.getValue())
                || StringUtils.isEmpty(stringRedisTemplate.opsForValue().get(String.format(RedisConstant.TOKEN_TEMPLATE, cookie.getValue())))) {
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
        }

        return null;
    }
}

??我們使用了shouldFilter對路徑先做判斷,如果返回true再執(zhí)行run方法。再來看看買家的Filter邏輯:

@Component
public class AuthBuyerFilter extends ZuulFilter {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

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

    @Override
    public boolean shouldFilter() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        if ("/order/order/create".equals(request.getRequestURI())) {
            return true;
        }

        return false;
    }

    @Override
    public Object run() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        /**
         * /order/create 只能買家訪問(cookie里有openid)
         */
        Cookie cookie = CookieUtil.get(request, "openid");
        if (cookie == null || StringUtils.isEmpty(cookie.getValue())) {
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
        }

        return null;
    }
}

??這樣我們就將鑒權(quán)的Filter拆分的更細(xì)更加合理

5.4跨域

??當(dāng)我們做前后端分離的時候,我們知道,發(fā)起ajax請求如果不是同源的話會引起跨域問題。那么我們的Zuul在解決跨域問題的時候,其實(shí)也就是在解決Spring的跨域問題,有的同學(xué)認(rèn)為我們可以在被調(diào)用的類或者方法上增加@CrossOrigin注解來解決,但是我們的類和方法成千上萬,這樣做會大大增加成本。這里我們使用另外一個方法,也就是在Zuul里面增加CorsFilter過濾器。一個簡單的配置類就可以搞定

@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();

        config.setAllowCredentials(true);//允許coockie跨域
        config.setAllowedOrigins(Arrays.asList("*")); //http:www.a.com
        config.setAllowedHeaders(Arrays.asList("*"));//允許的請求頭
        config.setAllowedMethods(Arrays.asList("*"));//允許的請求方法
        config.setMaxAge(300l);//多長時間內(nèi)不用再檢查是否跨域

        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

??關(guān)于Zuul的知識我們就講解到這里,但是這里我還是要寫一段相對比較悲觀的話,Zuul和Eureka一樣也是不會再被維護(hù)的范疇了,后續(xù)的文章中我們將會講解Spring-cloud-Gateway,它是目前為止Zuul的完美替代

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容