目前我們所有用戶端請求都是通過微服務網關轉發完成的,但是還是可以直接通路微服務位址的方式來擷取服務,我們使用PostMan分别向
- localhost:8301/system/hello(網關)
- localhost:8202/hello(非網關)
發送GET請求,都能得到響應結果。
localhost:8301/system/hello(網關)
localhost:8202/hello(非網關)
為了避免用戶端請求繞過網關,直接調用微服務,我們可以在網關轉發請求至微服務前和微服務被調用前之間做一些必要的過濾和攔截處理。
自定義Zuul過濾器
解決這個問題的原理:
- 在網關轉發請求前,請求頭部加入網關資訊
- 在處理請求的微服務子產品裡定義全局攔截器,校驗請求頭部的網關資訊
這樣就能避免用戶端直接通路微服務了。
在自定義Zuul過濾器前,我們先來簡單了解下Zuul的核心過濾器。Zuul中預設定義了4種不同生命周期的過濾器類型,如下圖所示:
這4種過濾器處于不同的生命周期,是以其職責也各不相同:
PRE:PRE過濾器用于将請求路徑與配置的路由規則進行比對,以找到需要轉發的目标位址,并做一些前置加工,比如請求的校驗等;
ROUTING:ROUTING過濾器用于将外部請求轉發到具體服務執行個體上去;
POST:POST過濾器用于将微服務的響應資訊傳回到用戶端,這個過程種可以對傳回資料進行加工處理;
ERROR:上述的過程發生異常後将調用ERROR過濾器。ERROR過濾器捕獲到異常後需要将異常資訊傳回給用戶端,是以最終還是會調用POST過濾器。
Spring Cloud Zuul為各個生命周期階段實作了一批過濾器,如下所示:
這些過濾器的優先級和作用如下表所示:
生命周期 優先級 過濾器 描述
pre -3 ServletDetectionFilter 标記處理Servlet的類型
pre -2 Servlet30WrapperFilter 包裝HttpServletRequest請求
pre -1 FormBodyWrapperFilter 包裝請求體
route 1 DebugFilter 标記調試标志
route 5 PreDecorationFilter 處理請求上下文供後續使用
route 10 RibbonRoutingFilte serviceId請求轉發
route 10 SimpleHostRoutingFilter url請求轉發
route 50 SendForwardFilter forward請求轉發
post 0 SendErrorFilter 處理有錯誤的請求響應
post 10 SendResponseFilter 處理正常的請求響應
從上面的表格可以看到,PreDecorationFilter用于處理請求上下文,優先級為5,是以我們可以定義一個優先級在PreDecorationFilter之後的過濾器,這樣便可以拿到請求上下文。
在網關轉發請求前,請求頭部加入網關資訊
在elsa-gateway子產品的com.elsa.gateway.filter路徑下建立ElsaGatewayRequestFilter:
@Component
public class ElsaGatewayRequestFilter extends ZuulFilter {
private Logger log = LoggerFactory.getLogger(this.getClass());
//對應Zuul生命周期的四個階段:pre、post、route和error,我們要在請求轉發出去前添加請求頭,是以這裡指定為pre;
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
//過濾器的優先級,數字越小,優先級越高。PreDecorationFilter過濾器的優先級為5,是以我們可以指定為6讓我們的過濾器優先級比它低;
@Override
public int filterOrder() {
return 6;
}
//方法傳回boolean類型,true時表示是否執行該過濾器的run方法,false則表示不執行;
@Override
public boolean shouldFilter() {
return true;
}
//定義過濾器的主要邏輯。這裡我們通過請求上下文RequestContext擷取了轉發的服務名稱serviceId和請求對象HttpServletRequest,并列印請求日志。随後往請求上下文的頭部添加了Key為ZuulToken,Value為elsa:zuul:123456的資訊。這兩個值可以抽取到常量類中。
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
String serviceId = (String) ctx.get(FilterConstants.SERVICE_ID_KEY);
HttpServletRequest request = ctx.getRequest();
String host = request.getRemoteHost();
String method = request.getMethod();
String uri = request.getRequestURI();
log.info("請求URI:{},HTTP Method:{},請求IP:{},ServerId:{}", uri, method, host, serviceId);
byte[] token = Base64Utils.encode(ElsaConstant.ZUUL_TOKEN_VALUE.getBytes());
ctx.addZuulRequestHeader(ElsaConstant.ZUUL_TOKEN_HEADER, new String(token));
return null;
}
}
參數配置化:因為需要在各個微服務裡定義一個全局攔截器攔截請求,并校驗Zuul Token。這個攔截器需要被各微服務子產品使用,是以把它定義在通用子產品elsa-common裡。在elsa-common的com.elsa.common.entity路徑下建立ElsaConstant:
public class ElsaConstant {
/**
* Zuul請求頭TOKEN名稱(不要有空格)
*/
public static final String ZUUL_TOKEN_HEADER = "ZuulToken";
/**
* Zuul請求頭TOKEN值
*/
public static final String ZUUL_TOKEN_VALUE = "elsa:zuul:123456";
}
校驗Zuul Token
在處理請求的微服務子產品裡定義全局攔截器,校驗請求頭部的網關資訊。
在elsa-common子產品的com.elsa.common路徑下建立interceptor包,然後在該包下建立ElsaServerProtectInterceptor攔截器:
public class ElsaServerProtectInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
// 從請求頭中擷取 Zuul Token
String token = request.getHeader(ElsaConstant.ZUUL_TOKEN_HEADER);
String zuulToken = new String(Base64Utils.encode(ElsaConstant.ZUUL_TOKEN_VALUE.getBytes()));
// 校驗 Zuul Token的正确性
if (StringUtils.equals(zuulToken, token)) {
return true;
} else {
ElsaResponse elsaResponse = new ElsaResponse();
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write(JSONObject.toJSONString(elsaResponse.message("請通過網關擷取資源")));
return false;
}
}
}
ElsaServerProtectInterceptor實作了HandlerInterceptor的preHandle方法,該攔截器可以攔截所有Web請求。在preHandle方法中,我們通過HttpServletRequest擷取請求頭中的Zuul Token,并校驗其正确性,當校驗不通過的時候傳回403錯誤。
要讓該過濾器生效我們需要定義一個配置類來将它注冊到Spring IOC容器中,在elsa-common子產品的com.elsa.common.configure路徑下建立ElsaServerProtectConfigure:
public class ElsaServerProtectConfigure implements WebMvcConfigurer {
@Bean
public HandlerInterceptor elsaServerProtectInterceptor() {
return new ElsaServerProtectInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(elsaServerProtectInterceptor());
}
}
我們在該配置類裡注冊了ElsaServerProtectInterceptor,并且将它加入到了Spring的攔截器鍊中。
同樣的,要讓該配置類生效,我們可以定義一個@Enable注解來驅動它。在elsa-common子產品的com.elsa.common.annotation路徑下建立EnableElsaServerProtect:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ElsaServerProtectConfigure.class)
public @interface EnableElsaServerProtect {
}
因為現在微服務需要校驗Zuul Token,是以我們需要在上一節定義的Feign攔截器裡也加入Zuul Token,否則Feign調用微服務會報403異常。改造elsa-common子產品下的ElsaOAuth2FeignConfigure類:
public class ElsaOAuth2FeignConfigure {
@Bean
public RequestInterceptor oauth2FeignRequestInterceptor() {
return requestTemplate -> {
// 添加 Zuul Token
String zuulToken = new String(Base64Utils.encode(ElsaConstant.ZUUL_TOKEN_VALUE.getBytes()));
requestTemplate.header(ElsaConstant.ZUUL_TOKEN_HEADER, zuulToken);
Object details = SecurityContextHolder.getContext().getAuthentication().getDetails();
if (details instanceof OAuth2AuthenticationDetails) {
String authorizationToken = ((OAuth2AuthenticationDetails) details).getTokenValue();
requestTemplate.header(HttpHeaders.AUTHORIZATION, "bearer " + authorizationToken);
}
};
}
}
PostMan測試
源碼下載下傳
源碼位址:微服務防護