网关Zuul

十、网关Zuul的使用

1.网关的作用

“网关”在计算机网络的概念里面,是用来实现不同网段之间的区分。192.168.2.10和192.168.3.11这两台电脑处于两个网段的,相当于是两个局域网。于是这两台电脑所处的网段就可以用相应的网关来表示:192.168.2.1网关和192.168.3.1网关。——这是基于255.255.255.0的子网掩码。如果子网掩码是255.255.0.0,这个时候网段就是192.168.1.1

在微服务中,网关的作用就更加的重要了:路由、限流、降级、安全控制、服务聚合。其中 限流、降级、安全控制都属于服务治理。

2.常见的网关

  • zuul:推荐使用zuul,性能和功能足够的好

  • gateway: springcloud使用的,可以使用

  • nginx:也可以做网关,功能没有zuul强大

  • 自研的网关

3. zuul的路由转发功能

通过创建一个独立的服务:网关服务,来实现网关的功能

1)引入zuul的依赖

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

2)在启动类上开启zuul. @EnableZuulProxy

@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class MyZuulApplication {

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

}

3)编写配置文件:主要配的是路由表

spring:
  application:
    name: zuul-gateway
server:
  port: 8765
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
#zuul的路由表的配置
zuul:
  routes:
    api-product: # 可以自己定义一个路由项目的名称
      path: /api/product/** # 要进行路由拦截的路径
      serviceId: PRODUCT-FEIGN-SERVICE
    api-user:
      path: /api/user/**
      serviceId: USER-SERVICE

4.路由的细节

  • 统一前缀和剥离前缀
#zuul的路由表的配置
zuul:
  routes:
    api-product: # 可以自己定义一个路由项目的名称
      path: /product/** # 要进行路由拦截的路径
      serviceId: PRODUCT-FEIGN-SERVICE
    api-user:
      path: /user/**
      serviceId: USER-SERVICE
      # 默认情况是true,如果说设置成了false,那么/user这个路径在USER-SERVICE的接口中是必须存在的
      stripPrefix: false
  # 统一前缀
  prefix: /api
  • 保护敏感路径

希望路由时一些敏感路径不被路由

#zuul的路由表的配置
zuul:
  routes:
    api-product: # 可以自己定义一个路由项目的名称
      path: /product/** # 要进行路由拦截的路径
      serviceId: PRODUCT-FEIGN-SERVICE
    api-user:
      path: /user/**
      serviceId: USER-SERVICE
      # 默认情况是true,如果说设置成了false,那么/user这个路径在USER-SERVICE的接口中是必须存在的
      stripPrefix: false
  # 统一前缀
  prefix: /api
  # 此时下游服务中带/admin/的接口将访问不到
  ignored-patterns: /**/admin/**
  • 携带cookie
private Set<String> sensitiveHeaders = new LinkedHashSet(Arrays.asList("Cookie", "Set-Cookie", "Authorization"));

从zuul的源码中看出zuul默认情况下会把cookie作为敏感头数据,不会传递cookie

所以需要做如下设置,给sensitive-headers赋一个空值,这样cookie就不会被当作敏感头,于是cookie就可以被传递。

#zuul的路由表的配置
zuul:
  routes:
    api-product: # 可以自己定义一个路由项目的名称
      path: /product/** # 要进行路由拦截的路径
      serviceId: PRODUCT-FEIGN-SERVICE
    api-user:
      path: /user/**
      serviceId: USER-SERVICE
      # 默认情况是true,如果说设置成了false,那么/user这个路径在USER-SERVICE的接口中是必须存在的
      stripPrefix: true
  # 统一前缀
  prefix: /api
  # 此时下游服务中带/admin/的接口将访问不到
  ignored-patterns: /**/admin/**
  # 敏感头:zuul会把一些请求头中的数据作为敏感的请求头数据,不被转发。
  sensitive-headers:
  • 网关的超时设置

因为zuul也是用ribbon进行通信,超时设置通过设置ribbon即可

spring:
  application:
    name: zuul-gateway
server:
  port: 8765
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
#zuul的路由表的配置
zuul:
  routes:
    api-product: # 可以自己定义一个路由项目的名称
      path: /product/** # 要进行路由拦截的路径
      serviceId: PRODUCT-FEIGN-SERVICE
    api-user:
      path: /user/**
      serviceId: USER-SERVICE
      # 默认情况是true,如果说设置成了false,那么/user这个路径在USER-SERVICE的接口中是必须存在的
      stripPrefix: true
  # 统一前缀
  prefix: /api
  # 此时下游服务中带/admin/的接口将访问不到
  ignored-patterns: /**/admin/**
  # 敏感头:zuul会把一些请求头中的数据作为敏感的请求头数据,不被转发。
  sensitive-headers:
    # 该strip-prefix是针对于外部的prefix进行作用的
#  strip-prefix: false
# 设置请求的超时时间为10s
ribbon:
  ReadTimeout: 10000
  ConnectTimeout: 10000

4.zuul网关的错误回调如何实现

通过编写FallbackProvider接口的实现类,来明确对哪个服务,或者所有服务进行错误回调设置。

package com.qf.my.zuul.fallback;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

@Component
public class ProductZuulFallback implements FallbackProvider {
    /**
     * return null: 开启对所有的路由进行错误回调
     * return "PRODUCT-FEIGN-SERVICE": 开启对PRODUCT-FEIGN-SERVICE服务的错误回调
     * @return
     */
    @Override
    public String getRoute() {
        return "PRODUCT-FEIGN-SERVICE";
    }

    /**
     * 具体发生错误时回调的内容——设置响应消息
     * @param route
     * @param cause
     * @return
     */
    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        return new ClientHttpResponse() {
            /**
             * 如果下游出现错误,调用zuul的上游应该拿到一个错误提示,但是这一次调用不应该是失败的。
             * @return
             * @throws IOException
             */
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;//表示响应成功
            }

            /**
             * 返回响应的状态码
             * @return
             * @throws IOException
             */
            @Override
            public int getRawStatusCode() throws IOException {
                return HttpStatus.OK.value();
            }

            /**
             * 响应行中的具体的文本,描述响应行的状态的文本
             * @return
             * @throws IOException
             */
            @Override
            public String getStatusText() throws IOException {
                return HttpStatus.OK.getReasonPhrase();
            }

            @Override
            public void close() {

            }
            /**
             * 设置响应消息体,返回json数据
             * @return
             * @throws IOException
             */
            @Override
            public InputStream getBody() throws IOException {
                //使用jackson来封装响应体
                ObjectMapper objectMapper = new ObjectMapper();
                Map<String,Object> map = new HashMap<>();
                map.put("code",1000);
                map.put("message","请检查你的网络");
                //把数据写成json字符串
                String json = objectMapper.writeValueAsString(map);
                //把字符串转换成byte[]数组 写入到inputStream中
                return new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
            }

            /**
             * 设置响应消息头: Content-type: application/json
             * @return
             */
            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }
}

5.zuul的过滤器的实现

1)过滤器的分类

image-20210412155635207

zuul中共有4个过滤器

  • pre:在开始路由之前触发的过滤器
  • routing: 在进行路由之时触发的过滤器
  • post: 在完成路由以后触发的过滤器
  • error: 在上述过滤器执行过程中出现了异常将会执行errorfilter

2)过滤器如何实现

编写ZuulFilter的子类

package com.qf.my.zuul.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

@Component
public class MyZuulFilter extends ZuulFilter {
    /**
     * zuul过滤器有四个类型:
     * pre
     * routing
     * post
     * error
     * @return
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 同一种过滤器类型的先后顺序
     * @return
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * 是否执行过滤
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 具体怎么过滤?
     * 模拟用户是否已登陆,如果没有登陆则不允许访问。如果已登陆,则放行
     * jwt  token=>redis中获取用户信息=》zuul得知当前用户已登陆,可以放心/如果没拿到,那就不放行
     * Authorization: Bearer eyJhbGciOiJIUzI1NiIsI.eyJpc3MiOiJodHRwczotcGxlL.mFrs3Zo8eaSNcxiNfvRh9dqKP4F1cB
     * Cookie login_token
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        //1.获取请求头中的数据-》获取request对象
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        //2.从request对象中获取Cookie 的数据
        Cookie[] cookies = request.getCookies();
        String loginToken = null;
        if(cookies!=null){
            for (Cookie cookie : cookies) {
                if("login_token".equals(cookie.getName())){
                    loginToken = cookie.getValue();
                    break;
                }
            }
        }
        if(StringUtils.isNotBlank(loginToken)){
            //拿到loginToken :去redis验证一下。如果ok: 放行
            //放行
            context.setSendZuulResponse(true);//false 不放行
            context.setResponseStatusCode(200);
            return null;
        }
        //不放行
        context.setSendZuulResponse(false);
        context.setResponseStatusCode(413);//权限不够
        return null;
    }
}

6.使用zuul网关实现后端服务的限流

使用redis来记录登陆次数

package com.qf.my.zuul.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

/**
 * 计数器限流
 */
@Component
public class CountZuulFilter extends ZuulFilter {


    @Autowired
    private RedisTemplate redisTemplate;


    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

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

    /**
     * 维护一个计数:数值存放在哪里?——redis里面 zuul肯定要搭建集群的。
     *
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext context = RequestContext.getCurrentContext();

        //1.获取redis中的访问数值
        String countKey = "zuul:count";
        /*
        redis的非原子操作: 出现安全问题。
         */
       /* int count = (int) redisTemplate.opsForValue().get(countKey);
        //================
        if(count>=10000){
            //限流

        }else{
            //放行。然后让count+1
            redisTemplate.opsForValue().set(countKey,count+1);
        }*/
        //原子操作(每访问一次就记录下来增加一次)
        Long count = redisTemplate.opsForValue().increment(countKey);
        if (count > 10) {
            //限流
            //不放行
            context.setSendZuulResponse(false);
            context.setResponseStatusCode(200);
            context.setResponseBody("当前流量过大,请稍后重试");
            return null;
        }
        //放行
        context.setSendZuulResponse(true);
        context.setResponseStatusCode(200);


        return null;
    }
}

posted @ 2021-07-21 22:07  牛奶配苦瓜  阅读(229)  评论(0编辑  收藏  举报