路由网关 zuul 的主要功能是进行路由转发和安全性校验

路由转发:

如果不懂什么叫路由转发,不知道 zuul 有什么作用,那么往下看你会知道答案。


下面看两个路由转发的简单例子:

  • 将 http://127.0.0.1:8888/api/blog/22 转发到 http://williamszhang.top/blog/22
  • 将 http://127.0.0.1:8888/api/hello 转发到 spring-cloud-provider 的 hello 服务上

gateway-service-zuul-simple:

  1. pom.xml 中添加依赖:

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    </dependency>
    

  2. 启动类中添加@EnableZuulProxy注解,表示对网关路由的支持:

    @EnableZuulProxy
    @SpringBootApplication
    public class GatewayServiceZuulApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(GatewayServiceZuulApplication.class, args);
        }
    
    }
    

  3. 配置文件 application.properties:

    spring.application.name=gateway-service-zuul-simple
    server.port=8888
    
    zuul.routes.api.path=/api/**
    zuul.routes.api.url=http://williamszhang.top/
    

    这里的配置表示访问 http://127.0.0.1:8888/api/** 直接重定向到 http://williamszhang.top/**


启动 gateway-service-zuul-simple 项目访问 http://127.0.0.1:8888/api/blog/22 会看到 http://williamszhang.top/blog/22 页面。


gateway-service-zuul-eureka:

  1. pom.xml 中添加依赖如上所示。


  2. 启动类中添加 @EnableZuulProxy 注解和 @EnableDiscoveryClient 注解。

    @EnableDiscoveryClient 注解用于将此服务注册到注册中心。

    @EnableDiscoveryClient
    @EnableZuulProxy
    @SpringBootApplication
    public class GatewayServiceZuulEurekaApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(GatewayServiceZuulEurekaApplication.class, args);
        }
    
    }
    

  3. 配置文件 application.properties:

    spring.application.name=gateway-service-zuul-eureka
    server.port=8889
    zuul.routes.api.path=/api/**
    zuul.routes.api.service-id=spring-cloud-producer
    eureka.client.serviceUrl.defaultZone=http://localhost:8000/eureka/
    

    zuul.routes.api.service-id 表示路由将转发到此服务上,因此访问 http://127.0.0.1:8889/api/hello?name=neo 将直接重定向到 spring-cloud-provider 的 hello 服务上,即跟访问 http://127.0.0.1:9000/hello?name=neo 所见的页面一样。(注:基于前一篇文章的例子,9000端口为 spring-cloud-provider 服务的端口)


  4. 其实网关已经做了默认配置,只要是被注册到注册中心的微服务,那么 zuul 就会自动代理,直接用服务的 spring.application.name 请求即可。

    即配置文件 application.properties 注释掉关于路由的配置:

    spring.application.name=gateway-service-zuul-eureka
    server.port=8889
    #zuul.routes.api.path=/api/**
    #zuul.routes.api.service-id=spring-cloud-producer
    eureka.client.serviceUrl.defaultZone=http://localhost:8000/eureka/
    

    请求地址变为:http://127.0.0.1:8889/spring-cloud-producer/hello?name=neo


模拟集群:

  1. 复制一个 spring-cloud-producer 项目改名叫 spring-cloud-producer-2

  2. 把 HelloController 里的 first 改成 two:

    @RestController
    public class HelloController {
    
        @RequestMapping("/hello")
        public String index(@RequestParam String name){
            return "hello "+name+",this is two message";
        }
    }
    

  3. 把 application.properties 端口号改成 9002,name 的名字不变:

    spring.application.name=spring-cloud-producer
    server.port=9002
    eureka.client.serviceUrl.defaultZone=http://localhost:8000/eureka/
    

    进 http://localhost:8000 注册中心查看可以看到有两个服务提供者。


现在进行测试,访问 http://127.0.0.1:8889/api/hello?name=neo 不停刷新,可以看到 hello neo,this is first messagehello neo,this is two message 不停变动。

由此就可以比较通俗的解释 zuul 的作用了:

在实际的项目中,一个项目可能会包含很多个服务,每个服务的端口和 IP 都可能不一样。例如我们这儿如果你想要调用 spring-cloud-producer 服务,那么需要访问 http://127.0.0.1:9000/hello?name=neo ,而如果你想要访问 spring-cloud-producer-2 服务,需访问 http://127.0.0.1:9002/hello?name=neo 。一个9000端口,一个9002端口。

那么,如果我们以这种形式提供接口给外部调用,代价是非常大的。从安全性上考虑,系统对外提供的接口应该进行合法性校验,防止非法请求,如果按照这种形式,那每个服务都要写一遍校验规则,维护起来也很麻烦。 这个时候,我们需要统一的入口,接口地址全部由该入口进入,而服务只部署在局域网内供这个统一的入口调用,这就是 zuul 的作用所在了。

当然这种多个不同的地址配置相同的名字,最终查询这个名字的客户机解析这个名字时得到其中一个地址,这也是负载均衡常用的技术。


因为这是用负载均衡演示的可能不太直观,下面换一个方式:

  1. 修改 spring-cloud-producer 中的 HelloController:

    @RequestMapping("/hello")
    @RestController
    public class HelloController {
    
        @RequestMapping("/getFirst")
        public String index(@RequestParam String name){
            return "hello "+name+",this is first message";
        }
    }
    

  2. 修改 spring-cloud-producer-2 中的 HelloController:

    @RequestMapping("/hello")
    @RestController
    public class HelloController {
    
        @RequestMapping("/getSecond")
        public String index(@RequestParam String name){
            return "hello "+name+",this is two message";
        }
    }
    

    那么都可以用一个端口号 8889 来访问,即提供了统一的入口:

    http://127.0.0.1:8889/api/hello/getFirst?name=neo 得到 hello neo,this is first message

    http://127.0.0.1:8889/api/hello/getSecond?name=neo 得到 hello neo,this is second message

过滤器:

gateway-service-zuul-eureka:

  1. zuul 通过继承过滤器 ZuulFilter 进行处理:

    public class ApiFilter extends ZuulFilter {
        @Override
        public String filterType() {
            return "pre";
        }
    
        @Override
        public int filterOrder() {
            return 0;
        }
    
        @Override
        public boolean shouldFilter() {
            return true;
        }
    
        @Override
        public Object run() throws ZuulException {
            return null;
        }
    }
    
    • filterType 为过滤类型,可选值有 pre(路由之前)、routing(路由之时)、post(路由之后)、error(发生错误时调用)。
    • filterOrder 为过滤的顺序,如果有多个过滤器,则数字越小越先执行。
    • shouldFilter 表示是否过滤,这里可以做逻辑判断,true 为过滤,false 不过滤。
    • run 为过滤器执行的具体逻辑,在这里可以做很多事情,比如:权限判断、合法性校验等。

  2. 下面进行一个简单的安全校验:

     @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
    
        String token = request.getParameter("token");// 获取请求的参数
    
        if (!"token".equals(token)) {
            context.setSendZuulResponse(false); //对请求进行路由
            context.setResponseStatusCode(500);
            try {
                context.getResponse().getWriter().write("token is invalid");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
    

下面进行一下测试:访问 http://127.0.0.1:8889/api/hello/getFirst?name=neo 得到 token is invalid

访问 http://127.0.0.1:8889/api/hello/getFirst?name=neo&token=token 得到 hello neo,this is first message