一、zuul简单应用

1.1、zuul动态路由介绍

1、什么是zuul动态路由
  • 定义:Zuul 是在Spring Cloud Netflix平台上提供动态路由,监控,弹性,安全等边缘服务的框架,是Netflix基于jvm的路由器和服务器端负载均衡器,相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门
2、zuul应用架构图

springgateway 静态路由 动态路由_微服务

1.2、在项目中创建zuul网关子模块

1、添加子模块gataway-server

springgateway 静态路由 动态路由_微服务_02

2、gataway-server的pom依赖
  • spring-cloud-starter-netflix-zuul就是引入zuul的相关依赖
dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-client</artifactId>
        </dependency>
        <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>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>
       
        <dependency>
            <groupId>com.mall.common</groupId>
            <artifactId>mall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
3、启动类加上@EnableZuulProxy标签
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class GataweyServerApplication {

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

}

1.3、gataway-server的配置

1、本地配置
server:
  port: 9000
spring:
  application:
    name: gataway
  cloud:
    config:
      discovery:
        enabled: true
        service-id: CONFIG
      profile: dev
vcap:
  application:
    instance_index: ${spring.cloud.config.profile}
2、git远程仓库配置
  • zuul.sensitive-headers: 为空就是设置所有动态路由服务请求允许带cookie
  • zuul.routes.自定义服务名.sensitiveHeaders:当前服务请求允许带cookie
  • zuul.routes.自定义服务名.path:设置路由要映射的url;以product为例,不设置path值默认就是serviceId的值,设置了serviceId也能路由;这里就是**/product2/product开头的都可以路由到product服务**的url
  • zuul.routes.自定义服务名.serviceId:对应配置文件中的spring.application.name 的值
  • zuul.routes.自定义服务名.sensitiveHeaders:允许当前服务请求带cookie,是局部的单个服务,不是全局所有的服务都可以带cookie
  • zuul.routes.ignored-patterns:忽略掉一部分url路由;一般用于忽略服务间通信的接口
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
zuul:
  # 设置所有服务都允许传递cookie
  sensitive-headers: 
  routes:
    product:
       path: /product2/**
       serviceId: product
       url: http://localhost:8083/
    order:
       path: /order/**
       serviceId: order
       url: http://localhost:8082/
    user:
       path: /myUser/**
       serviceId: user
       url: http://localhost:8084/
    ignored-patterns:
       - /**/productClient/providerList
       - /**/productClient/decreaseStock

spring:
  redis:
    database: 4
    host: localhost
    password: 123456
    port: 6379
    timeout: 10000
    jedis:
      pool:
        #jedis线程池最大活跃数
        max-active: 20
        #jedis线程池最大闲置数
        max-idle: 15
        # jedis线程池最小闲置数
        min-idle: 5
        # jedis线程池最大等待时间,ms为单位
        max-wait: 1000
3、路由测试

springgateway 静态路由 动态路由_微服务_03

二、zuul过滤器实战

1.1、实现user服务的登录接口

1、user服务数据库表
DROP TABLE IF EXISTS `seller_info`;

CREATE TABLE `seller_info` (
  `id` VARCHAR(32) CHARACTER SET gbk NOT NULL,
  `username` VARCHAR(32) CHARACTER SET gbk NOT NULL,
  `password` VARCHAR(32) CHARACTER SET gbk NOT NULL,
  `openid` VARCHAR(64) CHARACTER SET gbk NOT NULL COMMENT '微信openid',
  `role` INT(1) DEFAULT NULL  COMMENT '角色标识:0代表卖家,1代表买家',
  `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `delete_status` INT(1) DEFAULT NULL COMMENT '类目是否删除标志:0代表已删除,1代表在用',
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='卖家信息表';

INSERT  INTO `seller_info`(`id`,`username`,`password`,`openid`,`role`,`update_time`,`create_time`,`delete_status`) VALUES
('432155','mark','123456','abc',0,'2019-05-26 15:39:53','2019-05-26 15:39:31',1),
('542367','jack','234556','owD2P1XCwKnKbTxh_Nv5dtem2owk',1,'2019-05-26 21:17:10','2019-05-26 21:11:59',1);
2、登录接口
/**
 * 登录处理接口
 */
public interface UserService {

    /**
     * 登录
     * @return
     */
    String login(String openId, HttpServletResponse response, HttpServletRequest request);

}
3、实现登录接口
  • 用户每登录一次就随机一个uuid,并将uuid设置为一个cookie的value值;cookie存放内容是token = uuid
  • 用户没登录一次以上面的uuid拼接redis hash数据的key值filed值,方便下一次获取redis缓存中用户登录状态,key值拼接完成是login_token_uuid;filed值拼接完成是user_uuid;hash数据的value值就是LoginToken 对象值,包括用户名,用户角色,用户openId
  • 每次登录之前校验request中是否存在key值为token的cookie,如果存在以及cookie对应的redis数据中是当前openID证明用户已登录,无需重复登录。
  • 不足:目前一个hash数据存储一名用户的登录状态浪费资源,需要实现一个hash数据存储多名用户的登录cookie,并且给field设置失效时间;而不是hash key设置失效时间
@Autowired
    private UserInfoRepository userInfoRepository;

    @Autowired
    private JedisService jedisService;

    @Autowired
    private JedisConfig jedisConfig;

    /**
     * 处理登录逻辑
     * @return
     */
    @Override
    public String login(String openId, HttpServletResponse response, HttpServletRequest request) {
        //1、判断用户是否已经登录:cookie对应的redis数据中是否是当前openID
        Cookie cookie = CookieUtil.getCookie(request, CookieConstant.TOKEN);
        if (cookie != null){
            String redisKey = String.format(RedisKeyConstant.LOGIN_TOKEN,cookie.getValue());
            String redisField = String.format(RedisKeyConstant.USER_INFO,cookie.getValue());
            LoginToken loginToken = jedisService.hgetObj(redisKey, redisField, LoginToken.class, jedisConfig.getDatabase());
            if (loginToken != null && loginToken.getOpenid().equals(openId)){
                  return loginToken.getUserName();
            }
            UserInfo userInfo = doUserPost(openId, response, request);
            //5、将用户名返回
            return userInfo.getUserName();
        }
        UserInfo userInfo = doUserPost(openId, response, request);
        //5、将用户名返回
        return userInfo.getUserName();
    }

    private UserInfo doUserPost(String openId, HttpServletResponse response, HttpServletRequest request){
        //2、查询userinfo信息是否存在
        UserInfo userInfo = userInfoRepository.findUserInfoByOpenidAndDeleteStatus(openId, DeleteStatusEnum.ON_USE.getCode());
        if(userInfo == null){
            throw new MyException(ResultEnum.USER_INFO_NOT_EXIST);
        }
        //3、redis里面存储openId和role
        String uuid = UUID.randomUUID().toString();
        String redisKey = String.format(RedisKeyConstant.LOGIN_TOKEN,uuid);
        String redisKeyField = String.format(RedisKeyConstant.USER_INFO,uuid);
        LoginToken loginToken = new LoginToken();
        BeanUtils.copyProperties(userInfo,loginToken);
        //todo 待优化:一个hash存贮多个用户登录信息,但是目前是一个key存一个用户信息,
        // 且过期时间是根据key决定的;后续业务需要改成一个hash存多个用户登录信息;过期时间根据field决定
        jedisService.hsetObjEx(redisKey,redisKeyField,loginToken,RedisKeyConstant.EXPIRE,jedisConfig.getDatabase());
        //4、cookie里面设值 token = uuid
        CookieUtil.setCookie(response, CookieConstant.TOKEN,uuid, CookieConstant.AGE_TIME);
        return userInfo;
    }

1.2、zuul过滤器实现登录校验

1、zuul常用过滤器
  • Zuul中定义了四种标准过滤器类型
    1、 PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
    2、ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
    3、POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
    4、ERROR:在其他阶段发生错误时执行该过滤器
  • 以上四种过滤器实现均要继承ZuulFilter 抽象类;filterType决定使用那种类型的过滤器,filterOrder设置过滤器等级
2、编写zuul自定义过滤器:买家登录校验
/**
 * zuul前置过滤器:鉴权
 */
@Component
@Slf4j
public class AuthBuyerFilter extends ZuulFilter {

    @Autowired
    private JedisService jedisService;

    @Autowired
    private JedisConfig jedisConfig;

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

    @Override
    public int filterOrder() {
        return FilterConstants.FORM_BODY_WRAPPER_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        String requestURI = request.getRequestURI();
        /**
         * /order/order/buyer/create 创建订单:只能买家访问
         * /order/order/buyer/cancel 取消订单:只能买家访问
         * /order/order/buyer/paid 支付订单:只能买家访问
         * /order/order/seller/finish 完结订单:只能卖家访问
         * /product/product/list 商品列表:卖家和买家都能够访问
         * /order/order/detail 查看商品详情:卖家和买家都能访问
         */
        switch(requestURI){
            case "/order/order/buyer/create":
                return true;
            case "/order/order/buyer/cancel":
                return true;
            case "order/order/buyer/paid":
                return true;
            default:
                break;
        }
        return false;
    }

    @Override
    public Object run() throws ZuulException {

        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        Cookie cookie = CookieUtil.getCookie(request, CookieConstant.TOKEN);
        //1、判断cookie是否存在
        if (cookie == null){
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
//            throw new MyException(ResultEnum.COOKIE_ERROR);
            return null;
        }
        //2、判断角色是否正确
        String redisKey = String.format(RedisKeyConstant.LOGIN_TOKEN,cookie.getValue());
        String redisField = String.format(RedisKeyConstant.USER_INFO,cookie.getValue());
        LoginToken loginToken = jedisService.hgetObj(redisKey, redisField, LoginToken.class, jedisConfig.getDatabase());
        if(loginToken == null){
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
//            throw new MyException(ResultEnum.LOGIN_ERROR);
            return null;
        }
        if(loginToken.getUserName() == null ||
           loginToken.getOpenid() == null ||
           loginToken.getRole() == UserRoleEnum.SELLER.getCode()){
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
//            throw new MyException(ResultEnum.LOGIN_ERROR);
            return null;
        }
        return null;
    }
}
3、编写zuul自定义过滤器:卖家登录校验
/**
 * zuul前置过滤器:鉴权
 */
@Component
public class AuthSellerFilter extends ZuulFilter {

    @Autowired
    private JedisService jedisService;

    @Autowired
    private JedisConfig jedisConfig;

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

    @Override
    public int filterOrder() {
        return FilterConstants.FORM_BODY_WRAPPER_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        String requestURI = request.getRequestURI();
        /**
         * /order/order/buyer/create 创建订单:只能买家访问
         * /order/order/buyer/cancel 取消订单:只能买家访问
         * /order/order/buyer/paid 支付订单:只能买家访问
         * /order/order/seller/finish 完结订单:只能卖家访问
         * /product/product/list 商品列表:卖家和买家都能够访问
         * /order/order/detail 查看商品详情:卖家和买家都能访问
         */
        switch (requestURI){
            case "/order/order/seller/finish":
                return true;
            default:
                break;
        }
        return false;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        HttpServletResponse response = requestContext.getResponse();
        Cookie cookie = CookieUtil.getCookie(request, CookieConstant.TOKEN);
        //1、判断cookie是否存在
        if (cookie == null){
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
            return null;
//            throw new MyException(ResultEnum.COOKIE_ERROR);
        }
        //2、判断角色是否正确
        String redisKey = String.format(RedisKeyConstant.LOGIN_TOKEN,cookie.getValue());
        String redisField = String.format(RedisKeyConstant.USER_INFO,cookie.getValue());
        LoginToken loginToken = jedisService.hgetObj(redisKey, redisField, LoginToken.class, jedisConfig.getDatabase());
        if(loginToken == null){
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
//            throw new MyException(ResultEnum.LOGIN_ERROR);
            return null;
        }
        if(loginToken.getUserName() == null ||
                loginToken.getOpenid() == null ||
                loginToken.getRole() == UserRoleEnum.BUYER.getCode()){
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
//            throw new MyException(ResultEnum.LOGIN_ERROR);
            return null;
        }
        return null;
    }
}

1.3、zuul令牌桶限流

1、令牌桶
  • 令牌桶的优先级必须是最高,所以它必须是所有zuul过滤器之前执行
  • 令牌桶的作用限流,当同时有很多请求后台时,限流就显得格外重要
  • 令牌桶实现方式有两种:这里使用的是方式一
    方式一:使用guava谷歌封装的令牌桶
    方式二:spring-cloud-zuul-ratelimit方式限流
/**
 * zuul前置过滤器:路由限流;这里使用令牌桶的方式限流
 */
@Component
public class RouteLimiterFilter extends ZuulFilter {

    /**
     * 方式一:使用guava谷歌封装的令牌桶:
     * 下面设置的是每秒释放100个令牌;如果一秒钟访问的次数超过令牌数,就会限流
     * 方式二:spring-cloud-zuul-ratelimit方式限流
     * 使用方法:https://www.jianshu.com/p/699348ee5607
     * @return
     */
    private static final RateLimiter RATE_LIMITER = RateLimiter.create(100);


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

    @Override
    public int filterOrder() {
        //路由限流过滤器的优先级必须最高
        return FilterConstants.SERVLET_DETECTION_FILTER_ORDER - 1;
    }

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

    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        if(!RATE_LIMITER.tryAcquire()){
           //如果没有令牌了就抛异常
            requestContext.setResponseStatusCode(HttpStatus.SC_REQUEST_TOO_LONG);
            requestContext.setSendZuulResponse(false);
        }
        return null;
    }
}

1.4、zuul后置过滤器

  • 这里只是一个简单的测试后置过滤器的代码,在整个服务中并没有什么重要作用
/**
 * zuul后置过滤器:路由转发并请求成功以后被执行
 */
//@Component
public class MyPostFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
    }

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

    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletResponse response = requestContext.getResponse();
        response.setHeader("A-foo", UUID.randomUUID().toString());
        return null;
    }
}