Spring Cloud-Zuul(十)
个人理解
在微服务体系体系中 我们会有很多服务。在内部体系中 通过eureka实现服务的自动发现通过ribbon实现服务的调用。但是如果对外部体系提供接口 我们就会涉及到接口的安全性,我们不能可能对每个服务做一套安全校验。这样运维是很不方便的,
Zuul则是提供对外访问服务的一个统一的入口,可以通过Zuul做统一的身份 登录签名验证
简单例子
1.引入依赖
<!--内部依赖rabbon histrix actuator 提供/routes返回所有路由端点--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <!--开启端点 用于dashboard监控--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
rabbon 实现网关对服务转发时的负载均衡和请求重试
histrix:实现网关对服务转发的时候的保护机制 线程隔离和断路器 防止转发微服务故障而引起的网关资源无法释放 影响网关的对外服务
2.applicaton开启网关功能
@SpringBootApplication @EnableZuulProxy //开启网关的功能 public class SpringCloudApigatewayApplication { public static void main(String[] args) { SpringApplication.run(SpringCloudApigatewayApplication.class, args); } }
3.yml配置
spring:
application:
name: apiZuul
server:
port: 5555
#传统配置方式单例 不推荐使用
zuul:
routes:
api-a-url:
path: /api-a-url/** #表示这个开头的都会路由到下面的地址
url: http://localhost:8081/
4.启动访问
访问/api-a-url/开头的都将转发到http://localhost:8081/
后台有转发到这个地址
面向服务路由
1.增加pom依赖
<!--使用服务自动发现来注册路由规则 来进行路由--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>
2.yml配置文件修改
#服务的自动发现 eureka: client: serviceUrl: defaultZone: http://peer1:1111/eureka,http://localhost:peer2/eureka #申明api-a 和api 2个路由规则 分别路由到不同的服务 如果服务对个部署 会自动配置 zuul: routes: api-a: path: /consumer/** #路由规则 serviceId: consumer #服务名称 api-b: path: /provider/** #路由规则 serviceId: PROVIDER #服务名称
或者
zuul:
routes:
consumer:
path: /consumer/**
provider:
path: /consumer/**
结果等同
当请求/consumer/** 将自动转发到consumer对应的服务地址
3.在provier UserContorller增加服务信息的打印用于网关的负载均衡
@Autowired Registration registration; @RequestMapping("/registrationInfo") @ResponseBody public String registrationInfo(){ return registration.getHost()+":"+registration.getPort(); }
4.启动前面项目的eureka conusmer和provider测试 providerr需要启动2个不同端口的项目
下面为我的打包路径
java -jar /Users/liqiang/Desktop/java开发环境/javadom/spring-cloud-parent/spring-cloud-provider/target/spring-cloud-provider-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1
java -jar /Users/liqiang/Desktop/java开发环境/javadom/spring-cloud-parent/spring-cloud-provider/target/spring-cloud-provider-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2
5.测试
http://127.0.0.1:5555/provider/user/registrationInfo
zuul在转发服务集群情况下 利用rabbon负载均衡
请求过滤
通过请求过滤实现网关转发前的身份认证权限校验等操作
简单例子
1.创建一个过滤器
public class AccessFilter extends ZuulFilter { private static Logger log= LoggerFactory.getLogger(AccessFilter.class); /*** * 过滤器执行时机 * pre: 可以在请求被路由之前调用。 * route: 在路由请求时被调用。 * post: 在 routing 和 error 过滤器之后被调用。 * error: 处理请求时发生错误时被调用。 */ @Override public String filterType() { return "pre"; } /** * 过滤器执行顺序 * 当同一个时机存在多个过滤器由此来取值用哪个 * @return */ @Override public int filterOrder() { return 0; } /*** * 判断过滤器是否执行 * @return */ @Override public boolean shouldFilter() { return true; } /** * 过滤器执行逻辑 * @return * @throws ZuulException */ @Override public Object run() throws ZuulException { //使用网关校验必须有token RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); log.info("send{} request to{}", request.getMethod(), request.getRequestURL().toString()); //校验是否带有token 如果没有验证不通过 Object accessToken = request.getParameter("accessToken"); if (accessToken == null) { log.warn("access token is empty"); //令zuul过滤该请求 不进行转发 ctx.setSendZuulResponse(false); //返回状态码 ctx.setResponseStatusCode(401); //防止乱码 ctx.getResponse().setContentType("application/json;charset=UTF-8"); //设置body ctx.setResponseBody("无效token"); return null; } log.info("access token ok"); return null; } }
2,创建一个javaconfig 配置zuul
@Configuration public class ZuulConfig { @Bean public AccessFilter initFilter(){ return new AccessFilter(); } }
3.测试 如果我们后缀不带token参数将不会转发
Zuul配置
多实例配置
不适用服务自动发现注册zuul 如何实现多实例的负载均衡
spring:
application:
name: apiZuul
http:
encoding:
enabled: true
charset: utf-8
force: true
server:
port: 5555
tomcat:
uri-encoding: UTF-8
#下面指定了serviceId 默认会去注册中心发现服务 所以禁用
ribbon:
eureka:
enabled:false
zuul:
routes:
provider:
path: /provider/**
serviceId: provider #与下面的provider相对应
provider:
ribbon:
listOfServers: http://localhost:8081/,http://localhost:8082/ #多个服务
默认路由规则
当我们不单独为服务配置路由规则 默认是会以{服务名}/**来配置
其他配置
忽略表达式
zuul.ignored-patterns=/**/hello/** 可以指定规则不被路由
增加前缀
zuul.prefix=/api 给所有路由增加api前缀
#zuul.stripPrefix= false 代理默认会移除前缀 关闭代理移除前缀
#zuul.routes.<route>.strip-prefix=true 指定某个服务的移除前缀
本机跳转
#本机服务器跳转 #zuul.routes.api-b.path=/api-b/** #zuul.routes.api-b.url=forward:/local
转发铭感数据
zuul.sensitiveHeaders=Cookie,Set-Cookie,Authorization 默认禁用这3个参数转发
可以通过zuul.sensitiveHeaders= #空全局取消禁用
或者 指定路由
#方法一:对指定路由开启自定义敏感头 zuul.routes.<router>.customSensitiveHeaders=true
#方法二:将指定路由的敏感头设置为空
zuul.routes.<router>.sensitiveHeaders=
重定向
zuul.addHostHeader=true
解决网关转发到登录页面 登录成功跳转到具体实例而不是网关host
histrix和ribbon超时
#设置zuul使用histrixCommand转发请求的超时时间
hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 1000
#ribbon: # ReadTimeout: 3000 #rabbon 建立连接的超时时间 如果小于hystix的超时时间 则超时会开启重试直到大于等于hystrix的超时日期 # ConnectTimeout: 1000 #请求建立连接的超时时间 如果小于hystirx 则会触发重试 直到大于等于hystirx的超时日期
关闭重试
#以下2个参数为针对上面的禁止发起重试 # zuul.retryable= false # zuul.routes.<route>.retryable= false
自定义路由规则
微服务定义不同版本的服务 起到每次服务更新 而不是强制所有调用方都更新
服务通过servicename-v1来进行命名
@Configuration public class ZuulConfig { @Bean public AccessFilter initFilter() { return new AccessFilter(); } @Bean public PatternServiceRouteMapper serviceRouteMapper() { return new PatternServiceRouteMapper( "(?<name>.+)-(?<version>v.+$)", "${version}/${name}"); } }
服务名字以servicename-v1 默认路由规则 会变成servicename-v1/**
我们通过上面PatternServiceRouteMapper 第一个参数 会匹配所有servicename-v1 的路由规则 并通过正则表达式的捕获将服务名字和版本抓取出来 改为${version}/${name} 填充此规则
路径匹配规则
默认过滤
* pre级别的默认过滤器:
* 1.ServletDetectionFilter filterOrder:-3 最先执行 用于判断是zuulServlet 还是dispacherServlet 来处理运行
* 2.Servlet30WrapperFilter filterOrder:-2 将HttpServletRequest包装成Servlet30RequestWrapper
* 3.FormBodyWrapperFilter filterOrder:-1 通过1判断如果是dispacherServlet 通过2判断content-type 类型是application/x-www form-urlencoded
* 或者multipart/formdata 则将body包装为FormBodyRequestWrapper
* 4.DebugFilter filterOrder:1 主要用于如果设置了 zuul.debug.request.debug=true 来判断是否执行这个过滤器器 debugRouting debugRequest判断打印日志类型
* 5.PreDecorationFilter filterOrder:5 在转发请求前做预处理 可以通过RequestContext. getCurrentContext () 拿到请求报文信息
* route级别的过滤器:
* 1.RibbonRoutingFilter filterOrder 10 判断上下文是否存在serviceid 如果存在则用Hystrix 或者rabbon 对服务实例发起请求 并将结果返回、
* 2.SimpleHostRoutingFilter filterOrder:100 判断上下文是否存在routeHost 就是我们配置路由的ip地址配置 直接发起请求使用httpClient实现 没有使用histrix
* 3.SendForwardFilter 主要用于判断上下文是否存在forward.to 就是配置文件配置的本地跳转forward:/local
* post级别过滤器:
* SendErrorFilter 执行顺序为0 判断上下文是否存在error.status_code(之前过滤器设置的错误编码) 根据错误信息forward 到/error错误端点产生错误响应
* SendPesponseFilter 执行顺序是100 判断上下文是否有响应头信息(响应头和响应流) 主要是根据上下文响应信息 组织需要发送回客户端的响应信息
*
异常处理
如果过滤器发生异常将会怎么样
1.测试在第一个例子中抛出一个RuntimeException异常
/** * 过滤器执行逻辑 * @return * @throws ZuulException */ @Override public Object run() throws ZuulException { int i= 1/0; return null; }
利用SendErrorFilter抛出更友好提示
@Override public Object run() throws ZuulException { // 测试异常过滤器处理 RequestContext ctx = RequestContext.getCurrentContext(); try { int i = 1 / 0; }catch (Exception e){ //防止乱码 ctx.getResponse().setHeader("Content-Type","text/html;charset=UTF-8"); //异常过滤器shouldFilter判断这个参数为false才会执行 ctx.set("sendErrorFilter.ran",false); throw new ZuulException(e, "Forwarding error", 500,"发生了异常"); } return null; }
SendErrorFilter模式是交给org.springframework.boot. autoconfigure.web.BasicErrorController 来处理异常的 我们可以查看源码 参照重写覆盖原有的返回更友好提示
@RestController public class ErrorHandler implements ErrorController { @GetMapping(value = "/error") public ResponseEntity<ErrorBean> error(HttpServletRequest request) { String message = request.getAttribute("javax.servlet.error.message").toString(); ErrorBean errorBean = new ErrorBean(); errorBean.setMessage(message); errorBean.setReason("程序出错"); return new ResponseEntity<>(errorBean, HttpStatus.BAD_GATEWAY); } @Override public String getErrorPath() { return "error"; } private static class ErrorBean { private String message; private String reason; public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public String getReason() { return reason; } public void setReason(String reason) { this.reason = reason; } } }
再次执行返回
自定义errorFilter
自定义之前先了解sendErrorFilter部分源码
public class SendErrorFilter extends ZuulFilter { private static final Log log = LogFactory.getLog(org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter.class); protected static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran"; /* 处理异常的contoller地址 可以通过error.path自定义默认是/error 对应org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController */ @Value("${error.path:/error}") private String errorPath; public SendErrorFilter() { } public String filterType() { return "error"; } public int filterOrder() { return 0; } public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); //如果ctx有异常同时sendErrorFilter.ran为false 则执行异常过滤器 return ctx.getThrowable() != null && !ctx.getBoolean("sendErrorFilter.ran", false); } public Object run() { try { RequestContext ctx = RequestContext.getCurrentContext(); org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter.ExceptionHolder exception = this.findZuulException(ctx.getThrowable()); HttpServletRequest request = ctx.getRequest(); request.setAttribute("javax.servlet.error.status_code", exception.getStatusCode()); log.warn("Error during filtering", exception.getThrowable()); request.setAttribute("javax.servlet.error.exception", exception.getThrowable()); if (StringUtils.hasText(exception.getErrorCause())) { request.setAttribute("javax.servlet.error.message", exception.getErrorCause()); } //获得对应的处理器 RequestDispatcher dispatcher = request.getRequestDispatcher(this.errorPath); if (dispatcher != null) { ctx.set("sendErrorFilter.ran", true); if (!ctx.getResponse().isCommitted()) { ctx.setResponseStatusCode(exception.getStatusCode()); //转发到对应的contoller执行 dispatcher.forward(request, ctx.getResponse()); } } } catch (Exception var5) { ReflectionUtils.rethrowRuntimeException(var5); } return null; } }
上面有一段return ctx.getThrowable() != null && !ctx.getBoolean("sendErrorFilter.ran", false);
尝试吧之前的测试做修改 发现怎么也不会到异常过滤器
@Override public Object run() throws ZuulException { // 测试异常过滤器处理 RequestContext ctx = RequestContext.getCurrentContext(); try { int i = 1 / 0; }catch (Exception e){ //防止乱码 ctx.getResponse().setHeader("Content-Type","text/html;charset=UTF-8"); //异常过滤器shouldFilter判断这个参数为false才会执行 ctx.set("sendErrorFilter.ran",false); ctx.setThrowable(e); } return null; }
查看过滤器的核心入口com.netflix.zuul.http.ZuulServlet
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { try { this.init((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse); RequestContext context = RequestContext.getCurrentContext(); context.setZuulEngineRan(); try { //执行pre过滤器 this.preRoute(); } catch (ZuulException var13) { //发生了异常 交给error过滤器 error过滤器处理了 再交由postRoute过滤器 this.error(var13); this.postRoute(); return; } try { //执行toute过滤器 this.route(); } catch (ZuulException var12) { this.error(var12); //发生了异常 交给error过滤器 error过滤器处理了 再交由postRoute过滤器 this.postRoute(); return; } try { //执行post过滤器 this.postRoute(); } catch (ZuulException var11) { //发生了异常 交给error过滤器 this.error(var11); } } catch (Throwable var14) { //抛出的非zuul异常 最外层转为zuul异常 this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName())); } finally { RequestContext.getCurrentContext().unset(); } }
为什么都会调用postRoute呢 因为postRoute里面有一个SendPesponseFilter是组织响应内容
优化
通过上面也发现一个问题 执行post过滤器的时候发生了异常 就调用了额 异常过滤器 而没有调用post过滤器 组织异常
1.为请求上下文自定义属性 用于判断当前过滤器的级别
/** * • getinstance(): 该方法用来获取当前处理器的实例。 • setProcessor(FilterProcessor processor): 该方法用来设置处理器实 例, 可以使用 此方法来设置自定义的处理器。 • processZuulFilter(ZuulFilter filter): 该方法定义了用来执行 filter 的具体逻辑, 包括对请求上下文的设置, 判断是否应 该 执行, 执行时 一些异常的处理 等。 • getFiltersByType(Stringng filterType) : 该 方 法 用 来 根 据 传 入 的 filtererType 获取API网关中对应类型的过滤器, 并根据这些过滤器的 filterOrder从小到大排序, 组织成一个列表返回。 • runFi让ers(String sType): 该方法会根据传入的 filterType 来调用 getFi让ersByType(String fiterType)获取排序后的过滤器列表, 然后轮 询这些过滤器, 并调用 processZuulFiler(ZuulFilter filter)来依次执 行它们。 • preRoute(): 调用runFilters("pre")来执行所有pre类型的过滤器。 • route(): 调用runFilters("route")来执行所有route类型的过滤器。 • postRoute(): 调用runFi辽ers("post")来执行所有post类型的过滤器。 • error(): 调用runFilters("error")来执行所有error类型的过滤器。 */ @Component public class DidiFilterProcessor extends FilterProcessor { /** * 调用p此方法执行post过滤器 我们可以自定义属性 判断是是post过滤器 导致的异常 用于errorExtFilter使用 * @throws ZuulException */ @Override public Object processZuulFilter(ZuulFilter filter) throws ZuulException { try { return super.processZuulFilter(filter); }catch (Exception e){ RequestContext ctx = RequestContext.getCurrentContext(); ctx.set("failed.filter", filter); throw e; } } }
2.main方法注入
@SpringBootApplication @EnableZuulProxy //开启网关的功能 public class SpringCloudApigatewayApplication { public static void main(String[] args) { SpringApplication.run(SpringCloudApigatewayApplication.class, args); //注入自定义processor 配合errorExtFilter FilterProcessor.setProcessor (new DidiFilterProcessor()); } }
3.自定义一个异常过滤器
@Component public class ErrorExtFilter extends SendErrorFilter { @Override public String filterType() { return "error"; } //大于SendErrorFilter 的就行 @Override public int filterOrder() { return 1; } @Override public boolean shouldFilter() { //因为别的异常 sendErrorFilter都处理了所以我们只处理post级别过滤器就行 判断是否是post级别过滤请求 //failed.filter 此属性DidiFilterProcesor里面社会组 //判断:仅处理来自post过滤器引起的异常 RequestContext ctx = RequestContext.getCurrentContext(); ZuulFilter failedFilter = (ZuulFilter) ctx.get("failed.filter"); return failedFilter!=null&&failedFilter.filterType().equals("post"); } @Override public Object run() { //这里可以单独对post产生的异常做处理 return super.run(); } }
4.添加一个post过滤器 用于测试
/** * 查看zuulServlet可以发现 如果post过滤器出现异常 是不会调用post post则是返回客户端信息 则报错 * 为了解决这个问题我们自定义一个专门处理post异常的过滤器ErrorExtFilter */ public class PostFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.POST_TYPE; } @Override public int filterOrder() { return 99; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { //对post产生的异常做post处理 throw new RuntimeException(""); } }
如何禁用过滤器
zuul.<SimpleClassName>.<filterType>.disable=true 第一个为过滤器类名 第二个为阶段
查看所有路由规则
1.配置文件配置端点启用
#启用端点 management: endpoints: web: exposure: include: routes info: enabled: false
2.pom依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
3.测试
获得request和命中路由信息