前言:本课程是在慕课网上学习Spring Cloud微服务实战 第9章 Zuul综合使用 时所做的笔记,供本人复习之用.

代码地址  https://github.com/springcloud-demo

目录

第一章 Pre与Post过滤器

1.1 实现Pre过滤器

1.2 实现Post过滤器

第二章 限流

2.1 代码实现

第三章 Zuul实现权限控制例子思路概要

第四章 添加用户服务

4.1 用户服务简介

4.2 用户服务的实现

第五章 订单状态改变功能实现

第六章 Zuul鉴权

6.1 代码优化

6.2 网关通过数据库鉴权

6.3 总结

第七章 跨域


第一章 Pre与Post过滤器

下面是整个项目的架构图

可以看到所有的请求都要经过Zuul,然后才会到Service A,B,C.那么我们现在要对请求做一个权限的校验,假如没有A,B,C,我们都要对权限校验一次,比较麻烦,权限校验可以放在Zuul这里统一的进行校验,

1.1 实现Pre过滤器

写一个Filter实现ZuulFilter,重写四个方法.

第一个方法代表这个过滤器在哪一部分的过滤器,如果是在pre部分的过滤器,返回PRE_TYPE,如果是post部分的过滤器,返回PRE_POST,更多的类型可以见常量类FilterConstants.

第二个方法代表这个过滤器的优先级,数字越小,优先级越高,这里我们选择我们的过滤器作用在PRE_DECORATION_FILTER的前面,所以找到其所对应的顺序数字减一.更多的过滤器级别数字可以见FilterConstants.

第三个方法统一返回true.

第四个方法实现我们的过滤器的逻辑,通过RequestContext获取request请求,如果request的请求中有token字符串就放过,没有就设置请求失败,返回HttpStatus设置的401错误.

@Component
public class TokenFilter extends ZuulFilter {//代表是pre,还是post@Overridepublic String filterType() {return PRE_TYPE;}@Overridepublic int filterOrder() {return PRE_DECORATION_FILTER_ORDER-1;}@Overridepublic boolean shouldFilter() {return true;}@Overridepublic Object run() throws ZuulException {RequestContext requestContext = RequestContext.getCurrentContext();HttpServletRequest request = requestContext.getRequest();String token = request.getParameter("token");if(StringUtils.isEmpty(token)){requestContext.setSendZuulResponse(false);requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());}return null;}
}

结果:

请求 http://localhost:9000/myProduct/product/list?token=123 ,成功

请求 http://localhost:9000/myProduct/product/list ,失败,显示401错误.

1.2 实现Post过滤器

大体思路同上.

@Component
public class AddResponseHeaderFilter extends ZuulFilter {@Overridepublic String filterType() {return POST_TYPE;}@Overridepublic int filterOrder() {return SEND_RESPONSE_FILTER_ORDER -1 ;}@Overridepublic boolean shouldFilter() {return true;}@Overridepublic Object run() throws ZuulException {RequestContext requestContext = RequestContext.getCurrentContext();HttpServletResponse response = requestContext.getResponse();response.setHeader("X-Foo", UUID.randomUUID().toString());return null;}
}

结果:

第二章 限流

由于Zuul充当的是API网关的角色,每个请求都会经过它,所以很适合在它上面对API做限流保护,防止网络攻击.比如某个API是发短信的,我们就要限制API的请求速率.在一定程度上抵御短信轰炸攻击,降低损失.

限流是放在前置过滤器中去做的,更现实一点说,是在请求被转发之前调用.如果前置过滤器有多个操作,限流应该放到最前面的地方,比如Zuul的前置过滤器里有限流,也有鉴权,那么限流应该早于鉴权,

限流的方案很多,有一种方案叫做令牌桶限流,会以一定的速率向桶中放入令牌,如果放满了就会丢弃掉,web请求过来会从令牌桶中获取到令牌,拿到令牌后才可以继续往下走,如果令牌都拿不到就会被拒绝.

2.1 代码实现

设置每秒钟向令牌桶里放入100个令牌,因为是限流,所以应该在所有过滤器的最前面,这通过设置Order为最小的Order数字减1,来设置优先级最高.最后过滤时的逻辑是,尝试获得令牌,如果获取不到令牌抛出异常.

@Component
public class RateLimitFilter extends ZuulFilter {private static final RateLimiter RATE_LIMITER = RateLimiter.create(100);@Overridepublic String filterType() {return PRE_TYPE;}@Overridepublic int filterOrder() {return SERVLET_DETECTION_FILTER_ORDER-1;}@Overridepublic boolean shouldFilter() {return true;}@Overridepublic Object run() throws ZuulException {if(!RATE_LIMITER.tryAcquire()){throw new RateLimitException();}return null;}
}
public class RateLimitException extends RuntimeException{
}

第三章 Zuul实现权限控制例子思路概要

我们要实现角色的划分,最终让不同的角色访问不同的url.

我们写三个功能作为例子.

创建订单功能: /order/create  只能买家角色访问

改变订单状态功能: /order/finish 只能卖家角色访问

查看商品 /poduct/list 都可以访问

我们将分几步去实现这些功能

1.创建user服务,在user中完成买家与卖家的登陆

2.在订单服务中完成/order/finish的功能(/order/create功能在前面已经创建过了)

3.在Zuul中区别是买家登陆还是卖家登陆

4.最后将三者结合,用user服务进行登陆,用户访问订单服务时由Zuul进行鉴权,是买家才能访问 /order/create,是卖家才能访问 /order/finish.

第四章 添加用户服务

4.1 用户服务简介

买家登陆接口

GET /login/buyer  参数 openid:abc

返回:

1. Cookie中设置 openid=abc

2. 返回的提示{ code:0,msg"成功",data:null }

之所以带的参数是openid而不是用户名密码是因为用的是微信登陆,当用户扫码登陆后,微信会返回openid.

卖家登陆接口:

GET /login/seller  参数 openid:xyz

返回:

1. Cookie中设置 token=UUID

2.redis设置key=UUID, value=xyz

3. 返回的提示{ code:0,msg"成功",data:null }

经过上面这样的登陆设置,我们用openid来作为用户登陆请求的凭证,根据Cookie中的是token还是openid来分辨用户是否已登陆以及用户的权限.

用户表的结构:

4.2 用户服务的实现

买家登陆:

@GetMapping("/buyer")public ResultVO buyer(@RequestParam("openid") String openid, HttpServletResponse response){//1.通过openid去数据库中查找对应用户UserInfo userInfo = userService.findByOpenid(openid);if(userInfo==null){return ResultVOUtil.error(ResultEnum.LOGIN_ERROR.getCode(),ResultEnum.LOGIN_ERROR.getMessage());}//2.判断用户的角色是不是买家if(RoleEnum.BUYER.getCode()!=userInfo.getRole()){return ResultVOUtil.error(ResultEnum.ROLE_ERROR.getCode(),ResultEnum.ROLE_ERROR.getMessage());}//3.如果是买家就在cookie里设置openid=xxx,然后返回CookieUtil.set(response, CookieConstant.OPENID,openid,CookieConstant.expire);return ResultVOUtil.success();}

结果:

卖家登陆

@GetMapping("/seller")public ResultVO seller(@RequestParam("openid") String openid, HttpServletResponse response, HttpServletRequest request){//判断是否已登陆,为避免每次登陆都生成不同的UUID.Cookie cookie = CookieUtil.get(request, CookieConstant.TOKEN);if(cookie!=null&&//如果从cookie中取出的token在redis中能找到,说明已登陆,直接返回登陆成功.       !StringUtils.isEmpty(stringRedisTemplate.opsForValue().get(String.format(RedisConstant.Token_TEMPLATE,cookie.getValue())))){return ResultVOUtil.success();}//1.通过openid去数据库中查找对应用户UserInfo userInfo = userService.findByOpenid(openid);if(userInfo==null){return ResultVOUtil.error(ResultEnum.LOGIN_ERROR.getCode(),ResultEnum.LOGIN_ERROR.getMessage());}//2.判断用户的角色是不是卖家if(RoleEnum.SELLER.getCode()!=userInfo.getRole()){return ResultVOUtil.error(ResultEnum.ROLE_ERROR.getCode(),ResultEnum.ROLE_ERROR.getMessage());}//3.如果是卖家,redis设置key=UUID,value=xxxString token = UUID.randomUUID().toString();Integer expire =  CookieConstant.expire;stringRedisTemplate.opsForValue().set(String.format(RedisConstant.Token_TEMPLATE,token),openid,expire,TimeUnit.SECONDS);//且设置cookie token=UUIDCookieUtil.set(response, CookieConstant.TOKEN,token,CookieConstant.expire);return ResultVOUtil.success();}

结果:

第五章 订单状态改变功能实现

controller:

@PostMapping("/finish")public ResultVO<OrderDTO> finish(@RequestParam("orderId") String orderId){return ResultVOUtil.success(orderService.finish(orderId));}

service:

@Transactional@Overridepublic OrderDTO finish(String orderId) {//1.根据订单id找到订单Optional<OrderMaster> orderMasterOptional = orderMasterRepository.findById(orderId);if(!orderMasterOptional.isPresent()){throw new OrderException(ResultEnum.ORDER_NOT_EXIST.getCode(),ResultEnum.ORDER_NOT_EXIST.getMessage());}//2.判断订单状态是否为未完结OrderMaster orderMaster = orderMasterOptional.get();if(OrderStatusEnum.NEW.getCode()!=orderMaster.getOrderStatus()){throw new OrderException(ResultEnum.ORDER_STATUS_ERROR.getCode(),ResultEnum.ORDER_STATUS_ERROR.getMessage());}//3.修改订单状态为已完结orderMaster.setOrderStatus(OrderStatusEnum.FINISHED.getCode());orderMasterRepository.save(orderMaster);//封装订单详情List<OrderDetail> orderDetailList = orderDetailRepository.findByOrderId(orderId);if(CollectionUtils.isEmpty(orderDetailList)){throw new OrderException(ResultEnum.ORDER_DETAIL_NOT_EXIST.getCode(),ResultEnum.ORDER_DETAIL_NOT_EXIST.getMessage());}OrderDTO orderDTO = new OrderDTO();//OrderDTO为订单orderMaster的部分信息,避免泄漏关键数据BeanUtils.copyProperties(orderMaster,orderDTO);orderDTO.setOrderDetailList(orderDetailList);return orderDTO;}

运行结果,将orderStatus的状态由0变为1.

第六章 Zuul鉴权

访问订单服务时都要经过Zuul,所以我们将权限校验放在Zuul,要让Zuul区分出当前是买家正在登陆,还是卖家正在登陆.以便决定最终能不能访问对应的url.

根据第四章说明,如果是买家登陆cookie里有openid.

如果是卖家登陆,cookie有token,并且对应的redis中的值.

这里要在application.properties注意要去除敏感头,不然cookie不能返回.

我们通过在Zuul中新增加一个过滤器的方式来实现Zuul的鉴权

@Slf4j
@Component
public class AuthFilter extends ZuulFilter {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic String filterType() {return PRE_TYPE;}@Overridepublic int filterOrder() {return PRE_DECORATION_FILTER_ORDER-1;}@Overridepublic boolean shouldFilter() {return true;}@Overridepublic Object run() throws ZuulException {RequestContext requestContext = RequestContext.getCurrentContext();HttpServletRequest request = requestContext.getRequest();//用户访问如下链接,必须是买家才可以访问.if("/order/order/create".equals(request.getRequestURI())){Cookie cookie = CookieUtil.get(request, "openid");if(cookie ==null||StringUtils.isEmpty(cookie.getValue())){requestContext.setSendZuulResponse(false);requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());}}//用户访问如下链接,必须是卖家才可以访问.if("/order/order/finish".equals(request.getRequestURI())){Cookie cookie = CookieUtil.get(request, "token");//没有cookie或者cookie没有值或cookie里的值不能对应//redis中的key则设置请求失败,返回401if(cookie == null|| StringUtils.isEmpty(cookie.getValue())|| StringUtils.isEmpty(stringRedisTemplate.opsForValue().get(String.format(RedisConstant.Token_TEMPLATE,cookie.getValue())))){requestContext.setSendZuulResponse(false);requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());}}return null;}
}

开启所有服务

结果,用9000是经过Zuul的,

cookie中不带token

cookie中带token,表明是卖家.

6.1 代码优化

上面的鉴权虽然实现了功能,但是相当不好维护.判断的权限很多,现在我们是耦合进行判断的,即create与finish都在一个filter里,如果我们要删除一个权限判断如create,我们要找到代码位置将其删除,如果代码多了不好维护.可以进行如下优化.

对于每一种权限校验,都单独建立一个过滤器.

买家权限校验写在AuthBuyerFilter过滤器中,是否要做校验写在shouldFilter中.

卖家权限校验写在AuthSellerFilter过滤器中,是否要做校验写在shouldFilter中.

买家权限校验核心代码:

@Overridepublic boolean shouldFilter() {RequestContext requestContext = RequestContext.getCurrentContext();HttpServletRequest request = requestContext.getRequest();//对于买家权限这里是一个地址,如果是多个地址就向这里面去加就行了.// /order/create 只能买家访问(cookie里有openid)if("/order/order/create".equals(request.getRequestURI())) return true;else return false;}@Overridepublic Object run() throws ZuulException {RequestContext requestContext = RequestContext.getCurrentContext();HttpServletRequest request = requestContext.getRequest();Cookie cookie = CookieUtil.get(request, "openid");if(cookie ==null||StringUtils.isEmpty(cookie.getValue())){requestContext.setSendZuulResponse(false);requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());}return null;}

卖家权限校验核心代码:

@Overridepublic boolean shouldFilter() {RequestContext requestContext = RequestContext.getCurrentContext();HttpServletRequest request = requestContext.getRequest();if("/order/order/finish".equals(request.getRequestURI())){return true;}return false;}@Overridepublic Object run() throws ZuulException {RequestContext requestContext = RequestContext.getCurrentContext();HttpServletRequest request = requestContext.getRequest();// /order/finish 只能卖家访问(cookie里有token,并且对应的redis中有值)Cookie cookie = CookieUtil.get(request, "token");if(cookie == null|| StringUtils.isEmpty(cookie.getValue())|| StringUtils.isEmpty(stringRedisTemplate.opsForValue().get(String.format(RedisConstant.Token_TEMPLATE,cookie.getValue())))){requestContext.setSendZuulResponse(false);requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());}return null;}

6.2 网关通过数据库鉴权

在这里我们用openid与token的方式鉴别买家与卖家,大多数情况下,我们会把这类的信息存储到数据库中去.

是不是应该在run方法中用去连接数据库来判断用户的身份呢?这样做不大好,因为api-gateway主要是做一个网关,而让它直接去连user数据库是不合适的,要注意边界.

我们可以去调用user的服务.但是如果每次请求鉴权都去调用user服务,user服务去调数据库的话,还是对数据库压力挺大的.

个人建议还是api-gateway直接去调用redis里的信息就可以直接判断用户的权限,当然redis里的信息怎么过来呢?可以像之前说的异步扣库存的方式,用户信息一变动可以发一个消息出来,网关这边监听消息,把它记录到redis中这样.

6.3 总结

1. 前面几节我们添加了用户服务,通过Zuul完成了对不同角色url的控制,微服务架构下,多个微服务都需要对访问进行鉴权,每个微服务都需要明白当前访问的用户及其权限,在Zuul的前置过滤器里实现相关逻辑是值得考虑的方案,同时在微服务中,多个服务的无状态化一般会考虑两种技术方案,一种是分布式session,一种是OAuth2.

我们采用的是分布式session方案.就是将关于用户认证的信息存储到共享储存中.且通常用用户会话作为key来实现简单的分布式哈希映射,当用户访问微服务时,用户数据可以从共享储存中获取,用户登陆状态是不透明的,同时也是高可用且扩展的解决方案.

另一种是OAuth2.0与Spring Security结合方案.

2. 我们在添加用户服务的时候,util之类的代码总是在复制,我们这里少了一个基础服务,如果公司是比较大型的项目进行改造,基础服务会比较了然的被拆出来,因为这部分代码已经有了所以比较好分辨.但是如果是一个从头开始开发的项目,不是很有把握,首先可以将微服务的公用组件放到公用模块上去.就比如现在的user,order,product服务的common模块中,有了积累之后就可以很自然的将代码剥离出来作为公共组件下层,成为公共服务.

3. SpringCloud体系内架构的所有微服务都是通过Zuul对外提供统一的访问入口,这个时候如果公司里有两套系统,一个是传统的项目,另外一套是微服务架构的项目,让这两套项目在线上同时运行,Zuul会非常关键.

第七章 跨域

前面提到我们的点餐项目是前后端分离的,也就是说前端是通过ajax发起请求,浏览器的ajax是有同源策略的,如果违反了同源策略就会产生跨域问题,Zuul作为api网关,在它上面处理跨域也是一种选择.

Zuul的跨域可以看成是Spring的一种跨域,Spring跨域常用的一种方法是在被调用的类或方法上增加@CrossOrigin注解.这种方式的缺点挺明显的,这种方式的作用域是在类或者方法上,我们这里光应用就很多,应用里的类或者方法就更多了.

也可以在nginx上来做

还可以在在Zuul里增加CorsFilter过滤器.我们下面介绍这种方法.

在Zuul中新建CorsConfig类,C - Cross O - Origin R - Resource S - sharing

@Configuration
public class CorsConfig {@Beanpublic CorsFilter corsFilter(){final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();final CorsConfiguration config = new CorsConfiguration();//支持cookie跨域config.setAllowCredentials(true);//放原始域(http:www.a.com) *是全部允许config.setAllowedOrigins(Arrays.asList("*"));//放允许的头config.setAllowedHeaders(Arrays.asList("*"));//放允许哪些方法config.setAllowedMethods(Arrays.asList("*"));//设置缓存时间,在这个时间段里,对于相同的跨域请求不再进行检查.config.setMaxAge(300l);//将跨域的配置注册到source上面去source.registerCorsConfiguration("/**",config);return new CorsFilter(source) ;}
}

如果对跨域感兴趣,推荐一门慕课网上免费的跨域课程,ajax跨域完全讲解(不是我推荐的,是讲课的老师推荐的,打算以后看看,就在此记录了一下)

SpringCloud入门(六)相关推荐

  1. Docker入门六部曲——Swarm

    原文链接:http://www.dubby.cn/detail.html?id=8738 准备工作 安装Docker(版本最低1.13). 安装好Docker Compose,上一篇文章介绍过的. 安 ...

  2. Docker入门六部曲——Stack

    原文链接:http://www.dubby.cn/detail.html?id=8739 准备知识 安装Docker(版本最低1.13). 阅读完Docker入门六部曲--Swarm,并且完成其中介绍 ...

  3. Docker入门六部曲——服务

    原文链接:http://www.dubby.cn/detail.html?id=8735 准备 已经安装好Docker 1.13或者以上的版本. 安装好Docker Compose.如果你是用的是Do ...

  4. Jmeter Web 性能测试入门 (六):Jmeter 解析 response 并传递 value

    解析response中的内容,并把获取到的value传递到后续的request中,常用的方法就是在想要解析response的request上添加后置处理器 本章介绍两种常用的组件 BeanShell ...

  5. springcloud 入门 10 (eureka高可用)

    eureka高可用: 说白了,就是加一个实例作为原实例的备份,然后一起对外提供服务.这样可以保证在一台机器宕机的时候,整个系统不会死掉.保证其继续对外服务. eureka的集群化: 服务注册中心Eur ...

  6. SpringCloud入门之应用程序上下文服务(Spring Cloud Context)详解

    构建分布式系统非常复杂且容易出错.Spring Cloud为最常见的分布式系统模式提供了简单易用的编程模型,帮助开发人员构建弹性,可靠和协调的应用程序.Spring Cloud构建于Spring Bo ...

  7. MySQL入门 (六) : 字元集与资料库

    1 Character Set与Collation 任何资讯技术在处理资料的时候,如果只是单纯的数值和运算,那就不会有太复杂的问题:如果处理的资料是文字的话,就会面临世界上各种不同语言的问题. 以资料 ...

  8. Python爬虫入门六之Cookie的使用

    大家好哈,上一节我们研究了一下爬虫的异常处理问题,那么接下来我们一起来看一下Cookie的使用. 为什么要使用Cookie呢? Cookie,指某些网站为了辨别用户身份.进行session跟踪而储存在 ...

  9. SpringCloud入门 —— SSO 单点登录

    前言 本文适合初学者,如有不足或错误之处,还请大家在下方留言指正.(文章稍长,建议点赞收藏) 一.SSO单点登录是什么? 单点登录简介 单点登录SSO (Single Sign On) 是指在一个多系 ...

  10. 图论入门六:哥尼斯堡七桥问题

    转载自https://blog.csdn.net/saltriver/article/details/54585595 哥尼斯堡七桥问题: 1736年,年仅29岁的数学家欧拉来到普鲁士的古城哥尼斯堡( ...

最新文章

  1. 强烈推荐一款完全免费的绿色JRE+Tomcat+MySQL集成开发工具 - JTM
  2. vim-go开发环境安装
  3. HDOJ 1082 模拟 水
  4. 2048游戏C语言代码
  5. Introduction to Byteball — Part 3: Smart Contracts
  6. CSS——京东首页实战总结
  7. SAP ABAP实用技巧介绍系列之 ABAP XSLT 定义变量
  8. 前端学习(1964)vue之电商管理系统电商系统之渲染分类参数的tab页标签
  9. Crystal Report 2008
  10. 12-order by和group by 原理和优化 sort by 倒叙
  11. MySql 表的分区介绍
  12. 五个拿来就能用的炫酷登录页面
  13. android studio按键精灵,按键精灵要点讲解一 - godlike的个人页面 - OSCHINA - 中文开源技术交流社区...
  14. Computer-System Structures八大思想
  15. 计算机检查磁盘,教你win7系统电脑检测到磁盘错误的解决教程
  16. 解决tomcat正常启动但是对应的网页却无法访问
  17. SeetaFace使用(问题)
  18. python读取只读word只读_10.9 只读数据库(Read-only Database)
  19. 使用sasjs构建html5 javascript css应用
  20. 想知道程序员的前景吗?

热门文章

  1. matlab 字符串比较相等,关于判断两个字符串相等的问题
  2. Linux 创建本地仓设置SSH公钥,创建分支,连接远程仓,提交代码流程
  3. c语言藏尾词输入一组英文单词,C程序统计单词.doc
  4. Codeforces 1506F - Triangular Paths
  5. 头条爆文今日如何采集文章(今日头条爆文示例)
  6. 2012-8-06可樂美文分享《Ifnbsp;Inbsp;Rest…
  7. 钉钉付款审批与金蝶K3对接方案
  8. 品牌方发行NFT时,应如何考量实用性?
  9. IT项目变更需要注意哪些内容
  10. C++好难(1):C++的入门