阅读 939

Spring Cloud Alibaba 实战(五)Zuul篇

白菜Java自习室 涵盖核心知识

Spring Cloud Alibaba 实战(一)准备篇
Spring Cloud Alibaba 实战(二)Nacos篇
Spring Cloud Alibaba 实战(三)Sentinel篇
Spring Cloud Alibaba 实战(四)Oauth2篇
Spring Cloud Alibaba 实战(五)Zuul篇
Spring Cloud Alibaba 实战(六)RocketMQ篇
Spring Cloud Alibaba 实战(七)Seata篇
Spring Cloud Alibaba 实战(八)SkyWalking篇

项目 GitHub 地址:github.com/D2C-Cai/her…

1. Zuul 简介

Zuul 微服务网关是为 Spring Cloud Netflix 提供动态路由,监控,弹性,安全等服务的框架。可以和Eureka、Ribbon、Hystrix 等组件配合使用。

1.1. Zuul 主要功能:

  • 认证和安全: 识别每个需要认证的资源,拒绝不符合要求的请求。
  • 性能监测: 在服务边界追踪并统计数据,提供精确的生产视图。
  • 动态路由: 根据需要将请求动态路由到后端集群。
  • 压力测试: 逐渐增加对集群的流量以了解其性能。
  • 负载卸载: 预先为每种类型的请求分配容量,当请求超过容量时自动丢弃。
  • 静态资源处理: 直接在边界返回某些响应。

2. Zuul 网关的引入

上文 oauth2 篇遗留的问题:

认证逻辑和微服务耦合,每个微服务都需要各自建立一遍,而且存在多次重复的认证逻辑。当一个请求的微服务调用链路很长时,岂不是每个微服务都要请求 Oauth2 认证服务(服务端)一次,这样形成了请求和认证 1比N 的次数关系,如果请求流量比较大,让 oauth2-service 承受了巨大压力。

2.1. Zuul 带来什么优势

没有使用网关服务

  1. 客户端会多次请求不同的微服务,增加了客户端的复杂性。
  2. 存在跨域请求,在一定场景下处理相对复杂。
  3. 认证复杂,每个服务都需要独立认证。

使用服务网关之后

  1. 易于监控,可在微服务网关收集监控数据并将其推送到外部系统进行分析。
  2. 易于认证,可在微服务网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
  3. 减少了客户端与各个微服务之间的交互次数。

3. Zuul 网关的快速搭建

  1. 添加 pom 文件依赖
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
复制代码
  1. 添加 @EnableZuulProxy 注解
@EnableZuulProxy
@SpringBootApplication
public class GatewayServiceApplication {

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

}
复制代码

理论上到这一步,zuul 网关就能直接使用了。

3.1. Zuul 整合 Oauth2 认证

我们要对上文 Oauth2 整个服务的架构进行一些改造:

  1. Oauth2 认证中心(服务端) 保持不变。
  2. Oauth2 资源服务(客户端) 改成 zuul 网关鉴权。
  • 会员服务:herring-member-service,微服务之一,接收到请求后会到认证中心验证。
  • 订单服务:herring-orders-service,微服务之二,接收到请求后会到认证中心验证。
  • 商品服务:herring-product-service,微服务之三,接收到请求后会到认证中心验证。
  1. 删去所有 会员服务、订单服务、商品服务 中有关 Oauth2 的代码

这里还有 feign 部分代码,可以保留也可以删除,建议是保留,因为虽然由网关 zuul 统一鉴权,但是单个微服务还需要在业务上用到用户信息(如用户ID),常规做法是网关 zuul 将认证通过的 token 解码后,将用户信息带入 request 请求上下文中,再通过 feign 的拦截器统一传递。

  1. 在 zuul 服务中,添加 pom 文件依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
复制代码

如果在 jwt 中加入了额外信息,而在接收到 jwt 格式的 token 之后,用户客户端要把 jwt 解析出来。

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
复制代码
  1. ResourceServerConfig 类的配置:
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class JwtResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Resource
    private TokenStore jwtTokenStore;

    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
        accessTokenConverter.setSigningKey("sign-8888");
        accessTokenConverter.setVerifierKey("sign-8888");
        return accessTokenConverter;
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenStore(jwtTokenStore);
        resources.resourceId("gateway-service");
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/oauth/**").permitAll();
    }

}
复制代码

注意:这里和原先微服务项目中的文件有两处不同

  • resourceId: 数据库表里改为 gateway-service;
        resources.resourceId("gateway-service");
复制代码

什么意思呢? resource_ids 描述资源服务器和客户端的关系,原先比如我定义了cleint-id=app-client,resource_ids=member-service,orders-service,product-service,就是 app-client 端的请求允许访问这些资源服务器;我现在 resource_ids=gateway-service,就是 app-client 端的请求允许访问 zuul 服务器,其余的我就不控制,都全部交给网关了。

  • 添加请求白名单 /oauth/**;
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/oauth/**").permitAll();
    }
复制代码

为什么呢?我们设想的是所有请求都由经网关 zuul 再到我们的微服务,oauth2-service 认证中心虽然保持不变也是一个微服务,因为 zuul 增加了认证逻辑,而进行转发 /oauth/** 类似登录和刷新 token 的请求本身不带 token,需要白名单放行才能正常进入认证中心。

  1. application.yml 配置文件:
server:
  port: 8080

spring:
  application:
    name: gateway-service
    
zuul:
  sensitive-headers:
  routes:
    oauth2-routing:
      path: /oauth2-service/**
      serviceId: oauth2-service
    member-routing:
      path: /member-service/**
      serviceId: member-service
    orders-routing:
      path: /orders-service/**
      serviceId: orders-service
    product-routing:
      path: /product-service/**
      serviceId: product-service

ribbon:
  eager-load:
    enabled: true
    clients: oauth2-service,member-service,orders-service,product-service
复制代码

以下是两种配置文件的配置方式,可以根据需要选取对自己项目有利的配置。

  • 指定 url 型(这种方式配置的路由不会作为 Hystrix 命令执行,同时也不能使用 Ribbon 来负载均衡多个URL):
# 该配置方式中,member-routing 只是给路由一个名称,可以任意起名。
zuul:
  routes:
    member-routing:
      url: http://localhost:10801/  # 指定的url
      path: /member-service/**      # 对应的路径
复制代码
  • 指定 serviceId 型(这种方式配置要配合 Nacos一起使用,并且路由会自动启用 Hystrix,Ribbon 功能特性):
# 该配置方式中,member-routing 只是给路由一个名称,可以任意起名。
zuul:
  routes:
    member-routing:
      serviceId: member-service  # 指定的serviceId
      path: /member-service/**   # 对应的路径
复制代码

这种配置方式有一个比较坑的点,如果你全使用默认参数,每次第一次请求,永远会报 Hystrix 超时错误

{
  "timestamp": "2021-02-18T08:08:38.183+0000",
  "status": 504,
  "error": "Gateway Timeout",
  "message": "com.netflix.zuul.exception.ZuulException: Hystrix Readed time out"
}
复制代码

网上查阅资料结果,是 Ribbon 懒加载的问题,时间过长 Hystrix 导致熔断。两种配置方式,使用的 zuul 提供的超时参数不同,这个 zuul 官网有,这里就不详细介绍。目前一个有效的解决办法如下:

zuul:
    host:
      socket-timeout-millis: 60000
      connect-timeout-millis: 60000

ribbon:
  eager-load:
    enabled: true
  ReadTimeout: 60000
  ConnectTimeout: 60000

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 60000
复制代码

配置完成,我们可以来对比下请求的 url(假如对微服务的网络进行物理隔离,那么请求就只能走网关):

  • 对 oauth2-service 的访问请求:

没有网关:

POST http://localhost:10800/oauth/token
复制代码

带上网关:

POST http://localhost:8080/oauth2-service/oauth/token
复制代码
  • 对 member-service 的访问请求:

没有网关:

GET http://localhost:10801/api/member/hello
复制代码

带上网关:

GET http://localhost:8080/member-service/api/member/hello
复制代码

3.2. 测试 Zuul 整合 Oauth2 认证结果

一、向 Oauth2 认证中心(服务端)请求 token:

#### 向 Oauth2 认证中心(服务端)请求 token

POST http://localhost:8080/oauth2-service/oauth/token?grant_type=password&username=admin&password=123456&client_id=app-client&client_secret=client-secret-8888&scope=all
Accept: */*
Cache-Control: no-cache
复制代码

得到请求结果:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib3JkZXJzLXNlcnZpY2UiLCJnYXRld2F5LXNlcnZpY2UiLCJtZW1iZXItc2VydmljZSIsInByb2R1Y3Qtc2VydmljZSJdLCJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTYxMjg1ODQxNywiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJiMGQ5ZTI1Yy1jZGE3LTQ4MDctOWJmZS02ZjcyYjM4NGVhNTMiLCJjbGllbnRfaWQiOiJhcHAtY2xpZW50In0.w4M9zCahAVISQ_wfKdkT6n9Aaw6kFtoh5HmCJ_uy-vU",
  "token_type": "bearer",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib3JkZXJzLXNlcnZpY2UiLCJnYXRld2F5LXNlcnZpY2UiLCJtZW1iZXItc2VydmljZSIsInByb2R1Y3Qtc2VydmljZSJdLCJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImIwZDllMjVjLWNkYTctNDgwNy05YmZlLTZmNzJiMzg0ZWE1MyIsImV4cCI6MTYxMjkyMzIxNywiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiIzZmQ2MWM4ZS1kNTcyLTQ0YjYtYjViNC0zMzc3ODQ5NjY4YmQiLCJjbGllbnRfaWQiOiJhcHAtY2xpZW50In0.WxisDVLUlfP45pepc4sQM1M7UCvzsET0O8JvF11tKAI",
  "expires_in": 7199,
  "scope": "all",
  "jwt-ext": "JWT 扩展信息",
  "jti": "b0d9e25c-cda7-4807-9bfe-6f72b384ea53"
}
复制代码

二、向 Oauth2 资源服务(客户端)请求数据:

请求不带 token:

#### 向 Oauth2 资源服务(客户端)请求数据

GET http://localhost:8080/member-service/api/member/hello
Accept: */*
Cache-Control: no-cache
复制代码

得到请求结果:

{
  "error": "unauthorized",
  "error_description": "Full authentication is required to access this resource"
}
复制代码

请求带正确的 token:

#### 向 Oauth2 资源服务(客户端)请求数据

GET http://localhost:8080/member-service/api/member/hello
Accept: */*
Cache-Control: no-cache
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib3JkZXJzLXNlcnZpY2UiLCJnYXRld2F5LXNlcnZpY2UiLCJtZW1iZXItc2VydmljZSIsInByb2R1Y3Qtc2VydmljZSJdLCJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTYxMjg1ODQxNywiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJiMGQ5ZTI1Yy1jZGE3LTQ4MDctOWJmZS02ZjcyYjM4NGVhNTMiLCJjbGllbnRfaWQiOiJhcHAtY2xpZW50In0.w4M9zCahAVISQ_wfKdkT6n9Aaw6kFtoh5HmCJ_uy-vU
复制代码

得到请求结果:

Hello, Member! Hello, Product! Hello, Orders! 
复制代码

3.3. Zuul 整合 Oauth2 鉴权分析

认证授权 是两个不同的步骤。认证部分我们已经完成了,授权部分指的是具体 url 的权限,这个东西业务性较强具体代码就不贴了,但是能给大家提供思路。

为什么说授权 url 就业务性较强呢? 相信大家网上也看过很多例子,最简单的就是 RBAC0(Role-Based Access Control) 模型五表结构(用户表-角色表-资源表-用户角色关系-角色资源关系),公司组织无非是用户的分组,基本样子也是大同小异。我们在互相借鉴的同时不妨停下来思考一下,企业级应用不是套模型增删改查,如果我的微服务想要各自维护一套权限体系呢?我这个所谓的 url 并不是单一大系统维护的,而是想要各个微服务自己维护一套 url 权限体系,这样将 url 授权逻辑写在网关中显得复杂耦合而鸡肋。最合适的解决办法便是:网关解决认证问题,upms(User Permissions Management System) 子系统解决授权问题

上一篇文章的 oauth2-service 中的 UserDetailsService:

@Slf4j
@Component(value = "herringUserDetailsService")
public class HerringUserDetailsService implements UserDetailsService {

    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("username is:" + username);
        // 查询数据库操作
        if (!username.equals("admin")) {
            throw new UsernameNotFoundException("the user is not found");
        } else {
            // 用户角色也应在数据库中获取
            String role = "ROLE_ADMIN";
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority(role));
            // 线上环境应该通过用户名查询数据库获取加密后的密码
            String password = passwordEncoder.encode("123456");
            return new org.springframework.security.core.userdetails.User(username, password, authorities);
        }
    }

}
复制代码

可以将这部分逻辑,专门抽出一个独立的 upms(User Permissions Management System)系统,简单介绍下 RBAC(Role-Based Access Control) 模型 :

RBAC0模型

这是权限最基础也是最核心的模型,它包括用户/角色/权限,其中用户和角色是多对多的关系,角色和权限也是多对多的关系。

用户:是发起操作的主体,按类型分可分为2B和2C用户,可以是后台管理系统的用户,可以是OA系统的内部员工,也可以是面向C端的用户,比如阿里云的用户。

角色:起到了桥梁的作用,连接了用户和权限的关系,每个角色可以关联多个权限,同时一个用户关联多个角色,那么这个用户就有了多个角色的多个权限。有人会问了为什么用户不直接关联权限呢?在用户基数小的系统,比如20个人的小系统,管理员可以直接把用户和权限关联,工作量并不大,选择一个用户勾选下需要的权限就完事了。但是在实际企业系统中,用户基数比较大,其中很多人的权限都是一样的,就是个普通访问权限,如果管理员给100人甚至更多授权,工作量巨大。这就引入了"角色(Role)"概念,一个角色可以与多个用户关联,管理员只需要把该角色赋予用户,那么用户就有了该角色下的所有权限,这样设计既提升了效率,也有很大的拓展性。

权限:是用户可以访问的资源,包括页面权限,操作权限,数据权限:

  • 页面权限: 即用户登录系统可以看到的页面,由菜单来控制,菜单包括一级菜单和二级菜单,只要用户有一级和二级菜单的权限,那么用户就可以访问页面。
  • 操作权限: 即页面的功能按钮,包括查看,新增,修改,删除,审核等,用户点击删除按钮时,后台会校验用户角色下的所有权限是否包含该删除权限,如果是,就可以进行下一步操作,反之提示无权限。有的系统要求"可见即可操作",意思是如果页面上能够看到操作按钮,那么用户就可以操作,要实现此需求,这里就需要前端来配合,前端开发把用户的权限信息缓存,在页面判断用户是否包含此权限,如果有,就显示该按钮,如果没有,就隐藏该按钮。某种程度上提升了用户体验,但是在实际场景可自行选择是否需要这样做。
  • 数据权限: 数据权限就是用户在同一页面看到的数据是不同的,比如财务部只能看到其部门下的用户数据,采购部只看采购部的数据,在一些大型的公司,全国有很多城市和分公司,比如杭州用户登录系统只能看到杭州的数据,上海用户只能看到上海的数据,解决方案一般是把数据和具体的组织架构关联起来,举个例子,再给用户授权的时候,用户选择某个角色同时绑定组织如财务部或者合肥分公司,那么该用户就有了该角色下财务部或合肥分公司下的的数据权限。

以上是RBAC的核心设计及模型分析,此模型也叫做RBAC0,而基于核心概念之上,RBAC还提供了扩展模式。包括RBAC1,RBAC2,RBAC3模型。

RBAC1模型

此模型引入了角色继承(Hierarchical Role)概念,即角色具有上下级的关系,角色间的继承关系可分为一般继承关系和受限继承关系。一般继承关系仅要求角色继承关系是一个绝对偏序关系,允许角色间的多继承。而受限继承关系则进一步要求角色继承关系是一个树结构,实现角色间的单继承。这种设计可以给角色分组和分层,一定程度简化了权限管理工作。

RBAC2模型

基于核心模型的基础上,进行了角色的约束控制,RBAC2模型中添加了责任分离关系,其规定了权限被赋予角色时,或角色被赋予用户时,以及当用户在某一时刻激活一个角色时所应遵循的强制性规则。责任分离包括静态责任分离和动态责任分离。主要包括以下约束:

  • 互斥角色: 同一用户只能分配到一组互斥角色集合中至多一个角色,支持责任分离的原则。互斥角色是指各自权限互相制约的两个角色。比如财务部有会计和审核员两个角色,他们是互斥角色,那么用户不能同时拥有这两个角色,体现了职责分离原则。
  • 基数约束: 一个角色被分配的用户数量受限;一个用户可拥有的角色数目受限;同样一个角色对应的访问权限数目也应受限,以控制高级权限在系统中的分配。
  • 先决条件角色: 即用户想获得某上级角色,必须先获得其下一级的角色。

RBAC3模型

即最全面的权限管理,它是基于RBAC0,将RBAC1和RBAC2进行了整合。

Spring Cloud Alibaba 实战(一)准备篇
Spring Cloud Alibaba 实战(二)Nacos篇
Spring Cloud Alibaba 实战(三)Sentinel篇
Spring Cloud Alibaba 实战(四)Oauth2篇
Spring Cloud Alibaba 实战(五)Zuul篇
Spring Cloud Alibaba 实战(六)RocketMQ篇
Spring Cloud Alibaba 实战(七)Seata篇
Spring Cloud Alibaba 实战(八)SkyWalking篇

项目 GitHub 地址:github.com/D2C-Cai/her…

文章分类
后端
文章标签