总结之SpringCloud之路由网关——Zuul

Zuul是Spring Cloud全家桶中的微服务API网关。

所有从设备或网站来的请求都会经过Zuul到达后端的Netflix应用程序。作为一个边界性质的应用程序,Zuul提供了动态路由、监控、弹性负载和安全功能。Zuul底层利用各种filter实现如下功能:
•认证和安全 识别每个需要认证的资源,拒绝不符合要求的请求。
•性能监测 在服务边界追踪并统计数据,提供精确的生产视图。
•动态路由 根据需要将请求动态路由到后端集群。
•压力测试 逐渐增加对集群的流量以了解其性能。
•负载卸载 预先为每种类型的请求分配容量,当请求超过容量时自动丢弃。
•静态资源处理 直接在边界返回某些响应。

zuul入门

新建一个服务zuul-service作为路由网关服务。
pom.xml中引入依赖

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

使用@EnableZuulProxy启用Zuul(使用@EnableZuulServer也可以启用Zuul,只是不会自动从Eureka中获取并自动代理服务,也不会自动加载部分Zuul过滤器,但是可以选择性地替换代理平台的各个部分)。

@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulApplicationStarter {

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

路由配置

server:
  port: 9000
spring:
  application:
    name: zuul Service
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8771/eureka/
    fetch-registry: false
zuul:
  strip-prefix: false
  routes:
    users: #你自定义规则名字
      path: /user/** #url地址
      serviceId: one #路由到serviceid
  ignored-patterns: /user/one #url拦截 防止外面通过url请求内部通信路径
  #简便写法
    #one: /user/**
ribbon:
  eureka:
    enabled: false
one:
  ribbon:
    listOfServers: http://localhost:8773
management:
  security:
    enabled: false

相关参数
Zuul会自动读取注册中心的已经注册的服务。user-service服务会自动设置/user-service/**这样的路由,即/user-service/users会被代理到user-service服务的/users请求。
zuul.ignoredServices可以指定忽略注册中心获取的服务
zuul.routes.=路由key使用一个服务名称,对应一个路由路径
zuul.routes.<key>.serviceId=<serviceId>指定一个服务对应路由路径为zuul.routes.<key>.path
zuul.routes.<key>.url=<url>指定一个服务的url或者使用forward转向Zuul服务的接口,对应路由路径为zuul.routes.<key>.path
zuul.routes.<ribbon>=<path>使用自定义Ribbon实现路由

Zuul服务启动完成后,可以访问http://localhost:9000/routes获取路由列表

动态路由

Zuul结合SpringCloud配置中心,在修改路由配置信息后刷新配置可立即生效,无需重启Zuul服务,这样就实现了动态路由。

Zuul Filter

Zuul进行代理时,会有一系列的Zuul Filter对Http请求的request和response进行封装和操作。
一个Zuul Filter有下面四个要素:

Type:类型。Zuul Filter的类型包括pre,routing,post和error。routing过滤器是在路由阶段执行的,负责寻找原服务、请求转发和返回接收。pre和post分别在routing之前和之后执行。如果Zuul执行代理的过程中抛出ZuulException异常,则会被error过滤器捕获并进行相应处理。
在这里插入图片描述
Execution Order:执行顺序。通过一个整型的值从小到大依次执行(相同类型过滤器间互相比较)。
Criteria:执行条件。当满足一定条件时,才会执行该过滤器。
Action:执行动作。当执行条件满足时,进行的操作。
实现一个过滤器只要继承ZuulFilter,并实现filterType(),filterOrder(),shouldFilter()和run()四个方法。这些方法与上面的四个要素对应。
如果要禁用一个Zuul过滤器,只需要配置zuul.<SimpleClassName>.<filterType>.disable=true,比如需要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter需要配置zuul.SendResponseFilter.post.disable=true
下面我们使用一个pre过滤器实现token验证,如果Http header里面没有一个固定的token,则禁止访问。
禁用Zuul默认的error过滤器,设置固定的token和需要验证的路由key名单

zuul:
 # 禁用SpringCloud自带的error filter
 SendErrorFilter:
   error:
     disable: true

zuul-filter:
 token-filter:
   # 访问时,需要进行认证的路由key
   un-auth-routes:
     - users
     - smsApi
   # 固定的token
   static-token: xF2fdi8M

读取自定义token配置信息

@Component
@ConfigurationProperties("zuulFilter.tokenFilter")
public class TokenValidateConfiguration {

    // 在这个列表里面存储的routeId都是需要使用TokenValidateFilter过滤的
    private List<String> unAuthRoutes;

    // 给定的token
    private String staticToken;

    public List<String> getUnAuthRoutes() {
        return unAuthRoutes;
    }

    public void setUnAuthRoutes(List<String> unAuthRoutes) {
        this.unAuthRoutes = unAuthRoutes;
    }

    public String getStaticToken() {
        return staticToken;
    }

    public void setStaticToken(String staticToken) {
        this.staticToken = staticToken;
    }
}

自定义过滤器

@Component
public class TokenValidateFilter extends ZuulFilter {

    protected static final Logger logger = LoggerFactory.getLogger(TokenValidateFilter.class);

    @Autowired
    private TokenValidateConfiguration tvConfig;

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

    @Override
    public int filterOrder() {//执行顺序
        return FilterConstants.PRE_DECORATION_FILTER_ORDER;
    }

    @Override
    public boolean shouldFilter() {//是否拦截
        RequestContext ctx = RequestContext.getCurrentContext();
        return tvConfig.getUnAuthRoutes().contains(ctx.get(FilterConstants.PROXY_KEY));
    }

    @Override
    public Object run() {//拦截后操作
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        String token = request.getHeader("Authorization");
        if (token == null) {
            logger.warn("Http Header Authorization is null");
            forbidden();
            return null;
        }

        String staticToken = tvConfig.getStaticToken();
        if (StringUtils.isBlank(staticToken)) {
            logger.warn("property zuulFilter.tokenFilter.staticToken was not set");
            forbidden();
        } else if (!staticToken.equals(token)) {
            logger.warn("token is not valid");
            forbidden();
        }
        return null;
    }

    /**
     * 设置response的状态码为403
     */
    private void forbidden() {
        // zuul中,将请求附带的信息存在线程变量中。
        RequestContext.getCurrentContext().setResponseStatusCode(HttpStatus.FORBIDDEN.value());
        ReflectionUtils.rethrowRuntimeException(new ZuulException("token is not valid", HttpStatus.FORBIDDEN.value(),
                "token校验不通过"));
    }
} 

注意:如果使用zuul.routes.=方式配置的路由,则ctx.get(FilterConstants.PROXY_KEY)会得到去掉头尾的url(/smsApi/**会得到smsApi,/smsApi/target/**会得到smsApi/target),而并非路由key。所以之前配置文件中的路由

测试:携带正确的token访问成功

新建一个error过滤器,当捕获到ZuulException时,返回一个JSON对象

@Component
public class SendErrorRestFilter extends ZuulFilter {

    private static final Logger logger = LoggerFactory.getLogger(SendErrorRestFilter.class);

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

    @Override
    public int filterOrder() {
        return FilterConstants.SEND_ERROR_FILTER_ORDER;
    }

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

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        Throwable throwable = getCause(context.getThrowable());
        // 获取response状态码
        int status = context.getResponseStatusCode();
        JSONObject info = new JSONObject();
        info.put("code", "异常码" + status);
        info.put("message", throwable.getMessage());
        // 记录日志
        logger.warn("请求异常,被error filter拦截", context.getClass());

        // 设置response
        context.setResponseBody(info.toJSONString());
        context.getResponse().setContentType("application/json;charset=UTF-8");
        context.getResponse().setStatus(HttpStatus.OK.value());

        // 处理了异常之后清空异常
        context.remove("throwable");
        return null;
    }

    private Throwable getCause(Throwable throwable) {
        while (throwable.getCause() != null) {
            throwable = throwable.getCause();
        }
        return throwable;
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章