zoukankan      html  css  js  c++  java
  • 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/form­data 则将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和命中路由信息

  • 相关阅读:
    JavaEE——SpringMVC(11)--拦截器
    JavaEE——SpringMVC(10)--文件上传 CommonsMultipartResovler
    codeforces 460A Vasya and Socks 解题报告
    hdu 1541 Stars 解题报告
    hdu 1166 敌兵布阵 解题报告
    poj 2771 Guardian of Decency 解题报告
    hdu 1514 Free Candies 解题报告
    poj 3020 Antenna Placement 解题报告
    BestCoder5 1001 Poor Hanamichi(hdu 4956) 解题报告
    poj 1325 Machine Schedule 解题报告
  • 原文地址:https://www.cnblogs.com/LQBlog/p/10150544.html
Copyright © 2011-2022 走看看