1. 程式人生 > >Spring Cloud Zuul 綜合使用

Spring Cloud Zuul 綜合使用

autowire 獲取 完成 而在 openid 時間 接下來 ole github

Zuul:Pre和Post過濾器

目前我們項目的架構圖:
技術分享圖片

從上圖中可以看到,Zuul是我們整個系統的入口。當我們有參數校驗的需求時,我們就可以利用Zuul的Pre過濾器,進行參數的校驗。例如我現在希望請求都一律帶上token參數,否則拒絕請求。在項目中創建一個filter包,在該包中新建一個TokenFilter勞累並繼承ZuulFilter,代碼如下:

package org.zero.springcloud.apigateway.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;

/**
 * @program: api-gateway
 * @description: token過濾器
 * @author: 01
 * @create: 2018-08-25 17:03
 **/
@Component
public class TokenFilter extends ZuulFilter {

    @Override
    public String filterType() {
        // 聲明過濾器的類型為Pre
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        // 將這個過濾器的優先級放在 PRE_DECORATION_FILTER_ORDER 之前,數字越小優先級越高
        return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        // 開啟這個過濾器
        return true;
    }

    /**
     * 這個方法用於自定義過濾器的處理代碼
     *
     * @return Object
     * @throws ZuulException ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        // 從上下文中拿到請求對象
        HttpServletRequest request = requestContext.getRequest();

        // 拿出參數裏的token
        String token = request.getParameter("token");
        if (StringUtils.isEmpty(token)) {
            // 驗證失敗
            requestContext.setSendZuulResponse(false);
            // 返回401權限不通過
            requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
        }

        return null;
    }
}

重啟項目,我們來訪問一個接口,不帶上token參數,看看是否會返回401。如下:
技術分享圖片

帶上token參數再測試一下,請求成功:
技術分享圖片

從以上的示例中,可以看到利用Pre可以對請求進行一些預處理。如果希望在請求處理完成後,對返回的數據進行處理的話。就需要使用的Post過濾器,例如我們要在http返回頭中,加上一個自定義的X-Foo屬性。代碼如下:

package org.zero.springcloud.apigateway.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

/**
 * @program: api-gateway
 * @description:
 * @author: 01
 * @create: 2018-08-25 17:10
 **/
@Component
public class AddResponseHeaderFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }

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

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

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

        return null;
    }
}

重啟項目,同樣訪問之前那個接口,測試結果如下:
技術分享圖片


Zuul:限流

Zuul充當API網關的角色,所有的請求都經過它,所以很適合在其之上對API做限流保護,防止網絡×××。需要註意的是,用於限流的過濾器應該在請求被轉發之前調用,常見的限流算法有計數器、漏銅和令×××桶算法。

令×××桶算法示意圖:
技術分享圖片

Google開源工具包Guava提供了限流工具類RateLimiter,該類基於令×××桶算法(Token Bucket)來完成限流,非常易於使用。RateLimiter經常用於限制對一些物理資源或者邏輯資源的訪問速率,它支持兩種獲取permits接口,一種是如果拿不到立刻返回false,一種會阻塞等待一段時間看能不能拿到。

我們來創建一個過濾器,簡單使用一下這個RateLimiter。代碼如下:

package org.zero.springcloud.apigateway.filter;

import com.google.common.util.concurrent.RateLimiter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;
import org.zero.springcloud.apigateway.exception.RateLimiterException;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVLET_DETECTION_FILTER_ORDER;

/**
 * @program: api-gateway
 * @description: 限流過濾器
 * @author: 01
 * @create: 2018-08-25 21:04
 **/
@Component
public class RateLimiterFilter extends ZuulFilter {

    /**
     * 每秒鐘放入100個令×××
     */
    private static final RateLimiter RATE_LIMITER = RateLimiter.create(100);

    @Override
    public String filterType() {
        // 限流肯定是得在Pre類型的過濾器裏做
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        // 設置過濾器的優先級為最高
        return SERVLET_DETECTION_FILTER_ORDER - 1;
    }

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

    @Override
    public Object run() throws ZuulException {
        // 嘗試從令×××桶中獲取令×××
        if (!RATE_LIMITER.tryAcquire()) {
            // 獲取失敗拋出異常,或做其他處理
            throw new RateLimiterException();
        }

        return null;
    }
}

除了這個RateLimiter之外,GitHub上也有一些開源的實現。我這裏發現了一個還不錯的,地址如下:

https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit


Zuul:完成權限校驗

以上我們演示了pre、post過濾器的簡單使用,以及在Zuul上做限流,接下來我們看看如何通過Zuul實現鑒權。通常來說,我們鑒權的對象往往都是用戶,我這裏已經事先準備好了用戶服務以及相關接口。

需求,利用Zuul實現如下功能:

/**
 * /buyer/order/create 只能買家訪問 (cookie裏有openid)
 * /buyer/order/finish 只能賣家訪問 (cookie裏有token,並且redis存儲了session數據)
 * /buyer/product/list 都可以訪問
 */

因為判斷用戶角色權限的時候,需要通過cookie和redis裏緩存的數據進行判斷,所以修改配置文件如下:
技術分享圖片

將之前做實驗的所有過濾器都註釋掉,然後新建一個AuthBuyerFilter過濾器,用於攔截訂單創建的請求。代碼如下:

package org.zero.springcloud.apigateway.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.zero.springcloud.apigateway.utils.CookieUtil;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

/**
 * @program: api-gateway
 * @description: 買家權限過濾器
 * @author: 01
 * @create: 2018-08-25 17:03
 **/
@Component
public class AuthBuyerFilter extends ZuulFilter {

    private static final String ORDER_CREATE = "/order/buyer/order/create";

    @Override
    public String filterType() {
        // 聲明過濾器的類型為Pre
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        // 將這個過濾器的優先級放在 PRE_DECORATION_FILTER_ORDER 之前
        return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        // 從上下文中拿到請求對象
        HttpServletRequest request = requestContext.getRequest();

        // 如果訪問的是 ORDER_CREATE 則進行攔截,否則不進行攔截
        return ORDER_CREATE.equals(request.getRequestURI());
    }

    /**
     * 這個方法用於自定義過濾器的處理代碼
     *
     * @return Object
     * @throws ZuulException ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        // 從上下文中拿到請求對象
        HttpServletRequest request = requestContext.getRequest();

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

        return null;
    }
}

接著再新建一個AuthSellerFilter過濾器,用於攔截訂單完結的請求。代碼如下:

package org.zero.springcloud.apigateway.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.zero.springcloud.apigateway.constant.RedisConstant;
import org.zero.springcloud.apigateway.utils.CookieUtil;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

/**
 * @program: api-gateway
 * @description: 賣家權限過濾器
 * @author: 01
 * @create: 2018-08-25 17:03
 **/
@Component
public class AuthSellerFilter extends ZuulFilter {

    private final StringRedisTemplate redisTemplate;
    private static final String ORDER_FINISH = "/order/buyer/order/finish";

    @Autowired
    public AuthSellerFilter(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public String filterType() {
        // 聲明過濾器的類型為Pre
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        // 將這個過濾器的優先級放在 PRE_DECORATION_FILTER_ORDER 之前
        return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        // 從上下文中拿到請求對象
        HttpServletRequest request = requestContext.getRequest();

        // 如果訪問的是 ORDER_FINISH 則進行攔截,否則不進行攔截
        return ORDER_FINISH.equals(request.getRequestURI());
    }

    /**
     * 這個方法用於自定義過濾器的處理代碼
     *
     * @return Object
     * @throws ZuulException ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        // 從上下文中拿到請求對象
        HttpServletRequest request = requestContext.getRequest();

        // /buyer/order/finish 只能賣家訪問 (cookie裏有token,並且redis存儲了session數據)
        if (ORDER_FINISH.equals(request.getRequestURI())) {
            Cookie cookie = CookieUtil.get(request, "token");
            if (cookie == null ||
                    StringUtils.isBlank(cookie.getValue()) ||
                    StringUtils.isNotBlank(redisTemplate.opsForValue().get(String.format(RedisConstant.TOKEN_PREFIX, cookie.getValue())))) {
                requestContext.setSendZuulResponse(false);
                requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
            }
        }

        return null;
    }
}

額外話題:

  • 網關不要連接任何服務的關系型數據庫
  • 獲取數據應該通過調用服務接口的方式進行獲取
  • 經常需要獲取的數據有必要緩存到redis中,例如需要進行簡單的權限緩存

Zuul:跨域

現在我們的項目基本都是前後端分離的,前端通過ajax來請求後端接口。由於瀏覽器的同源策略,所以會出現跨域的問題。而在微服務架構中,我們可以在網關上統一解決跨域的問題。

在Zuul裏增加CorsFilter過濾器的配置類即可。代碼如下:

package org.zero.springcloud.apigateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 * @program: api-gateway
 * @description: 跨域配置
 * @author: 01
 * @create: 2018-08-27 23:02
 **/
@Configuration
public class CorsConfig {

    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 允許cookie跨域
        corsConfiguration.setAllowCredentials(true);
        // 允許任何域名使用
        corsConfiguration.addAllowedOrigin("*");
        // 允許任何頭
        corsConfiguration.addAllowedHeader("*");
        // 允許任何方法(post、get等)
        corsConfiguration.addAllowedMethod("*");
        // 設置跨域緩存時間,單位為秒
        corsConfiguration.setMaxAge(300L);

        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // 對接口配置跨域設置
        source.registerCorsConfiguration("/**", buildConfig());

        return new CorsFilter(source);
    }
}

Spring Cloud Zuul 綜合使用