SpringCloud-8-Zuul综合使用

项目整体架构图如下:

8.1 Zuul:Pre和Post过滤器

  1. 1.从以上架构图知道:所有的请求都会到Zuul,然后到ServicreA、然后到ServiceB、再到ServiceC、现在我们对整体服务做一个权限校验。假如没有zuul服务、那么ServicreA、ServicreB、ServicreC都得校验一次做的太多受不了。所以权限校验我们放在Zuul里面统一处理。
  2. 2.接下来我们演示如何对请求进行统一的校验,我们现在做一个所有经过Zuul的请求都要有一个token,并且内容不能为空,如果不带token参数的话,权限校验不通过。我们以:http://localhost:8084/myProduct/product/list?token=12121为例子.。我们只有在连接后面添加:token=?才允许校验通过。
    8.1.1 Zuul统一校验
    我们在api-gateway项目下面创建filter包,并新建一个TokenFilter类
  3. 1.新建TokenFilter做PRE过滤
    a.这里面我们做的是参数校验:所以filterType是:PRE_TYPE。
    b.我们在run()方法里面实现具体的逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Component
public class TokenFilter extends ZuulFilter{
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
/**
* filter的顺序:对应的值越低优先级越高
* 我们把此Filter放在PRE_DECORATION_FILTER_ORDER前面
*/
return PRE_DECORATION_FILTER_ORDER-1;
}
@Override
public boolean shouldFilter() {
/**
* 开启过滤
*/
return true;
}
@Override
public Object run() throws ZuulException {
//1.获取当前上下文
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
//这里从url参数里面获取,也可以从cookie,header里获取
String token = request.getParameter("token");
if(StringUtils.isEmpty(token)){
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
return null;
}
}

测试:http://localhost:8084/myProduct/product/list

携带token测试:http://localhost:8084/myProduct/product/list?token=112
正常返回

当然我们的判断逻辑可能不止token为空,也可以拿到token值之后从数据库读。

  1. 2.新建Post过滤器
    我们也可以定义postfilter,在请求到结果之后,对请求的结果进行处理加工。 这里我们往请求返回的header里面写一些东西,新建AddResponseHeaderFilter类。
    a.filterType->POST_TYPE
    b.自定义post filter时候,我们选择使用:SEND_RESPONSE_FILTER_ORDER之前的filter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component
public class AddResponseHeaderFilter extends ZuulFilter{
@Override
public String filterType() {
return POST_TYPE;
}
@Override
public int filterOrder() {
/**
*我们自定义post filter时候我们filter的顺序放到SEND_RESPONSE_FILTER_ORDER之前。
**/
return SEND_RESPONSE_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletResponse response = requestContext.getResponse();
/**
*请求成功之后,我们往header里面添加一些信息。
**/
response.setHeader("X-Foo", UUID.randomUUID().toString());
return null;
}
}

测试:

8.2 Zuul:限流

8.2.1 限流基本理论
  1. 1.Zuul充当的是api网关的角色、每个请求都会经过它、在他上面做api限流保护、防止攻击。比如我们的api是发短信的、我们需要限制客户端请求速度、从而在一定程序上抵制短信大攻击、降低损失。
  2. 2.Zuul的限流是放在前置过滤器去做的、更具体来说、时机是在请求被转发之前调用。如果前置过滤器有多个操作、限流放到最靠前那个。比如:Zuul前置过滤器里面有限流、鉴权。那么限流应该早于鉴权。

  3. 3.限流方案很多,这里以一种来说:令牌桶流。

    a.中间桶(token bucket):是存放令牌的、所以叫做令牌桶。
    b.最上面会以固定的速率往令牌桶中添加令牌、如果已经放满了就丢掉。
    c.外部请求过来、会从令牌桶中获取令牌、拿到令牌之后才可以继续往前走。如果拿不到令牌直接被拒绝。其实是和买房一样。

8.2.2 限流代码实现

限流也是一个Filter,我们以RateFilter
a.filterType–>PRE_TYPE
b.filterOrder–>优先级最高:优先级为最高:组件自带的优先级最高位-3 我们这边比他还高(SERVLET_DETECTION_FILTER_ORDER-1)

1.新建RateFilter类


8.3 Zuul:鉴权和添加用户服务

8.3.1 Zuul权限校验

我们使用Zuul的权限校验实现下面3个功能。

  1. 1./order/create 只能买家访问(创建订单)
  2. 2./order/finish 只能卖家访问(完结订单)
  3. 3./product/list 都可以访问(商品列表)

  4. 1.新建鉴权Filter(AuthFilter)
    新建鉴权Filter、用来区分买家和卖家

  5. 2.实现逻辑
    怎样区分买家和卖家呢?有些人想到cookie、既然想到了cookie那么必须买家和卖家登录了之后才能获取到信息。那么登录功能写到哪里了,就需要有一个用户服务。

8.3.2 添加用户服务
  1. 1.API接口登录区分
  1. 2.数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
CREATE TABLE `user_info` (
`id` varchar(32) NOT NULL,
`username` varchar(32) DEFAULT '',
`password` varchar(32) DEFAULT '',
`openid` varchar(64) DEFAULT '' COMMENT '微信openid',
`role` tinyint(1) NOT NULL COMMENT '1买家2卖家',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`)
);
```
userInfo中买家和卖家区分是根据:role字段。
3. 3.创建应用
选择的依赖组件:
1.配置:Cloud Config-->Config Cient
2.注册中心:Cloud Discovery-->Eureka Discovery
3.数据库:SQL-->Mysql/JPA
4.缓存:NoSQL-->redis
4. 4.修改版本依赖
5. 5.统一配置中心远端服务器创建user-dev.yml文件
我们只添加数据库连接信息和redis连接信息,其他信息在bootstrap.yml文件里面
6. 6.在config服务中访问:http://localhost:8083/user-dev.yml可以查看到对应文件
7. 7.因为user服务后面肯定会对外提供接口的、所以我们将其改造成多模块
![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/7.png)
8. 8.添加dataobject

@Entity
public class UserInfo {
@Id
private String id;
private String username;
private String password;
private String openid;
private Integer role;
public String getId() {
return id;
}
}

1
2
3
4
9. 9.service和serviceImpl

public interface UserService {
/**

  • 通过openid来查询用户信息
  • @param openid
  • @return
    */
    UserInfo findByOpenid(String openid);
    }
    @Service
    public class UserServiceImpl implements UserService {
    @Autowired
    private UserInfoRepository repository;
    @Override
    public UserInfo findByOpenid(String openid) {
    return repository.findByOpenid(openid);
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #### 8.3 Zuul:鉴权和添加用户服务
    1. 1.拷贝ResultVOUtil、ResultEnum到user
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/8.png)
    2. 2.书写买家登录:
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/9.png)
    3. 3.测试:
    a.正确输入:![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/10.png)
    b.错误输入:![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/11.png)
    c.数据库角色修成成2:![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/12.png)
    4. 4.往redis里面书写数据
    买家登录时候先去redis里面找,如果有说明已经登录了,没有的话才设置cookie
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/13.png)
    上面代码会出现一个问题,就是每次页面刷新,都会在redis里面写数据,导致很多脏数据产生,所以需要在卖家最前面添加判断。
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/14.png)
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/15.png)
    ### 8.4 订单完结接口开发
    我们在api-gateway的AuthFilter里面进行过滤校验时候:

    /**

  • 1./order/create 只能买家访问
  • 2./order/finish 只能卖家访问
  • 3./product/list 都可以访问
    */
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    分为以上3步,我们还差在Order服务中订单完结接口没有开发:"/order/finish";我们现在实现此接。
    ### 8.5 权限校验完成
    #### 8.5.1 权限基本操作
    1. 1.先访问user(卖家登录:http://localhost:8085/login/seller?openid=xyz)
    结果如下:
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/16.png)
    我们请求正常,并且也获取到cookie了,此时我们把cookie删掉.再次请求api-gateway(此时请求api-gateway的时候注释掉:TokenFilter中run认证)
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/17.png)
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/18.png)
    以上返回的是成功,但是cookies中没有值,之前提过Zuul敏感头设置:
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/19.png)
    假如我们对所有服务都提供敏感头,都需要传递cookie,则:
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/20.png)
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/21.png)
    此时我们能够确保通过api-gateway是可以正常登录的。
    2. 2.接下来我们书写AuthFilter中业务逻辑。
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/22.png)
    下面我们通过接口去调用:
    3. 3.我们通过postman创建订单(切记一定要加上order服务名)
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/23.png)
    我们改掉:
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/24.png)
    我们再次请求时候回返回401:权限不足->所以我们需要先登录
    注意:在postman中,每一个窗口是互相隔离的,我们在一个窗口中登录,在另一个窗口中也是不能请求过的。
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/26.png)
    所以我们自己需要写入一个cookie到postman中去
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/25.png)
    4. 4.上面我们使用if语句来判断不同授权用户,其实这种写法以后相当不好维护,如果用户很多,耦合进来判断,有一天假如对卖家放开不限制,那么只能删掉,如何改造呢?我们一般对卖家和买家各自做一个filter:
    a.拦截统一在shouldFilter方法中
    b.拦截后处理统一在run方法中
    AuthBuyerFilter:
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/12/27.png)
    AuthSellerFilter也是类似。
    #### 8.5.2 权限补充
    1. 1.大多数情况下,我们会把权限校验的uri存放到数据库中去,那是不是api-gateway直接去连接数据库进行判断呢?
    这里牵涉到一个边界的问题,api-gateway要做的是网关,如果去连接user服务的数据库,显然是不合适的,那能不能去调用user服务呢?调用user服务是合理的,应该去调用,但是每次鉴权的时候去调用user服务,user服务那边又去调用数据库的话,这样对数据库压力很大,我们应该使用redis,api-gateway直接去redis里面获取用户权限,那么redis里面的数据怎样过来呢?可以像之前"异步扣库存的方式->用户信息一变动,就发一个信息过来,网关这边监听消息,并记录到redis"
    2. 2.微服务架构下对所有服务都得鉴权,每个服务都需要明确当前用户的权限,在Zuul的前置过滤器里面实现相关逻辑是一个值得考虑的方案,同时在微服务框架中,多个服务的无状态化一般会考虑两种技术方案:
    a.分布式session(将用户认证信息存储在共享储存中,且通常用用户会话作为key来实现简单的分布式hash映射,当用户访问微服务时,用户数据可以从共享存储中获取,用户登录状态是不透明的,同时也是一个高可用且可扩展的解决方案)
    b.OAuth2与Spring Security结合
    3. 3.我们这有一个细节:用户模块的utils代码一直在copy,相同的我们就拷贝过来,我们少了一个基础服务(如果公司是一个大型服务改造的话,基础服务比较容易,一目连然拆出来,但是是一个从头开发的项目,没有多大把握,类似我们现在,建议是将每个微服务的公用组件放到公用模块里面去,比如我们现在的user,order,product服务都有common模块,有了一定积累后,很自然的将这块代码剥离出来,作为公共组件,下沉,成为公共的一个服务)
    4. 4.另外一点,在SpringCloud架构体系中的所有微服务都是通过Zuul对外提供统一访问入口,这个时候如果公司有两套系统,一套传统项目,一套微服务架构项目,让这两套项目在线上同时运行,Zuul会非常关键。
    ### 8.6 跨域
    #### 8.6.1 跨域问题
    1. 1.我们一般的大型项目都是前后端分离的,前端通过ajax发送请求,浏览器的ajax是有同源策略的,如果违反了同源策略就会有跨域问题,Zuul作为微服务的api网关,在它上面处理跨域也是一种选择。
    2. 2.跨域其实可以看成是Spring的跨域,Spring跨域常用的一种方法是在:被调用的类或方法上增加@CrossOrigin注解来声明自己支持跨域访问,这种方式的缺点其实很明显,作用域在类或者方法上,微服务里面的应用就有很多个,一个应用里面的类和方法更加多
    3. 3.解决跨域的另一种方法是:在Zuul里增加CorsFilter过滤器,此方案是添加到网关上,对内部应用代码没有任何改造。
    #### 8.6.2 跨域改造
    ##### 1.单个接口跨域
    1. 1.项目中跨域:我们在product服务里面的ProductController的list方法里面添加跨域注解:
    allowCredentials = "true"->true允许cookie跨域

@CrossOrigin(allowCredentials = “true”)
```

2.统一跨域设置
  1. 1.在api-gateway上添加配置文件:
毕业于<br>相信技术可以改变人与人之间的生活<br>码农一枚