Zuul使用和源码分析
1丶网关介绍
- 统一入口:为全部微服务提供唯一入口点,网关起到外部和内部隔离,保障了后台服务的安全
性。- 鉴权校验:识别每个请求的权限,拒绝不符合要求的请求。
- 动态路由:动态的将请求路由到不同的后端集群中。
- 减少客户端与服务的耦合,服务可以独立发展。通过网关层来做映射。
Zuul介绍
zuul 是netflix开源的一个API Gateway 服务器, 本质上是一个web servlet应用。
Zuul 在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和
Netflix 流应用的 Web 网站后端所有请求的前门。
2丶 基础环境搭建
创建工程Zuul
引入logback.xml,进行配置
server: port: 8888 logging: config: classpath:logback.xml eureka: client: service-url: defaultZone: http://localhost:8761/eureka
引入依赖
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> </dependencies>
开启zuul
引入msb-order配置
启动服务访问zuul
请求路由
Zuul可以通过配置完成请求路由配置
Zuul服务路由默认支持serviceld作为上下文
ignored-services可以禁用serviceld
3丶Zuul请求表达式详解
请求路由表达式
- ? : 匹配任意单个字符
- *:配置任意数量的字符
- ** :配置任意数量的字符,支持多级目录
4丶Zuul架构介绍
讲解调用流程
- PRE Filters(前置过滤器) :当请求会路由转发到具体后端服务器前执行的过滤器,比如鉴权过滤
器,日志过滤器,还有路由选择过滤器- ROUTING Filters (路由过滤器):该过滤器作用是把请求具体转发到后端服务器上,一般是
通过Apache HttpClient 或者 Netflix Ribbon把请求发送到具体的后端服务器上- POST Filters(后置过滤器):当把请求路由到具体后端服务器后执行的过滤器;场景有添加标
准http 响应头,收集一些统计数据(比如请求耗时等),写入请求结果到请求方等。- ERROR Filters(错误过滤器):当上面任何一个类型过滤器执行出错时候执行该过滤器
5丶自定义Filter
- 继承ZuulFilter并实现相应的方法
- 设置Filter类型、级别和是否启用
- 开发具体的业务逻辑
编写Filter
package com.msb.zuul.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import lombok.extern.slf4j.Slf4j; import org.springframework.web.servlet.mvc.condition.RequestCondition; import javax.servlet.http.HttpServletRequest; import java.util.Enumeration; @Slf4j public class MyFilter extends ZuulFilter { /** *filter类型 * @return */ @Override public String filterType() { return "pre"; } /** * filter执行顺序,越小优先级越高 * @return */ @Override public int filterOrder() { return 0; } /** * 是否启动:这给定制化编程提供了方便,比如我给不同公司提供不同方案,有的公司需要使用这个 * 过滤器,有的不需要 * @return */ @Override public boolean shouldFilter() { return false; } /** * Filter里面具体执行业务逻辑 * @return * @throws ZuulException */ @Override public Object run() throws ZuulException { // RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()){ String headName = headerNames.nextElement(); log.info("headName:{}, headValue:{}", headName, request.getHeader(headName)); } return null; } }
配置Filter
@Configuration public class ZuulConfig { @Bean public MyFilter initMyFilter(){ return new MyFilter(); } }
6丶限制ip黑名单的频繁请求
redis依赖引入
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
引入配置
spring: redis: database: 0 host: localhost port: 6379 # ip请求限制的参数配置 blackIp: continueCounts: ${counts:10} # ip连续请求的次数 timeInterval: ${interval:10} # ip判断的事件间隔,单位:秒 limitTimes: ${times:15} # 限制的事件,单位:秒
代码
package com.msb.zuul.util; import javax.servlet.http.HttpServletRequest; public class IPUtil { /** * 获取请求的IP: * 用户的真实IP不能用reqeust.getRemoteAddr() * 这是因为可能会使用一些代理软件,这样IP获取就不准确了,除此之外 * 如果使用了多级反向代理的话,ip需要从X-Forwarded-For中获取一个非unknow的ip才是有效的IP * @param request * @return */ public static String getRequestIP(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; } }
package com.msb.zuul.util; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; @Component public class RedisOperator { @Autowired private StringRedisTemplate redisTemplate; /** * 获取剩余时间 * @param ipRedisLimitKey * @return */ public long ttl(String ipRedisLimitKey) { return redisTemplate.getExpire(ipRedisLimitKey); } /** * 实现命令 increment key 增加key delta次 * @param ipRedisKey * @param delta * @return */ public long increment(String ipRedisKey, int delta) { return redisTemplate.opsForValue().increment(ipRedisKey,delta); } /** * 实现命令expire 设置过期时间 单位秒 * @param key * @param timeout */ public void expire(String key, Integer timeout) { redisTemplate.expire(key,timeout, TimeUnit.SECONDS); } /** * 对key设置超时时间 * @param key * @param value * @param timeout */ public void set(String key, String value, Integer timeout) { redisTemplate.opsForValue().set(key,value,timeout,TimeUnit.SECONDS); } }
@Component @RefreshScope public class BlackIPFilter extends ZuulFilter { @Value("${blackIp.continueCounts}") public Integer continueCounts; @Value("${blackIp.timeInterval}") public Integer timeInterval; @Value("${blackIp.limitTimes}") public Integer limitTimes; @Autowired private RedisOperator redis; @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 2; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { System.out.println("执行【ip黑名单】过滤器..."); System.out.println("continueCounts: " + continueCounts); System.out.println("timeInterval: " + timeInterval); System.out.println("limitTimes: " + limitTimes); // 获得上下文对象 RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest();
// 获得ip String ip = IPUtil.getRequestIp(request); /** * 需求: * 判断ip在10秒内的请求次数是否超过10次 * 如果超过,则限制这个ip访问15秒,15秒以后再放行 */ final String ipRedisKey = "zuul-ip:" + ip; final String ipRedisLimitKey = "zuul-ip-limit:" + ip; // 获得当前ip这个key的剩余时间 long limitLeftTime = redis.ttl(ipRedisLimitKey); // 如果当前限制ip的key还存在剩余时间,说明这个ip不能访问,继续等待 if (limitLeftTime > 0) { stopRequest(context); return null; } // 在redis中累加ip的请求访问次数 long requestCounts = redis.increment(ipRedisKey, 1); // 从0开始计算请求次数,初期访问为1,则设置过期时间,也就是连续请求的间隔时间 if (requestCounts == 1) { redis.expire(ipRedisKey, timeInterval); } // 如果还能取得请求次数,说明用户连续请求的次数落在10秒内 // 一旦请求次数超过了连续访问的次数,则需要限制这个ip的访问 if (requestCounts > continueCounts) { // 限制ip的访问时间 redis.set(ipRedisLimitKey, ipRedisLimitKey, limitTimes); stopRequest(context); } return null; } private void stopRequest(RequestContext context) { // 停止zuul继续向下路由,禁止请求通信 context.setSendZuulResponse(false); context.setResponseStatusCode(200); Map result = new HashMap<>(); result.put("message","数据进行限流"); String resultStr = JSONObject.toJSONString(result); context.setResponseBody(resultStr); context.getResponse().setCharacterEncoding("utf-8"); context.getResponse().setContentType(MediaType.APPLICATION_JSON_VALUE); }
}
7丶过滤器代码重构
/** * 通用的抽象过滤器类 */ public abstract class AbstractZuulFilter extends ZuulFilter { // 用于在过滤器之间传递消息,数据保存在每个请求的ThreadLocal中 // 扩展Map RequestContext context; private final static String NEXT = "next"; @Override public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); return (boolean)ctx.getOrDefault(NEXT,true); } @Override public Object run() throws ZuulException { context = RequestContext.getCurrentContext(); return cRun(); } protected abstract Object cRun(); Object fail(int code ,String msg){ context.set(NEXT, false); context.setSendZuulResponse(false); context.getResponse().setContentType("text/html;charset=UTF-8"); context.setResponseStatusCode(code); context.setResponseBody(String.format("{\"result\": \"%s!\"}", msg)); return null; } Object success() { context.set(NEXT, true); return null; } } /** * 前置路由过滤器 */ public abstract class AbstractPreZuulFilter extends AbstractZuulFilter{ @Override public String filterType() { return FilterConstants.PRE_TYPE; } } /** * 后置路由过滤器 */ public abstract class AbstractPostZuulFilter extends AbstractZuulFilter{ @Override public String filterType() { return FilterConstants.POST_TYPE; } }
8丶增加日志过滤器
@Slf4j @Component public class PreRequestFilter extends AbstractPreZuulFilter { @Override protected Object cRun() { context.set("startTime", System.currentTimeMillis()); return success(); } @Override public int filterOrder() { return 0; } } @Component public class AccessLogFilter extends AbstractPostZuulFilter { @Override protected Object cRun() { HttpServletRequest request = context.getRequest(); // 从 PreRequestFilter 中获取设置的请求时间戳 Long startTime = (Long) context.get("startTime"); String uri = request.getRequestURI(); long duration = System.currentTimeMillis() - startTime; // 从网关通过的请求都会打印日志记录: uri + duration log.info("uri: {}, duration: {}", uri, duration); return success(); } @Override public int filterOrder() { return 10; } }
9丶Zuul整合Hystrix
引入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
进行Hystrix属性配置
hystrix: command: default: execution: timeout: enabled: true isolation: thread: timeoutInMilliseconds: 10
进行访问
报错超时
降级设置
@Component public class MyFallback implements FallbackProvider { /** * 针对那一个路由进行 降级,return可以写 * * @return */ @Override public String getRoute() { return "msb-order"; } /** * 降级的处理方式 * @return */ @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.OK; } @Override public int getRawStatusCode() throws IOException { return 200; } @Override public String getStatusText() throws IOException { return "ok"; } @Override public void close() { } /** * 业务降级处理方式 * @return */ @Override public InputStream getBody() throws IOException { Map result = new HashMap<>(); result.put(200,"服务降级"); String resultJson = JSONObject.toJSONString(result); return new ByteArrayInputStream(resultJson.getBytes()); } /** * 设置head信息 * @return */ @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; } }
引入对应的依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.60</version> </dependency>
10丶Cookie和头信息处理
- Zuul帮助我们过滤了一些非安全的信息
- 诸如cookie、set-Cookie和authorization等
- 可以通过设置sensitiveHeaders来修改
msb-order项目中打印一下对应头信息
我们可以发现msb-zuul的日志里面是有数据的
msb-order里面也有对应的数据
我们配置sensitive-headers或者ignored-services