路由网关——Zuul

路由网关——Zuul

Springloud中API网关是Zuul。对于网关而言,存在两个作用:第一个作用将请求的地址映射为真实服务器的地址。

例如,用户请求http://localhost/user/1获取用户id为1的信息,而真实的服务是http://localhost:800l/user/I和http://localhost:8002/user/1都可以获取用户的信息,这时就可以通过网关使得localhost/user映射为对应真实服务器的地址。

然这个作用就起到路由分发的作用,从而降低单个节点的负载。从这点来说,可以把称为服务端负载均衡。从高可用的角度来说,则一个请求地址可以映射到多台服务上,如果单点出现障,则其他节点也能提供服务,这样这就是一个高可用的服务了。

Zuul网关的第二个作用是过滤服务,在互联网中,服务器可能面临各种攻击,Zuul提供了过滤器,通过它过那些恶意或者无效的请求,把它们排除在服务网站之外,这样就可以降低网站服务的风险。

构建Zuul网关

首先,有引入依赖是必要的:

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

接着就是在主要执行的程序上加一个注解:@EnableZuulProxy。

这个注解会启动Zuul网关。

我们来看一看这个注解的构成吧:

1
2
3
4
5
6
@EnableCircuitBreaker
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({ZuulProxyMarkerConfiguration.class})
public @interface EnableZuulProxy {
}

这么看其实有点不清晰,其实以下的注解包含了两种类型的过滤器。

pre类型过滤器

PreDecorationFilter:该过滤器根据提供的RouteLocator确定路由到的地址,以及怎样去路由。该路由器也可为后端请求设置各种代理相关的header。

route类型过滤器

(1) RibbonRoutingFilter:该过滤器使用Ribbon,Hystrix和可插拔的HTTP客户端发送请求。serviceId在RequestContext.getCurrentContext().get(“serviceId”)中。该过滤器可使用不同的HTTP客户端,例如

Apache HttpClient:默认的HTTP客户端

SquareupOkHttpClient v3:如需使用该客户端,需保证com.squareup.okhttp3的依赖在classpath中,并设置ribbon.okhttp.enabled = true。

Netflix Ribbon HTTP client:设置ribbon.restclient.enabled = true即可启用该HTTP客户端。需要注意的是,该客户端有一定限制,例如不支持PATCH方法,另外,它有内置的重试机制。

(2) SimpleHostRoutingFilter:该过滤器通过Apache HttpClient向指定的URL发送请求。URL在RequestContext.getRouteHost()中。

并且,他已经引入了断路机制。之所以引入断路机制,是因为在请求不到的时候会进行断路,以避免网关发生球无法释放的场景,导致微服务瘫痪。

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server:
port: 80

eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka/,http://localhost:7002/eureka/
instance:
prefer-ip-address: true
spring:
application:
name: zuul
zuul:
routes:
user-service:
path: /u/**
url: http://localhsot:9002/
product-service:
path: /p/**
serviceId: product

这个配置是将Zuul网关注册给服务治理中心,这样它就能够获取各个微服务的服务ID了看到用户微服务映射的配置,这里采用 zuul.routes..path 和zuul.routes.<key>url 配置,其中path是请求路径,这里使用了ANT风格的通配 “/u/**” ,而旧l代表转发地址,也就是满足path通配的时候,请求就会转发给端口为9002的用户微服务。

但是,这样配置有一个弊端,因为我们的用户微服务有两个节点,一个是9002端口,另一个是9003端口,这里只能映射到9002端口的微服务,而映射不到9003端口的微服务。

为了解决这个问题,Zuul还提供了面向服务的配置。再看到代码中的产品微服务映射配置,这里使用了zuul.routes..path和zuul.routes..serviceld进行配置,其中path的配置可参考用户微服务,而serviceld这里配置为“product”,这是一个产品微服务的名称,由产品微服务的属性spring.application.name配置。

这样配置后,Zuul会自动实现负载均衡,也就是会将请求转发到某个服务的节点上。

这里可以打开:http://localhost/p/product/ribbon。查看结果。

使用过滤器

上面只是将请求转发到具体的服务器或者具体的微服务上,但是有时候还希望网关功能更强大一些。

例如,监测用户登录、黑名单用户、购物验证码、恶意刷请求攻击等场景。如果这些在过滤器内判断失,那么就不再把请求转发到其他微服务上,以保护微服务的稳定。下面模拟这样的一个场景。

假设当前需要提交一个表单,而每一个表单都存在一个序列号(seria!Number),并且这个序列对应个验证码(verficationCode),在提交表单的时候,这两个参数都会一并提交到Zuul网关。

对于Redis服务器会以序列(seriaNumber)为键(key),而以验证码(verificationCode)为值(value)进行存储。

当路由网关过滤器判用户提交的验证码与Redis服务器保存不一致的时候,则不再转发请求到微服务。

测试过滤器

这里验证码使用Redis进行存储,所以会比使用数据库快得多,这有助于性能的提高,避免造成瓶颈。由于使用了Redis,因此需要在Zuul网关中引入Redis的依赖:

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
server:
port: 80

eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka/,http://localhost:7002/eureka/
instance:
prefer-ip-address: true
spring:
application:
name: zuul
redis:
jedis:
pool:
min-idle: 5
max-active: 10
max-idle: 10
max-wait: 2000
port: 6379
host: localhost
timeout: 1000
zuul:
routes:
user-service:
path: /u/**
url: http://localhsot:9002/
product-service:
path: /p/**
serviceId: product

Zuul存在一个抽象类,那就是ZuulFilter。

这里需要注意的是,只是画出了抽象类ZuulFilter自定义的抽象方法和接口ZuulFilter定义的抽类,也就是说,当需要定义一个非抽象的Zuul过滤器的时候,需要实现这4个抽象方法,在重写这4个抽象方法前,我们很有必要知道它们的作用。

  1. shouldFilter:返回boolean值,如果为true,则执行这个过滤器的run方法。

  2. run:运行过滤逻辑,这是过滤器的核心方法。

  3. filterType:过滤器类型,它是一字符串,可以配置为以下4种。

    pre:请求行之前filter。

    route:处理请求,进行路由。

    post:请求处理完成后执行的filter。

    error:出现错误时执行的filter。

  4. filterOrder:指定过滤器顺序,值越小则越优先。

使用过滤器判定验证码

我们可以在Reddit中生成一个验证码。当用户提交的验证码与Redis不一致时,就会被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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Component
@Qualifier
public class MyZuulFilter extends ZuulFilter {

// 注入StringRedisTemplate
@Autowired
private StringRedisTemplate residTemplate = null;

// 是否过滤
@Override
public boolean shouldFilter() {
// 请求上下文
RequestContext ctx = RequestContext.getCurrentContext();
// 获取HttpServletRequest对象
HttpServletRequest req = ctx.getRequest();
// 取出表单序列号
String serialNumber = req.getParameter("serialNumber");
// 如果存在验证码返回为true,启用过滤器
return !StringUtils.isEmpty(serialNumber);
}

// 过滤器逻辑方法
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest req = ctx.getRequest();
// 取出表单序列号和请求验证码
String serialNumber = req.getParameter("serialNumber");
String reqCode = req.getParameter("verificationCode");
// 从Redis中取出验证码
String verifCode = residTemplate.opsForValue().get(serialNumber);
// Redis验证码为空或者与请求不一致,拦截请求报出错误
if (verifCode == null || !verifCode.equals(reqCode)) {
// 不再转发请求
ctx.setSendZuulResponse(false);
// 设置HTTP响应码为401(未授权)
ctx.setResponseStatusCode(401);
// 设置响应类型为JSON数据集
ctx.getResponse().setContentType(MediaType.APPLICATION_JSON_UTF8.getType());
// 设置响应体
ctx.setResponseBody("{'success': false, " + "'message':'Verification Code Error'}");
}
// 一致放过
return null;
}

// 过滤器类型为请求前
@Override
public String filterType() {
return "pre";
}

// 过滤器排序,数字越小优先级越高
@Override
public int filterOrder() {
return 0;
}
}

述代码中在类上标注了@Component,这样Spring就会扫描它,将其装配到IoC容器中。为继承了抽象类ZuulFilter,所以Zuul自动将它识别为过滤器。filterType方法返回了“pre,则过滤器会在路由之前执行。

filterOrder返回为0,这个方法在指定多个过滤器顺序才有意义,数字越小,则越优先,这里只有一个过滤器,所以就返回0。

然后在shouldFilter中判断是否存在序列号参数,如果存在,则返回住时,这就意味着将启用这个过滤器,否则就不再启用这个过滤器。run方法是过滤器的核心,它首先获取请求中的序列号和验证码,跟着使用StringRedisTemplate通过序列号获取Redis服务器上的验证码。

然后将Redis的验证码与请求的验证码进行比较,如果匹配不一致,则设置再转发请求到微服系统,并且将晌应码设置为401,响应类型为JSON数据集,最后还会设置响应体的内容;如果一致,则返回null,放行服务。

这样用户提交的验证码与Redis保存的不一致时,请求就会在Zuul网关中被过滤器拦截,而不会转发到微服务中

总结

这里可以举一个例子:用户服务提供一个登录接口,用户名密码正确后返回一个Token,此Token作为用户服务的通行证,那么用户登录成功后返回的Token就需要进行加密或者防止篡改处理。在到达用户服务其他接口前,就需要对Token进行校验,非法的Token就不需要转发到用户服务中了,直接在网关层返回信息即可。

那么,我们再来看一张图:

上图显示这几种过滤器的前后调用顺序,第一个是pre过滤器然后是Route过滤器,最后响应是post过滤器。

这样