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和命中路由信息

  • 相关阅读:
    ⑬linux基础命令 wget
    爱情的诗·21~25节
    爱情的诗·16~20节
    爱情的诗·11~15节
    人生的诗·406~410节
    唐诗宋词学习·126~130节
    爱情的诗·6~10节
    人生的诗·401~405节
    唐诗宋词学习·121~125节
    唐诗宋词学习·100~105节
  • 原文地址:https://www.cnblogs.com/LQBlog/p/10150544.html
Copyright © 2011-2022 走看看