• SpringCloud-zuul


    引入pom文件

    
        
          org.springframework.cloud
          spring-cloud-starter-ribbon
        
        
        
          org.springframework.cloud
          spring-cloud-starter-netflix-eureka-client
        
        
        
          org.springframework.cloud
          spring-cloud-starter-zuul
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    注意上面部分的依赖,因为网关也是基于Eureka的说一客户端一定要引入,否则网关根本跑不起来
    另外网关默认也是基于ribbon的,所以也要引入。

    关于代码

    入口APP
    很简单:

    @SpringBootApplication
    @EnableZuulProxy
    public class App
    {
        public static void main( String[] args )
        {
            SpringApplication.run(App.class, args);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    关于配置

    Eurake的配置

    这个是基本的配置:

    server:
      port: 5000 #网关5字头,第一个
    
    • 1
    • 2

    指定当前eureka客户端的注册地址,也就是eureka服务的提供方,当前配置的服务的注册服务方

    eureka:
      instance:
        hostname: 172.17.7.90
      client:
        service-url:
          defaultZone: http://${eureka.instance.hostname}:2000/eureka
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    #指定应用名称

    spring:
      application:
        name: zuul-agw
    
    • 1
    • 2
    • 3

    这个和普通的基本eureka没啥区别,有了这个网关就能跑起来,其他不说了。

    关于超时

    Zuul超时有两种方式,比较靠谱的方式用ribbon进行超时。Zuul是用ribbon做负载均衡的,所以配置了ribbon就配置了超时。

    ribbon:
      eureka:
        enabled: true
      ReadTimeout: 3500
      ConnectTimeout: 3500
    
    • 1
    • 2
    • 3
    • 4
    • 5

    顺便说一下红色部分,红色部分很重要,如果使用eureka的服务列表,那么这个开关一定要打开,否则在eureka上线和下线的时候,通知不及时会导致服务异常!

    路由配置

    zuul:
      # 使用 prefix 添加前缀
      prefix: /zyth
      routes:
        eureka:
          path: /monitor-eureka/**
          url: url
    	  serviceId: oneServiceName
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上面就是一个最简单的配置。
    说明一下,prefix是统一前缀,就是先判断能否进入这个网关代理,有这个前缀的才进入网关代理。如果不配置,就是所有的请求都经过代理。

    Routes就是具体不同的二级后缀和实际转发的配置了。
    Path:是匹配的模式。
    模式匹配到了,就有两个转发模式:url或者serviceId
    url就会转入到指定的http地址中,而serviceId就会转入到一个具体的微服务地址中。如果url后面的值,不是http等,这个地方就会当作是一个serviceID处理,也就是说,url这里直接配置serviceId也是ok的

    关于统一前缀是否去掉

    关于统一前缀是否去掉。
    在这里插入图片描述

    关于路由

    服务路由有两种,默认情况下,配置了eureka后,直接就是/前缀/服务id/服务端点。这样方式。
    如果觉得服务前缀不爽,就要手动修改路由了,有两种方式,当然两种方式都是通过urlmapping实现的。

    第一种方式:通过直接的ip地址:
    网关配置

    zuul:
      # 使用 prefix 添加前缀
      prefix: /zyth
      routes:
        eureka:
          path: /monitor-eureka/**
          url: http://${eureka.instance.hostname}:2000/
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如上,是对eureka的配置,这个名字随便取
    下面有paht,这个无论哪种配置都是一样的,就是使用path路径映射
    然后,就是对应这个路径下的访问url了。
    下面url针对集群,是可以配置多个的,用’,’隔开就行。
    另外,以上就是,配置访问对eurake的配置逻辑。
    第二种方式:通过配置服务id,就是注册到eureka上的服务名进行访问:

    prefix: /zyth
      routes:
        oneservice:
          path: /monitor-eureka/**
          serviceId: serviceName
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这种方式中serviceId就是eureka上,可以按照动态列表进行获取
    另外对第一种方式的补充说明:
    如果没有经过Eureka服务器,那自然就得不到Ribbon的负载均衡功能了。针对这个问题,Zuul已经帮开发者想到了解决方案,在这种情况下开发者只需要禁用Ribbon与Eureka的自动集成设置,采用手工设置方式开启即可,配置如下:

    zuul:
    	routes:
    		myroutes1:
    			path: /mypath/**
    			serviceId: myserverId
    myserverId:
    	ribbon:
    	listOfServers: localhost:8080, localhost:8081
    
    ribbon:
    	eureka:
    	enabled: false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    默认路由和安全

    默认路由就是zuul啥都不配的情况下,默认就是微服务名称转向对应的微服务即:
    [servicename]:[servicename]
    这是一个巨坑,很多时候,我们要显示配置,微服务内部服务是不暴露出来的。这个需要手动配置关闭:

    zuul:
      ignored-services: '*'
      ignoredPatterns: /**/channel/pay/**,/**/channel/transfer/**
    
    • 1
    • 2
    • 3

    第二个配置项,和安全相关,就是直接屏蔽掉某些不该有的pattern

    跨域

    跨域实际就是在响应的时候统一加上跨域头部。
    实际代码可以是在setting中加入如下:

    @Component
    public class CorsFilter implements Filter {
        @Override
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;
           /* String curOrigin = request.getHeader("Origin");
            System.out.println("###跨域过滤器->当前访问来源->"+curOrigin+"###");   */
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Methods", "*");
            response.setHeader("Access-Control-Max-Age", "3600");
            response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
            chain.doFilter(req, res);
        }
        @Override
        public void init(FilterConfig filterConfig) {}
    
        @Override
        public void destroy() {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    说明,这个应该可以统一用网关的拦截器做的,还没有验证过,后面可以验证一下。

    拦截

    Zuul的拦截器,很多门道,基础就是继承一个类,实现拦截:ZuulFilter

    继承方法介绍

    public String filterType() 这个函数返回拦截器类型,可以之前拦截,可以之后拦截,主要看返回的是什么类型

    public int filterOrder() 这个函数是返回拦截器的执行顺序,可见是可以支持多个拦截器的
    public boolean shouldFilter() 这个函数是返回是否要执行本拦截

    public Object run() throws ZuulException 真正的拦截器逻辑代码

    设置注解

    这个要只要继承就可以使用,但是如果要加入到spring的注入中,就要增加要给注解,简单的就是用:

    @Component
    
    • 1

    Filter间传递值

    RequestContext ctx = RequestContext.getCurrentContext();
    
    • 1

    获取基本的servlet请求对象

    直接看代码吧

    RequestContext ctx = RequestContext.getCurrentContext();
    HttpServletRequest request = ctx.getRequest();
    String url = request.getRequestURI();
    log.info(String.format("LoginCheck(%s)->开始校验",url));
    
    • 1
    • 2
    • 3
    • 4

    如何拦截

    当该请求非法,就要拦截,拦截是在run函数里面执行的。Run函数返回null就行,无论成功还是失败。
    但真正的拦截就要靠如下代码:

    //登录验证失败了
    ctx.set(GWConst.FILTER_RESULT_NAME_LASTRESULT,false);
    ctx.setSendZuulResponse(false);
    ctx.set("sendForwardFilter.ran", true);
    ctx.setResponseBody(JSON.toJSONString(GWConst.LOGINCHECK_FAIL_NOLOGIN));
    log.info(String.format("LoginCheck(%s)->token(%s),rbacuui(%s)校验失败",url,token,rbacUserUUID));
    return null;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    ctx.setSendZuulResponse(false);表示要拦截,不转向到后面微服务代码了。所以反之如果成功的话这里要设置成true
    ctx.set(“sendForwardFilter.ran”, true);设置到ctx上面,标识已经拦截了,其他filter看到这个标记可以不拦截,当然这个业务自己实现的。
    ctx.set(GWConst.FILTER_RESULT_NAME_LASTRESULT,false);这个就比较有门道了,这里设置了一个失败标识。为什么呢?原因是,第一个setSendZuulResponse设置成false后,是不会分发请求了,但是后续的其他filter会继续执行的。所以这里设置了一个我们自己定义的一个特殊标识,其他filter中,可以在shouldFilter山上中取这个标识并判断是否需要继续filter了
    ctx.setResponseBody就是设置返回的结果了。

    增加头部

    一般我们的业务逻辑就是判断用户是否可以登录,如果可以登录,我们就会放置一些信息给后面的服务器,给后面服务器信息,最好就是在head上增加内容了。

    //其他情况下是成功的,需要进行转换
    ctx.addZuulRequestHeader(GWConst.FILTER_RESULT_NAME_HEAD_TOKEN, token);
    ctx.addZuulRequestHeader(GWConst.FILTER_RESULT_NAME_HEAD_RBACUSERUUID, rbacUserUUID);
    
    • 1
    • 2
    • 3

    修改请求参数

    参数拦截后,可能要根据拦截对象将整个请求参数修改掉。
    这里分两种情况:get请求和post请求。如果参数是写在url上的get请求,直接用ctx的方法即可。
    否则要重写整个requestbody。

    private void resetParameter(RequestContext ctx, Map<String, List<String>> paramList) {
            String method = ctx.getRequest().getMethod();
            if ("get".equalsIgnoreCase(method)) {
                ctx.setRequestQueryParams(paramList);
            }
            else if("POST".equalsIgnoreCase(method)){
                String charSet = ctx.getRequest().getCharacterEncoding();
                try{
                    StringBuilder postParamString = new StringBuilder();
                    boolean isFirst = true;
                    for (String name : paramList.keySet()) {
                        for (String value : paramList.get(name)) {
                            if (isFirst) {
                                isFirst = false;
                            }
                            else {
                                postParamString.append('&');
                            }
                            postParamString.append(name).append('=').append(URLEncoder.encode(value, charSet));
                        }
                    }
                    byte[] paramBytes = postParamString.toString().getBytes(charSet);
                    ctx.setRequest(new HttpServletRequestWrapper(ctx.getRequest()){
                        @Override
                        public ServletInputStream getInputStream() throws IOException{
                            return new ServletInputStreamWrapper(paramBytes);
                        }
    
                        @Override
                        public int getContentLength(){
                            return paramBytes.length;
                        }
    
                        @Override
                        public long getContentLengthLong(){
                            return paramBytes.length;
                        }
                    });
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    返回页面的乱码问题

    在yml的配置文件中增加:

    spring:
      http:
        encoding:
          charset: UTF-8
          enabled: true
          force: true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    或者每一个下发的时候,增加:

    ctx.getResponse().setCharacterEncoding("UTF-8");
    
    • 1

    参数拦截样例代码

    package com.qfkj.setting;
    
    import com.alibaba.fastjson.JSON;
    import com.netflix.zuul.ZuulFilter;
    import com.netflix.zuul.context.RequestContext;
    import com.netflix.zuul.exception.ZuulException;
    import com.qfkj.base.constants.GWConst;
    import com.qfkj.util.RedisUtil;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletRequest;
    import java.util.HashMap;
    import java.util.Map;
    
    @Component
    @Slf4j
    public class LoginCheck extends ZuulFilter {
        @Value("service.base.copyrightEn")
        private String copyrightEn;
    
        @Value("service.base.profileActive")
        private String profileActive;
    
        @Value("${service.mode.check}")
        private boolean isCheck;
    
        @Value("${service.mode.defalutToken}")
        private String defalutToken;
    
        @Value("${service.mode.defalutRbacUserUUID}")
        private String defalutRbacUserUUID;
    
        @Value("${service.sigprefix}")
        private String sigprefix;
    
        @Value("${service.cookiesName}")
        private String cookiesName;
    
        @Value("${service.tokenRedisKey}")
        private String tokenRedisKey;
    
        @Autowired
        private RedisUtil redisUtil;
    
    
        @Override
        public String filterType() {
            return FilterConstants.PRE_TYPE;
        }
    
        @Override
        public int filterOrder() {
            return 0;
        }
    
        @Override
        public boolean shouldFilter() {
            RequestContext ctx = RequestContext.getCurrentContext();
            HttpServletRequest request = ctx.getRequest();
            String url = request.getRequestURI();
    
            if (url.indexOf("swagger") > 0) {
                log.info(String.format("shouldFilter(%s)->不用校验",url));
                return false;
            }
            if (url.indexOf("webjars") > 0) {
                log.info(String.format("shouldFilter(%s)->不用校验",url));
                return false;
            }
            if (url.indexOf("/csrf") > 0) {
                log.info(String.format("shouldFilter(%s)->不用校验",url));
                return false;
            }
            if (url.indexOf("/api-docs") > 0) {
                log.info(String.format("shouldFilter(%s)->不用校验",url));
                return false;
            }
            if (url.lastIndexOf("/") == url.length()-1) {
                log.info(String.format("shouldFilter(%s)->不用校验",url));
                return false;
            }
            log.info(String.format("shouldFilter(%s)->开始校验",url));
            return true;
        }
    
        @Override
        public Object run() throws ZuulException {
            RequestContext ctx = RequestContext.getCurrentContext();
            HttpServletRequest request = ctx.getRequest();
            String url = request.getRequestURI();
            log.info(String.format("LoginCheck(%s)->开始校验",url));
            //判断当前的系统模式,是否是全场模式
            String token = this.defalutToken;
            String rbacUserUUID = this.defalutRbacUserUUID;
            if (this.isCheck) {
                //获取cookies
                String cookiesname = this.sigprefix + '-' + this.profileActive + '-' + this.cookiesName;
                Cookie[] cookies = request.getCookies();
                if (null != cookies && cookies.length > 0) {
                    for (Cookie cookie : cookies){
                        if (cookiesname.equals(cookie.getName())) {
                            token = cookie.getValue();
                        }
                    }
                }
    
                if (token!= null && !this.defalutToken.equals(token)) {
                    //从redis中获取token
                    String key = this.sigprefix + '-' + this.profileActive + '-' + this.tokenRedisKey + token;
                    rbacUserUUID = (String) this.redisUtil.get(key);
                }
                log.info(String.format("LoginCheck(%s)->token(%s),rbacuui(%s)",url,token,rbacUserUUID));
                if (token == null || this.defalutToken.equals(token) || defalutRbacUserUUID == null) {
                    //登录验证失败了
                    ctx.set(GWConst.FILTER_RESULT_NAME_LASTRESULT,false);
                    ctx.setSendZuulResponse(false);
                    ctx.set("sendForwardFilter.ran", true);
                    ctx.setResponseBody(JSON.toJSONString(GWConst.LOGINCHECK_FAIL_NOLOGIN));
                    log.info(String.format("LoginCheck(%s)->token(%s),rbacuui(%s)校验失败",url,token,rbacUserUUID));
                    return null;
                }
            }
            //其他情况下是成功的,需要进行转换
            ctx.addZuulRequestHeader(GWConst.FILTER_RESULT_NAME_HEAD_TOKEN, token);
            ctx.addZuulRequestHeader(GWConst.FILTER_RESULT_NAME_HEAD_RBACUSERUUID, rbacUserUUID);
            ctx.setSendZuulResponse(true);
            log.info(String.format("LoginCheck(%s)->token(%s),rbacuui(%s)校验成功",url,token,rbacUserUUID));
            return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136

    动态路由

    这个实际上是根据某些拦截的结果,动态的转到后面不同的服务的方案。
    比如,用户登录了,就转到user/login这个服务。否则就转到user/guest这个服务上。
    直接看代码吧:

    @Component
    public class ForwardFilter extends ZuulFilter{
        private Logger logger= LoggerFactory.getLogger(ForwardFilter.class);
    
        @Override
        public String filterType() {
            //注意,重要,重要,要动态修改路由,这个值必须是ROUTE_TYPE
            return FilterConstants.ROUTE_TYPE;
        }
        @Override
        public int filterOrder() {
            // filter执行顺序,通过数字指定 ,优先级为0,数字越大,优先级越低
            return 0;
        }
        @Override
        public boolean shouldFilter() {
            // 是否执行该过滤器,此处为true,说明需要过滤
            return true;
        }
        @Override
        public Object run() throws ZuulException {
            RequestContext ctx = RequestContext.getCurrentContext();
            HttpServletRequest request = ctx.getRequest();
            //获取请求的URI  //测试访问:http://localhost:6001/testforward/hello?name=zs&token=1
            String url=request.getRequestURI();//
            if(url.indexOf("testforward")>-1){
                try {
                    //[1]:设置RouteHost::说明Host我没试过,因为指定了host相当于违背了微服务了,负载均衡等要自己手动处理,一般我不这么干
                    URI uri1=new URI("http://127.0.0.1:8001/");
                    ctx.setRouteHost(uri1.toURL());
    				//[2]:设置URI
    				url=url.substring(url.indexOf("testforward")+12,url.length());
    				ctx.put(FilterConstants.REQUEST_URI_KEY,url);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    这里有一个很重要的代码:

    public String filterType() {
            //注意,重要,重要,要动态修改路由,这个值必须是ROUTE_TYPE
            return FilterConstants.ROUTE_TYPE;
        }
    
    • 1
    • 2
    • 3
    • 4

    config手动刷新路由配置

    这个部分参见Spring-Cloud-Config部分说明。
    这里注意的是,ruul的路由配置,可能自己一行实现了@ @RefreshScope注解,路由刷新,用curl -X POST http://localhost:4001/actuator/refresh访问一次即可

    Zuul中使用Controller

    一般而言,zuul中大部分都是路由配置服务,但也可以直接编写普通的Controller进行访问。Controller编写后,起Mapping可以直接生效,无需在进行路由映射配置。
    值得注意的时候,路由映射的优先级会大于Controller,换句话说,通过路由配置,可以直接将其结果转换拦截掉。

    Zuul中的Forward

    在zuul中可以配置forward的

    ### 网关配置
    zuul:
      # 路由信息配置
      routes:
        demo-local:
          # 访问的路径,此处要以 '/do/' 开头
          path: /local1/**
          # 访问的 url,forward:向本地转发
          url: forward:/local2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    而controller如下:

    /**
     * @Author:大漠知秋
     * @Description:网关本地的 Controller
     * @CreateDate:1:33 PM 2018/10/30
     */
    @RestController
    @RequestMapping(
            value = "/local2",
            produces = MediaType.APPLICATION_JSON_UTF8_VALUE
    )
    public class LocalController {
    
        @RequestMapping(value = "/testOne")
        public String testOne() {
    
            return "testOne";
    
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    如上,正常访问/local2/testOne来访问服务。
    但有上面的拦截和forward,那么,就可以通过/local1/testOne来访问

  • 相关阅读:
    《公共政策学》重点整理
    【数学建模】MATLAB应用实战系列(八十八)-组合权重法应用案例(附MATLAB和Python代码)
    记一次 .NET 差旅管理后台 CPU 爆高分析
    一个注解搞定SpringBoot接口定制属性加解密
    Unity Xlua热更新框架(二):构建AssetBundle
    设计模式-单例模式
    实践笔试题1
    Verilog基础:三段式状态机与输出寄存
    【问题处理】GIT合并解决冲突后,导致其他人代码遗失的排查
    【重装系统的血泪史
  • 原文地址:https://blog.csdn.net/wwwlgy/article/details/126661915