Spring Cloud Zuul 服务网关

Spring Cloud Zuul

在微服务架构中,后端服务往往不直接开放给调用端,而是通过一个API网关根据请求的url,路由到相应的服务。 当添加API网关后,在第三方调用端和服务提供方之间就创建了一面墙,这面墙直接与调用方通信进行权限控制,后将请求均衡分发给后台服务端。 Spring Cloud Zuul 是一个基于JVM路由和服务端的负载均衡器,提供动态路由,监控,弹性,安全等的边缘服务。

简介

Zuul 的主要功能是路由转发和过滤器(Filter)。不同类型的 Filter 用于处理请求,可以实现以下功能:

  • 权限控制和安全性:可以识别认证需要的信息和拒绝不满足条件的请求
  • 监控:监控请求信息
  • 动态路由:根据需要动态地路由请求到后台的不同服务集群
  • 压力测试:逐渐增大到集群的流量,以便进行性能评估
  • 负载均衡:为每种类型的请求分配容量并丢弃超过限额的请求
  • 限流
  • 黑白名单过滤
  • 静态资源处理:直接在zuul处理静态资源的响应而不需要转发这些请求到内部集群中

基础使用

创建一个 api-gateway

添加依赖

1
2
3
4
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>

添加注解

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

配置文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
spring:
  application:
    name: api-gateway

server:
  port: 9000

# 自定义路由规则
zuul:
  routes:
    user:
      path: /user/**
      serviceId: user 

测试

通过以上配置文件配置,即可通过 api-gateway 服务去请求 user 服务。

假设 user 服务的端口为 8080,其中包含一个 api 为 /info。 通过访问 http://localhost:9000/user/info,即可访问该 api。(若原接口包含路由前缀 /user,需要使用 /user/user/info 访问)

常用功能

统一前缀

1
2
zuul:
  prefix: /proxy

Header 过滤及重定向添加 Host

1
2
3
4
5
6
7
8
9
zuul:
  # 默认为该配置,会过滤 Cookie Set-Cookie Authorization 信息
  # 设置为空即不会过滤
  sensitive-headers: Cookie,Set-Cookie,Authorization

--- 

zuul:
  add-host-header: true  # 重定向会添加 host 请求头

查看路由信息

添加依赖

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

配置文件

1
2
3
4
5
6
7
8
server:
  port: 9000

management:
  endpoints:
    web:
      exposure:
        include: 'routes'

访问接口

  • 访问 http://localhost:9000/actuator/routes 获取信息
  • 访问 http://localhost:9000/actuator/routes/details 获取详细信息

Zuul 应用

Zuul Filter

Filter是Zuul的核心,用来实现对外服务的控制。Filter有4个生命周期。

  • pre
    • 在请求被路由到目标服务前执行。
    • 比如权限校验、打印日志等功能。
  • routing
    • 在请求被路由到目标服务时执行。
    • 用于构建发送给微服务的请求,并使用 Apache HttpClient 或 Netfilx Ribbon 请求微服务。
  • post
    • 这种过滤器在路由到微服务以后执行。
    • 为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
  • error
    • 其他阶段发生错误时执行该过滤器。

zuul-core

自定义 Filter

实现自定义 Filter,需继承 com.netflix.zuul.ZuulFilter,并覆盖继承的方法。

 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
@Component
public class MyFilter extends ZuulFilter {
    @Override
    String filterType() {
        // 定义filter的类型,pre、route、post、error
        return null;
    }

    @Override
    int filterOrder() {
        // 定义filter的顺序,数字越小表示顺序越高,越先执行
        return 0;
    }

    @Override
    boolean shouldFilter() {
        // 是否需要执行该filter,true表示执行,false表示不执行
        return false;
    }

    @Override
    Object run() {
        // filter需要执行的具体操作
        return null;
    }
}

Zuul 限流

限流在前置过滤器(pre)前使用,在请求被转发前调用,且优先级最高。

令牌桶限流示例。令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输。

令牌桶算法会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

token-bucket

 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
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;

@Component
public class RateLimiterFilter extends ZuulFilter {
    // 直接使用 guava 中的 RateLimiter 实现
    private static final RateLimiter RATE_LIMITER = RateLimiter.create(100);

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        // 限流是最高优先级,所以比最高优先级 -3 还要小
        return FilterConstants.SERVLET_DETECTION_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        // 如果没有拿到令牌
        if (!RATE_LIMITER.tryAcquire()) {
            throw new RuntimeException();
        }

        return null;
    }
}

Zuul 鉴权

在请求服务前,判断是否有权限访问。

 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
@Component
public class TokenFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        // 越小越靠前,放在 PRE_DECORATION_FILTER_ORDER 之前
        return PRE_DECORATION_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    // 需要定义的逻辑
    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        // 需要根据实际情况,从 header / cookie 中获取信息
        String token = ...;
        // 根据实际情况进行校验
        if (...) {
            // 首先设置 zuul
            requestContext.setSendZuulResponse(false);
            // 设置返回信息
            requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
        }

        return null;
    }
}

Zuul 跨域

在浏览器中的 ajax 请求是有同源策略的,如果违反了同源策略,就会有跨域问题。在 Zuul 中添加 CorsFilter 过滤器,是跨域问题的一种解决方案。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Configuration
public class CorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        final UrlBaseCorsConfigurationSource source = new UrlBaseCorsConfigrationSOurce();
        final CorsConfiguration config = new CorsConfiguration();

        config.setAllowCredentials(true);
        config.setAllowedOrigins(Collections.singletonList("*"));
        config.setAllowedHeaders(Collections.singletonList("*"))
        config.setAllowedMethods(Collections.singletonList("*"));
        config.setMaxAge(300L);

        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}