1、服务网关
服务网关是在微服务前边设置的一道屏障,请求先到服务网关,网关会对请求进行过滤、校验、路由等处理。有了服务网关可以提高微服务的安全性,网关校验请求的合法性,请求不合法将被拦截,拒绝访问。
简单来说,服务网关就是架设在前端和后端服务之间的一道关卡,提供了访问后端服务的唯一入口,前端的所有请求必须先经过服务网关过滤,然后过滤后的合法请求将会由服务网关路由到指定的微服务。
2、使用服务网关的理由
如果不使用服务网关,如下图:
所有请求由Nginx实现代理转发,如果微服务A、B、C、D都接入了SpringSecurity,那么所有请求都需要经过安全认证,由各个微服务自己来校验请求是否合法,这样其实挺麻烦的, 可以在微服务之前架设一个网关服务,由网关服务统一对所有请求进行校验,合法的请求再由网关服务路由到指定微服务,不合法的请求直接过滤掉,不会到达微服务,这样做的好处是缓解了下面各个服务端的压力,让各个服务专注于自己的核心业务,不用花费大量的资源在验证请求上。如下图:
加了服务网关之后,只有合法的请求才会到达服务端,不合法的请求由网关统一过滤拦截,不会到达。
3、Zuul
Zuul 是从设备和网站到应用程序后端的所有请求的前门。作为边缘服务应用程序,Zuul 旨在实现动态路由,监视,弹性和安全性。Zuul 包含了对请求的路由和过滤两个最主要的功能。
Zuul 是 Netflix 开源的微服务网关,它可以和 Eureka、Ribbon、Hystrix 等组件配合使用。Zuul 的核心是一系列的过滤器,这些过滤器可以完成以下功能:
- 身份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求。
- 审查与监控:在边缘位置追踪有意义的数据和统计结果,从而带来精确的生产试图。
- 动态路由:动态地将请求路由到不同的后端集群。
- 压力测试:逐渐增加只想集群的流量,以了解性能。
- 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求。
- 静态响应处理:在边缘位置直接建立部份响应,从而避免其转发到内部集群。
- 多区域弹性:跨越AWS Region进行请求路由,旨在实现ELB(Elastic Load Balancing)使用的多样化,以及让系统的边缘更贴近系统的使用者。
Zuul主要是对请求进行过滤和路由的。Zuul与Nginx在实际项目中需要配合使用,Nginx的作用是反向代理、负载均衡,Zuul的作用是保障微服务的安全访问,拦截微服务请求,校验合法性及负载均衡。如下图:
下面进行简单的测试。
3.1、搭建Zuul网关服务
(1)创建maven工程
就一个filter包。
(2)pom依赖
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
<relativePath />
</parent>
<groupId>com.ycz</groupId>
<artifactId>zuul-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>zuul-govern-gatewy</name>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
</properties>
<dependencies>
<!-- Zuul依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- eureka依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- web模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<!-- 引入spring cloud -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(3)application.yml配置
server:
port: 6000
servlet:
context-path: /api
spring:
application:
name: zuul-demo
## eureka配置
eureka:
client:
register-with-eureka: true ## 打开服务注册开关
fetch-registry: true ## 打开服务发现开关
service-url:
defaultZone: http://localhost:20000/eureka/
instance:
prefer-ip-address: true ## 将自己的IP地址注册到eureka服务中
ip-address: 127.0.0.1
instance-id: ${spring.application.name}:${server.port} ## 实例名称
## zuul配置
zuul:
routes:
app1:
path: /app1/** ## 所有以app1开头的请求会路由到实例app1
service-id: app1 ## 注册到eureka服务中心的名称为app1的实例
strip-prefix: false ## 转发时是否忽略前缀
sensitive-headers: ##是否取消黑名单
app2:
path: /app2/**
service-id: app2
strip-prefix: false
sensitive-headers:
(4)启动类
@SpringBootApplication
//表示这是一个网关代理服务
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class,args);
}
}
注意@EnableZuulProxy是一个组合注解,已经包含了eureka客户端的注册注解。
(5)自定义过滤器
package com.ycz.zuul.gateway.filter;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
/*
* 自定义过滤器
* 需要继承ZuulFilter
*/
@Component
public class MyFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
// 返回true表示该过滤器需要执行
return true;
}
// 过滤的核心代码在这个方法里写
@Override
public Object run() throws ZuulException {
// 获取上下文对象
RequestContext requestContext = RequestContext.getCurrentContext();
// 获取请求对象
HttpServletRequest request = requestContext.getRequest();
System.out.println(request.getRequestURI().toString());
// 取出头信息中的token
String token = request.getHeader("Token");
if (token == null || !token.equals("ycz123456")) {
// 拒绝访问
access_denied();
return null;
}
return null;
}
@Override
public String filterType() {
// 请求在被路由之前执行
return "pre";
}
@Override
public int filterOrder() {
// 数值表示过滤器的执行顺序,越小优先级越高
return 0;
}
// 定义一个私有方法,拒绝访问
private void access_denied() {
// 获取上下文对象
RequestContext requestContext = RequestContext.getCurrentContext();
// 获取响应对象
HttpServletResponse response = requestContext.getResponse();
response.setStatus(300);
response.setHeader("Content-Type", "application/json;charset=utf-8");
PrintWriter writer;
try {
writer = response.getWriter();
writer.write("{\"status\":\"error\",\"msg\":\"没有权限,请求已被网关拦截!\"}");
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
对以上4个重写方法的说明:
- shouldFilter:返回一个Boolean值,判断该过滤器是否需要执行。返回true表示要执行此过滤器,否则不执行。
- run:过滤器的核心业务逻辑。
- filterType:返回字符串代表过滤器的类型,pre表示请求在被路由之前执行,routing表示在路由请求时执行,post表示在routing和errror过滤器之后调用,error表示处理请求时发生错误调用。
- filterOrder:此方法返回整型数值,通过此数值来定义过滤器的执行顺序,数字越小优先级越高。
以上一个简单的zuul网关工程就搭建完成了,最好先测试一下,看能不能跑得起来,我已经测过没问题,可以运行,运行之后控制台一直报错,这是因为eureka服务中心还没搭建,这个报错并不影响。
3.2、搭建eureka服务中心
Zuul一般会和eureka等组件配合使用,下面搭建eureka的服务中心。
(1)创建maven工程
这个工程基本是空的,只有一个启动类。
(2)pom依赖
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
<relativePath />
</parent>
<groupId>com.ycz</groupId>
<artifactId>eureka-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
</properties>
<dependencies>
<!-- eureka依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(3)application.yml配置
server:
port: 20000
spring:
application:
name: eureka-server
## eureka配置
eureka:
## 客户端
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:20000/eureka/
## 服务端
server:
enable-self-preservation: false
eviction-interval-timer-in-ms: 600000
## 实例地址
instance:
hostname: 127.0.0.1
(4)启动类
@SpringBootApplication
//Eureka服务中心
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class,args);
}
}
同样启动工程测试一下能不能跑起来,这边启动后再启动zuul服务,那边就不会报错了。
3.3、搭建资源服务
下面搭建两个简单的资源服务。
(1)创建maven工程
(2)pom依赖
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath />
</parent>
<groupId>com.ycz</groupId>
<artifactId>app1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
</properties>
<dependencies>
<!-- eureka客户端依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- web模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(3)application.yml配置
server:
port: 6010
spring:
application:
name: app1
## eureka配置
eureka:
client:
register-with-eureka: true ## 打开服务注册开关
fetch-registry: true ## 打开服务发现开关
service-url:
defaultZone: http://localhost:20000/eureka/
instance:
prefer-ip-address: true ## 将自己的IP地址注册到eureka服务中
ip-address: 127.0.0.1
instance-id: ${spring.application.name}:${server.port} ## 实例名称
(4)启动类
@SpringBootApplication
@EnableDiscoveryClient//eureka客户端
public class App1Application {
public static void main(String[] args) {
SpringApplication.run(App1Application.class, args);
}
}
(5)控制器
@RestController
@RequestMapping("/app1")
public class App1Controller {
@RequestMapping("/t1")
public String t1() {
return "成功访问到资源服务A的资源";
}
}
就通过这个请求来获取资源服务的资源。
复制一份app1工程,重命名为app2,修改application.yml中的端口和名称:
再修改一下控制器里的内容:
@RestController
@RequestMapping("/app2")
public class App2Controller {
@RequestMapping("/t2")
public String t1() {
return "成功访问到资源服务B的资源";
}
}
那么到这里就搭建好了4个微服务。
3.4、配置Nginx代理
因为要通过Nginx来访问,所以配置一下代理设置:
将www.ycz.com/api开头的请求全部转发到网关代理服务,由网关处理请求,决定请求是转发到指定微服务还是直接拦截。配制完毕,启动Nginx。
3.5、测试
启动eureka-server、zuul-demo、app1、app2这四个工程。
先看eureka服务中心,访问20000端口:
其他3个微服务成功注册到了eureka服务中心。
然后访问:http://www.ycz.com/api/app1/t1
打开浏览器的调试工具查看请求的header:
这个300状态码是我在zuul过滤器里面定义的。
这个请求的header里面并没有带Token令牌,所以这个请求被过滤器过滤掉了,Zuul网关不会将请求往下发,这个请求在Zuul这里就死掉了。
说明Zuul网关服务的过滤器的确起了作用。
现在我关掉浏览器,用postman测试。
header现在是完全空的,发送请求,响应如下:
没带Token,请求被网关过滤掉了。
然后在header里面带上Token,但value值配成错误的,再发请求:
Token值不匹配,请求被网关过滤掉了。
现在换成正确的Token值,再发请求:
网关验证通过,将请求路由给了app1服务,成功获取到了资源。
再请求其他服务的资源:
网关验证通过,将请求路由给了app2服务,成功获取到了资源。
4、总结
通过以上的例子可以看到,网关可以起到验证权限的作用,常用的是Zuul网关,如果资源服务接入了Security,那么可以配一个Zuul网关服务,来对所有的请求进行过滤,验证是否有权限访问资源服务的资源,比如是否带JWT令牌,JWT令牌是否正确,令牌是否过期等,这些都可以在网关服务的过滤器中进行验证,验证通过后,网关服务将请求路由给指定微服务,这时直接获取资源就行了,不用再进行验证。使用服务网关,可以保证后端服务的安全,也可以缓解后端服务的资源压力。