一、zuul简单应用
1.1、zuul动态路由介绍
1、什么是zuul动态路由
- 定义:Zuul 是在Spring Cloud Netflix平台上提供动态路由,监控,弹性,安全等边缘服务的框架,是Netflix基于jvm的路由器和服务器端负载均衡器,相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门
2、zuul应用架构图
1.2、在项目中创建zuul网关子模块
1、添加子模块gataway-server
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、路由测试
二、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;
}
}