一、概述
上篇文章我们介绍了基于Eureka注册服务提供者和消费者,使用Feign、Ribbon、Hystrix实现服务间的调用、负载均衡及服务熔断和降级功能。本文将基于上述服务,利用SpringCloud Zull实现对外的统一网关服务,可在网关服务内实现签名验证、IP过滤、统一转发等功能,同时基于Hystrix实现对服务的监控功能
二、功能开发实现
1.创建统一网关服务
创建普通的SpringBoot项目gateway,在pom.xml文件中增加如下依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
在GatewayApplication主方法上添加@EnableZuulProxy注解,启用服务路由功能。查看@EnableZuulProxy源码可知,其包含了服务注册和断路保护注解功能
@EnableCircuitBreaker
@EnableDiscoveryClient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {
}
2.服务路由配置
Spring Cloud Zuul通过与 Eureka的整合,实现了对服务实例的自动化维护,所以在使用服务路由配置的时候,我们不需要像传统路由配置方式那样为serviceId去指定具体的服务实例地址,只需要通过一组zuul.routes.<route>.path
与zuul.routes.<route>.serviceId
参数对的方式配置即可。
修改配置文件,配置转发到eureka-consumer服务的请求路由
spring.application.name=gateway
server.port=9103
#注册用IP地址代替服务名
eureka.instance.preferIpAddress=true
eureka.instance.instance-id=${spring.cloud.client.ipAddress}:${spring.application.name}:${server.port}
eureka.client.serviceUrl.defaultZone=http://10.17.5.45:9911/eureka/,http://10.17.5.46:9912/eureka/
#zuul默认为所有服务开启默认的路由,为了服务安全,此处关闭
zuul.ignored-services=*
#自定义服务路由
zuul.routes.eureka-consumer.path=/eureka-consumer/**
zuul.routes.eureka-consumer.serviceId=eureka-consumer
启动服务,所有向/eureka-consumer/**的请求都会被转发到服务名为eureka-consumer的服务上去,打开postman测试请求
从日志中可以看出,zuul从DynamicServerListLoadBalancer中根据Ribbon负载均衡算法从服务列表中选取后端服务,然后发送请求,返回结果。
对于面向服务的路由配置,除了使用path和serviceId映射的配置方式之外,还有一种更简洁的配置方式:zuul.routes.<serviceId>=<path>
,其中<serviceId>
用来指定路由的具体服务名,<path>
用来配置匹配的请求表达式。比如上面的例子,它的路由规则等价于上面通过path
与serviceId
组合使用的配置方式。
zuul.routes.eureka-consumer=/eureka-consumer/**
修改配置文件,重启服务,用postman调用服务,可得同样的路由规则
3.实现IP白名单功能
我们主要利用Zuul的过滤器来实现该功能,其过滤器还可以用来实现统一的签名和验签服务以及其他任何你需要的服务,本例实现简单的IP过滤功能
新建IPFilter类,继承ZuulFilter,实现其方法
@Component
public class IPFilter extends ZuulFilter {
Logger logger= LoggerFactory.getLogger(getClass());
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx= RequestContext.getCurrentContext();
HttpServletRequest req=ctx.getRequest();
String ipAddr=this.getIpAddr(req);
logger.info("请求IP地址为:[{}]",ipAddr);
//配置本地IP白名单,生产环境可放入数据库或者redis中
List<String> ips=new ArrayList<String>();
ips.add("172.0.0.1");
if(!ips.contains(ipAddr)){
logger.info("IP地址校验不通过!!!");
ctx.setResponseStatusCode(401);
ctx.setSendZuulResponse(false);
ctx.setResponseBody("IpAddr is forbidden!");
}
logger.info("IP校验通过。");
return null;
}
/**
* 获取Ip地址
* @param request
* @return
*/
public String getIpAddr(HttpServletRequest request){
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
重启服务,再次发送请求,本地IP被禁止访问
在上面实现的过滤器代码中,我们通过继承ZuulFilter类并重写了下面的四个方法来实现自定义的过滤器.
filterType
:过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。这里定义为pre
,代表会在请求被路由之前执行。另外还有“route”、“post”、"error”等类型,具体解释如下-
/** * 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 */ abstract public String filterType();
filterOrder
:过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行。shouldFilter
:判断该过滤器是否需要被执行。这里我们直接返回true,因此该过滤器对所有请求都会生效,实际运用中我们可以利用该函数来指定过滤器的有效范围。run
:过滤器的具体逻辑。IP校验不通过时我们通过ctx.setSendZuulResponse(false)
令zuul过滤该请求,不对其进行路由,然后通过ctx.setResponseStatusCode(401)
设置了其返回的错误码,通过ctx.setResponseBody(body)
返回body内容。
三、小结
Demo源码地址:https://gitee.com/gengkangkang/springcloud.git