第06课:服务网关

本文,我们将学习 Spring Cloud的另一个组件:zuul,它提供微服务的网关功能,即中转站,通过它提供的接口,可以转发不同的服务。在学习 zuul 之前,我们先接着上一篇的代码,来看看服务提供者是如何提供服务的。

在服务提供者的 module 下创建 HelloController 类,添加内容如下:

@RestController
public class HelloController {

    @RequestMapping("index")
    public String index(){
        return "Hello World!";
    }
}

然后分别启动服务注册中心和服务提供者,浏览器输入:http://localhost:8762/index,即可看见如下画面:

1-16

在实际的项目中,一个项目可能会包含很多个服务,每个服务的端口和 IP 都可能不一样。那么,如果我们以这种形式提供接口给外部调用,代价是非常大的。从安全性上考虑,系统对外提供的接口应该进行合法性校验,防止非法请求,如果按照这种形式,那每个服务都要写一遍校验规则,维护起来也很麻烦。

这个时候,我们需要统一的入口,接口地址全部由该入口进入,而服务只部署在局域网内供这个统一的入口调用,这个入口就是我们通常说的服务网关。

Spring Cloud 给我们提供了这样一个解决方案,那就是 zuul,它的作用就是进行路由转发、异常处理和过滤拦截。下面,我将演示如果使用 zuul 创建一个服务网关。

创建 gateway 工程

在父项目上右键 -> New -> Module,创建一个名为 gateway 的工程,在其 pom.xml 中,加入如下依赖:

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

创建 Application 启动类,并增加 @EnableZuulProxy 注解:

@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

最后添加 application.yml 配置文件,内容如下:

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8080
spring:
  application:
    name: gateway
zuul:
  routes:
    api:
      path: /api/**
      serviceId: eurekaclient

我们可以看到,服务网关的配置多了几项,具体含义如下。

zuul.routes.api.path:指定请求基础地址,其中 API 可以是任何字符。

serviceId:转发到的服务 ID,也就是指定服务的 application.name,上述实例的含义表示只要包含 /api/ 的地址,都自动转发到 eurekaclient 的服务去。

然后我们启动服务注册中心、服务提供者、服务网关,访问地址:http://localhost:8080/api/index,我们可以看到和之前的界面完全一样。其实只要引入了 zuul,它就会自动帮我们实现反向代理和负载均衡。配置文件中的地址转发其实就是一个反向代理,那它如何实现负载均衡呢?

我们修改服务提供者的 Controller 如下:

RestController
public class HelloController {

    @Value("${server.port}")
    private int port;

    @RequestMapping("index")
    public String index(){
        return "Hello World!,端口:"+port;
    }
}

重新启动。然后再修改服务提供者的端口为8673,再次启动它(切记:原先启动的不要停止),访问地址:http://localhost:8761,我们可以看到 eurekaclient 服务有两个地址:

2-12

再不断访问地址:http://localhost:8080/api/index,可以看到交替出现以下界面:

3-9

4-7

由此可以得出,当一个服务启动多个端口时,zuul 服务网关会依次请求不同端口,以达到负载均衡的目的。

服务拦截

前面我们提到,服务网关还有个作用就是接口的安全性校验,这个时候我们就需要通过 zuul 进行统一拦截,zuul 通过继承过滤器 ZuulFilter 进行处理,下面请看具体用法。

新建一个类 ApiFilter 并继承 ZuulFilter:

@Component
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() {
        //这里写校验代码
        return null;
    }
}

其中:

  • filterType 为过滤类型,可选值有 pre(路由之前)、routing(路由之时)、post(路由之后)、error(发生错误时调用)。
  • filterOrdery 为过滤的顺序,如果有多个过滤器,则数字越小越先执行
  • shouldFilter 表示是否过滤,这里可以做逻辑判断,true 为过滤,false 不过滤
  • run 为过滤器执行的具体逻辑,在这里可以做很多事情,比如:权限判断、合法性校验等。
  • 下面,我们来做一个简单的安全验证:
@Override
    public Object run() {
        //这里写校验代码
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        String token = request.getParameter("token");
        if(!"12345".equals(token)){
            context.setSendZuulResponse(false);
            context.setResponseStatusCode(401);
            try {
                context.getResponse().getWriter().write("token is invalid.");
            }catch (Exception e){}
        }
        return null;
    }

启动 gateway,在浏览器输入地址:http://localhost:8080/api/index,可以看到以下界面:

5-6

再通过浏览器输入地址:http://localhost:8080/api/index?token=12345,可以看到以下界面:

6-5

错误拦截

在一个大型系统中,服务是部署在不同的服务器下面的,我们难免会遇到某一个服务挂掉或者请求不到的时候,如果不做任何处理,服务网关请求不到会抛出500错误,对用户是不友好的。

我们为了提供用户的友好性,需要返回友好性提示,zuul 为我们提供了一个名叫 ZuulFallbackProvider 的接口,通过它我们就可以对这些请求不到的服务进行错误处理。

新建一个类 ApiFallbackProvider 并且实现 ZuulFallbackProvider 接口:

@Component
public class ApiFallbackProvider implements ZuulFallbackProvider{

    @Override
    public String getRoute() {
        return "eurekaclient";
    }

    @Override
    public ClientHttpResponse fallbackResponse() {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return 200;
            }

            @Override
            public String getStatusText() throws IOException {
                return "{code:0,message:\"服务器异常!\"}";
            }

            @Override
            public void close() {

            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream(getStatusText().getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }

其中,getRoute 方法返回要处理错误的服务名,fallbackResponse 方法返回错误的处理规则。

现在开始测试这部分代码,首先停掉服务提供者 eurekaclient,再重启 gateway,请求地址:http://localhost:8080/api/index?token=12345,即可出现以下界面:
7-2

(全文完)

(转载本站文章请注明作者和出处 第06课:服务网关