目前我们所有客户端请求都是通过微服务网关转发完成的,但是还是可以直接访问微服务地址的方式来获取服务,我们使用PostMan分别向
发送GET请求,都能得到响应结果。
localhost:8301/system/hello(网关)
为了避免客户端请求绕过网关,直接调用微服务,我们可以在网关转发请求至微服务前和微服务被调用前之间做一些必要的过滤和拦截处理。
解决这个问题的原理:
这样就能避免客户端直接访问微服务了。
在自定义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";
}
在处理请求的微服务模块里定义全局拦截器,校验请求头部的网关信息。
在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);
}
};
}
}
源码地址:微服务防护