Spring Cloud Zuul 綜合使用
目前我們項目的架構圖:
從上圖中可以看到,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 綜合使用