20210121 Spring Cloud Netflix - 拉勾教育

环境信息

  • 视频 Spring Boot 版本:2.1.6.RELEASE
  • 视频 Spring Cloud 版本:Greenwich.RELEASE
  • 最新 Spring Cloud 版本:2020.0.0
    • 支持的 Spring Boot 版本: 2.4.1

微服务架构

互联网应用架构发展:

  • 单体应用架构
  • 垂直应用架构
  • SOA 应用架构
  • 微服务应用架构

微服务与 SOA:

  • 微服务架构和 SOA 架构相似又不同
  • 很明显的⼀个区别就是服务拆分粒度的不同
  • 微服务架构设计的核心思想就是
  • 微服务架构可以说是 SOA 架构的一种拓展,这种架构模式下拆分粒度更小、服务更独立。把应用拆分成为一个个微小的服务,不同的服务可以使用不同的开发语言和存储,服务之间往往通过 Restful 等轻量级通信。微服务架构关键在于微小、独立、轻量级通信。
  • 微服务是在 SOA 上做的升华,粒度更加细致,微服务架构强调的一个重点是:业务需要彻底的组件化和服务化

微服务架构的优点:

  • 微服务很小,便于特定业务功能的聚焦
  • 微服务很小,每个微服务都可以被一个小团队单独实施(开发、测试、部署上线、运维),团队合作一定程度解耦,便于实施敏捷开发
  • 微服务很小,便于重用和模块之间的组装
  • 微服务很独立,那么不同的微服务可以使用不同的语言开发,松耦合
  • 微服务架构下,更容易引入新技术
  • 微服务架构下,可以更好的实现 DevOps 开发运维一体化;

微服务架构的缺点:

  • 微服务架构下,分布式复杂难以管理,当服务数量增加,管理将越加复杂;
  • 微服务架构下,分布式链路跟踪难等;

微服务架构中的一些概念

  • 服务注册与服务发现
  • 负载均衡
  • 服务熔断
  • 链路追踪
  • API 网关

Spring Cloud 综述

Spring Cloud 是一系列框架的有序集合。利用 Spring Boot 的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用 Spring Boot 的开发风格做到一键启动和部署。 Spring Cloud 并没有重复制造轮子,它只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过 Spring Boot 风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。 —— 百度百科

需要注意, Spring Cloud 其实是一套规范,是一套用于构建微服务架构的规范,而不是一个可以拿来即用的框架(所谓规范就是应该有哪些功能组件,然后组件之间怎么配合,共同完成什么事情)。在这个规范之下第三方的 Netflix 公司开发了一些组件、 Spring 官方开发了一些框架 / 组件,包括第三方的阿里巴巴开发了一套框架 / 组件集合 Spring Cloud Alibaba ,这些才是 Spring Cloud 规范的实现。

Spring Cloud Netflix (SCN)

Spring Cloud Alibaba (SCA)

Spring Cloud 规范及实现意图要解决的问题其实就是微服务架构实施过程中存在的一些问题,比如微服务架构中的服务注册发现问题、网络问题(比如熔断场景)、统一认证安全授权问题、负载均衡问题、链路追踪等问题。

Spring Cloud 是一个微服务相关规范,这个规范意图为搭建微服务架构提供一站式服务, 采用组件(框架)化机制定义一系列组件,各类组件针对性的处理微服务中的特定问题,这些组件共同来构成 Spring Cloud 微服务技术栈

Spring Cloud 核心组件

Spring Cloud 生态圈中的组件,按照发展可以分为第一代 Spring Cloud 组件和第二代 Spring Cloud 组件。

第一代 Spring Cloud(Netflix, SCN) 第二代 Spring Cloud(主要就是 Spring Cloud Alibaba, SCA)
注册中心 Netflix Eureka 阿里巴巴 Nacos
客户端负载均衡 Netflix Ribbon 阿里巴巴 Dubbo LB、 Spring Cloud LoadBalancer
熔断器 Netflix Hystrix 阿里巴巴 Sentinel
网关 Netflix Zuul:性能一般,未来将退出Spring Cloud 生态圈 官方 Spring Cloud Gateway
配置中心 官方 Spring Cloud Config 阿里巴巴 Nacos、携程 Apollo
服务调用 Netflix Feign 阿里巴巴 Dubbo RPC
消息驱动 官方 Spring Cloud Stream
链路追踪 官方 Spring Cloud Sleuth/Zipkin
阿里巴巴 Seata 分布式事务方案

2020.0 版本移除了大量的 spring-cloud-netflix 相关的组件,例如,详见 Spring Cloud 2020.0 Release Notes

  • hystrix
  • ribbon
  • zuul

Spring Cloud 体系结构(组件协同工作机制)

img

Spring Cloud 中的各组件协同工作,才能够支持一个完整的微服务架构。比如:

  • **注册中心 **负责服务的注册与发现,很好将各服务连接起来
  • API 网关 负责转发所有外来的请求
  • 断路器 负责监控服务之间的调用情况,连续多次失败进行熔断保护
  • 配置中心 提供了统一的配置信息管理服务,可以实时的通知各个服务获取最新的配置信息

Spring Cloud 与 Dubbo 对比

Dubbo 是阿里巴巴公司开源的一个高性能优秀的服务框架,基于 RPC 调用,对于目前使用率较高的 Spring Cloud Netflix 来说,它是基于 HTTP 的,所以效率上没有 Dubbo 高,但问题在于 Dubbo 体系的组件不全,不能够提供一站式解决方案,比如服务注册与发现需要借助于 ZooKeeper 等实现,而 Spring Cloud Netflix 则是真正的提供了一站式服务化解决方案,且有 Spring 大家族背景。

前些年, Dubbo 使用率高于 Spring Cloud ,但目前 Spring Cloud 在服务化 / 微服务解决方案中已经有了非常好的发展趋势。

Spring Cloud 与 Spring Boot 的关系

Spring Cloud 只是利用了 Spring Boot 的特点,让我们能够快速的实现微服务组件开发,否则不使用 Spring Boot 的话,我们在使用 Spring Cloud 时,每一个组件的相关 Jar 包都需要我们自己导入配置以及需要开发人员考虑兼容性等各种情况。所以 Spring Boot 是我们快速把 Spring Cloud 微服务技术应用起来的一种方式。

案例准备

  • 自动投递微服务 就是一个服务消费者
  • 简历微服务 就是一个服务提供者

引入依赖管理

<!--spring boot 父启动器依赖-->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.6.RELEASE</version>
</parent>


<dependencyManagement>
    <dependencies>
        <!--spring cloud依赖管理,引入了Spring Cloud的版本-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

第一代 Spring Cloud 核心组件

Eureka 服务注册中心

服务注册中心本质上是为了解耦服务提供者和服务消费者。

img

分布式微服务架构中,服务注册中心用于存储服务提供者地址信息、服务发布相关的属性信息,消费者通过主动查询和被动通知的方式获取服务提供者的地址信息,而不再需要通过硬编码方式得到提供者的地址信息。消费者只需要知道当前系统发布了那些服务,而不需要知道服务具体存在于什么位置,这就是 透明化路由

主流服务中心对比

组件名 语言 CAP 对外暴露接口
Eureka Java AP(自我保护机制,保证可用) HTTP
Consul Go CP HTTP/DNS
ZooKeeper Java CP 客户端
Nacos Java 支持 AP/CP 切换 HTTP

服务注册中心组件 Eureka

Eureka 基础架构:

img

Eureka 交互流程及原理:

img

图例说明:

Eureka 包含两个组件: Eureka Server 和 Eureka Client , Eureka Client 是一个 Java 客户端,用于简化与 Eureka Server 的交互; Eureka Server 提供服务发现的能力,各个微服务启动时,会通过 Eureka Client 向 Eureka Server 进行注册自己的信息(例如网络信息), Eureka Server 会存储该服务的信息;

  1. 图中 us-east-1c 、 us-east-1d , us-east-1e 代表不同的区也就是不同的机房
  2. 图中每一个 Eureka Server 都是一个集群
  3. 图中 Application Service 作为服务提供者向 Eureka Server 中注册服务, Eureka Server 接受到注册事件会在集群和分区中进行数据同步, Application Client 作为消费端(服务消费者)可以从 Eureka Server 中获取到服务注册信息,进行服务调用。
  4. 微服务启动后,会周期性地向 Eureka Server 发送心跳(默认周期为 30 秒)以续约自己的信息
  5. Eureka Server 在一定时间内没有接收到某个微服务节点的心跳, Eureka Server 将会注销该微服务节点(默认 90 秒)
  6. 每个 Eureka Server 同时也是 Eureka Client ,多个 Eureka Server 之间通过复制的方式完成服务注册列表的同步
  7. Eureka Client 会缓存 Eureka Server 中的信息。即使所有的 Eureka Server 节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者

Eureka 通过心跳检测、健康检查和客户端缓存等机制,提高系统的灵活性、可伸缩性和可用性。

代码相关

  • 引入依赖:

    <!-- Eureka Server 依赖 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    
    <!-- Eureka Client 依赖 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    
  • C:\Windows\System32\drivers\etc\hosts 文件中配置:

    127.0.0.1       LagouCloudEurekaServerA
    127.0.0.1       LagouCloudEurekaServerB
    
  • 在Eureka 服务端启动类上增加注解 @EnableEurekaServer,客户端启动类增加注解 @EnableDiscoveryClient(或 @EnableEurekaClient不推荐))

  • 服务端配置示例如下:

    # eureka 客户端配置(和Server交互),Eureka Server 其实也是一个Client
    eureka:
      instance:
        hostname: LagouCloudEurekaServerA  # 当前eureka实例的主机名
      client:
        service-url:
          # 配置客户端所交互的Eureka Server的地址(Eureka Server集群中每一个Server其实相对于其它Server来说都是Client)
          # 集群模式下,defaultZone应该指向其它Eureka Server,如果有更多其它Server实例,逗号拼接即可
          defaultZone: http://LagouCloudEurekaServerB:8762/eureka
        register-with-eureka: true  # 集群模式下可以改成true
        fetch-registry: true # 集群模式下可以改成true
      dashboard:
        enabled: true
    
  • 客户端配置示例如下:

    eureka:
      client:
        service-url:
          # 注册到集群,就把多个Eurekaserver地址使用逗号连接起来即可;注册到单实例(非集群模式),那就写一个就ok
          defaultZone: http://LagouCloudEurekaServerA:8761/eureka,http://LagouCloudEurekaServerB:8762/eureka
      instance:
        prefer-ip-address: true  #服务实例中显示ip,而不是显示主机名(兼容老的eureka版本)
        # ⾃定义实例显示格式,加上版本号,便于多版本管理
        instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
        # 自定义Eureka元数据
        metadata-map:
          cluster: cl1
          region: rn1
    
  • 启动服务端和客户端后,访问 Eureka,URL 为 http://localhost:8761/

使用说明

  • 从 Spring Cloud Edgware 版本开始, @EnableDiscoveryClient@EnableEurekaClient 可省略(推荐不省略)。只需加上相关依赖,并进行相应配置,即可将微服务注册到服务发现组件上。考虑到通用性,推荐使用 @EnableDiscoveryClient

  • 注册到注册中心的应用名是 spring.application.name

  • Eureka 的元数据有两种:标准元数据和自定义元数据。可以使用 DiscoveryClient 获取指定微服务的所有元数据信息

    • 标准元数据: 主机名、 IP 地址、端口号等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。
    • 自定义元数据: 可以使用 eureka.instance.metadata-map 配置,符合 KEY/VALUE 的存储格式。这 些元数据可以在远程客户端中访问。
  • 客户端每隔 30 秒会向注册中心续约(心跳)一次(也称为 报活),如果没有续约,租约在 90 秒后到期,然后客户端会被失效。每隔 30 秒的续约操作我们称之为 心跳检测

    eureka:
      instance:
        # 租约续约间隔时间,默认30秒
        lease-renewal-interval-in-seconds: 30
        # 租约到期,服务时效时间,默认值90秒,服务超过90秒没有发⽣⼼跳,EurekaServer会将服务从列表移除
        lease-expiration-duration-in-seconds: 90
    
  • 客户端每隔 30 秒服务会从注册中心中拉取一份服务列表

    eureka:
      client:
        registry-fetch-interval-seconds: 30
    
  • Eureka Server 会定时(间隔值是 eureka.server.eviction-interval-timer-in-ms,默认 60 s)进⾏检查,如果发现实例在在⼀定时间(此值由客户端设置的 eureka.instance.lease-expiration-duration-in-seconds 定义,默认值为 90 s)内没有收到心跳,则会注销此实例

自我保护机制
  • 如果在 15 分钟内超过 85% 的客户端节点都没有正常的心跳,那么 Eureka 就认为客户端与注册中心出现了网络故障, Eureka Server 自动进入 自我保护机制
  • 默认情况下,如果 Eureka Server 在一定时间内(默认 90 秒)没有接收到某个微服务实例的心跳, Eureka Server 将会移除该实例。但是当 网络分区故障 发生时,微服务与 Eureka Server 之间无法正常通信,而微服务本身是正常运行的,此时不应该移除这个微服务,所以引入了自我保护机制。
  • 当处于自我保护模式时:
    • 不会剔除任何服务实例(可能是服务提供者和 Eureka Server 之间网络问题),保证了大多数服务依然可用
    • Eureka Server 仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上,保证当前节点依然可用,当网络稳定时,当前 Eureka Server 新的注册信息会被同步到其它节点中。
    • 在 Eureka Server 工程中通过 eureka.server.enable-self-preservation 配置可用关停自我保护,默认是打开
  • 建议生产环境打开自我保护机制

Ribbon 负载均衡

  • 负载均衡一般分为 服务器端负载均衡客户端负载均衡

    • 服务器端负载均衡,比如 Nginx 、 F5 这些,请求到达服务器之后由这些负载均衡器根据一定的算法将请求路由到目标服务器处理。
    • 客户端负载均衡,比如我们要说的 Ribbon ,服务消费者客户端会有一个服务器地址列表,调用方在请求前通过一定的负载均衡算法选择一个服务器进行访问,负载均衡算法的执行是在请求客户端进行。
  • Ribbon 是 Netflix 发布的负载均衡器。 Eureka 一般配合 Ribbon 进行使用, Ribbon 利用从 Eureka 中读取到服务信息,在调用服务提供者提供的服务时,会根据一定的算法进行负载。

  • eureka-client 依赖 Ribbon,无需单独引入依赖,在 RestTemplate 上添加 @LoadBalanced 注解即可使用

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
    	return new RestTemplate();
    }
    

Ribbon 负载均衡策略

Ribbon内置了多种负载均衡策略,内部负责复杂均衡的顶级接口为 com.netflix.loadbalancer.IRule

负载均衡策略 描述
RoundRobinRule 轮询 默认超过 10 次获取到的 server 都不可用,会返回 一个空的 server
RandomRule 随机 如果随机到的 server 为 null 或者不可用的话,会 while 不停的循环选取
RetryRule 重试 一定时限内循环重试。默认继承 RoundRobinRule ,也支持自定义注入, RetryRule 会在每次选取之后,对选举的 server 进行判断,是否为 null ,是否 alive ,并且在 500ms 内会不停的选取判断。而 RoundRobinRule 失效的策略是超过 10 次, RandomRule 是没有失效时 间的概念,只要 serverList 没都挂。
BestAvailableRule 最小 连接数 遍历 serverList ,选取出可用的且连接数最小的一 个 server 。该算法里面有一个 LoadBalancerStats 的成员变量,会存储所有 server 的运行状况和连接数。如果选取到的 server 为 null ,那么会调用 RoundRobinRule 重新选取。
AvailabilityFilteringRule 可用过滤 扩展了轮询策略,会先通过默认的轮询选取一个 server ,再去判断该 server 是否超时可用,当前 连接数是否超限,都成功再返回。
ZoneAvoidanceRule 区 域权衡(默认) 扩展了轮询策略,继承了 2 个过滤器: ZoneAvoidancePredicateAvailabilityPredicate ,除了过滤超时和链接数过多的 server ,还会过滤掉不符合要求的 zone 区域 里面的所有节点。
AWS-ZONE:在一个区域 / 机房 内的服务实例中轮询

img

#	在消费者端针对被调用方微服务名称,不加就是全局生效
lagou-service-resume:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载策略调整

Hystrix 熔断器

  • 扇入:代表着该微服务被调用的次数,扇入大,说明该模块复用性好

  • 扇出:该微服务调用其他微服务的个数,扇出大,说明业务逻辑复杂

  • 扇入大是一个好事,扇出大不一定是好事

  • 在微服务架构中,一个应用可能会有多个微服务组成,微服务之间的数据交互通过远程过程调用完成。这就带来一个问题,假设微服务 A 调用微服务 B 和微服务 C ,微服务 B 和微服务 C 又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务 A 的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的 雪崩效应

  • 雪崩效应解决方案:

    • 服务熔断
    • 服务降级
    • 服务限流
  • Hystrix 主要通过以下几点实现容错:

    • 包裹请求:使用 @HystrixCommand 包裹对依赖的调用逻辑。
    • 跳闸机制:当某服务的错误率超过一定的阈值时, Hystrix 可以跳闸,停止请求该服务一段时间。
    • 资源隔离: Hystrix 为每个依赖都维护了一个小型的线程池(舱壁模式)(或者信号量)。如果该线程池已满, 发往该依赖的请求就被立即拒绝,而不是排队等待,从而加速失败判定。
    • 监控: Hystrix 可以近乎实时地监控运行指标和配置的变化,例如成功、失败、超时、以及被拒绝 的请求等。
    • 回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑。回退逻辑由开发人员 自行提供,例如返回一个缺省值。
    • 自我修复:断路器打开一段时间后,会自动进入 半开 状态。

使用方式

  • 消费者端增加注解 @EnableCircuitBreaker

  • 定义服务降级处理方法,并在业务方法上使用 @HystrixCommandfallbackMethod 属性关联到服务降级处理方法,如果没有回退方法,会抛出异常 HystrixRuntimeException

  • 降级(兜底)方法必须和被降级方法相同的方法签名(相同参数列表、相同返回值)

  • 可以在类上使用 @DefaultProperties 注解统一指定整个类中共用的降级(兜底)方法

  • @HystrixCommand 使用示例:

    @GetMapping("/checkStateTimeoutFallback/{userId}")
    @HystrixCommand(
            // 线程池标识,要保持唯一,不唯一的话就共用了
            threadPoolKey = "findResumeOpenStateTimeoutFallback",
            // 线程池细节属性配置
            threadPoolProperties = {
                    @HystrixProperty(name="coreSize",value = "2"), // 线程数
                    @HystrixProperty(name="maxQueueSize",value="20") // 等待队列长度
            },
            // commandProperties熔断的一些细节属性配置
            commandProperties = {
                    // 每一个属性都是一个HystrixProperty
                    @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="2000")
                    // hystrix高级配置,定制工作过程细节
                    ,
                    // 统计时间窗口定义
                    @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds",value = "8000"),
                    // 统计时间窗口内的最小请求数
                    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "2"),
                    // 统计时间窗口内的错误数量百分比阈值
                    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "50"),
                    // 自我修复时的活动窗口长度
                    @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "3000")
            },
            fallbackMethod = "myFallBack"  // 回退方法
    )
    public Integer findResumeOpenStateTimeoutFallback(@PathVariable Long userId) {
        // 使用ribbon不需要我们自己获取服务实例然后选择一个那么去访问了(自己的负载均衡)
        String url = "http://lagou-service-resume/resume/openstate/" + userId;  // 指定服务名
        Integer forObject = restTemplate.getForObject(url, Integer.class);
        return forObject;
    }
    
    
    /*
        定义回退方法,返回预设默认值
        注意:该方法形参和返回值与原始方法保持一致
     */
    public Integer myFallBack(Long userId) {
        return -123333; // 兜底数据
    }
    

Hystrix 舱壁模式(线程池隔离策略)

  • 如果不进行任何设置,所有熔断方法使用一个 Hystrix 线程池( 10 个线程),那么这样的话会导致问题,这个问题并不是扇出链路微服务不可用导致的,而是我们的线程机制导致的,如果方法 1 请求服务 A 把 10 个线程都用了,方法 2 请求处理的时候压根都没法去访问服务 B ,因为没有线程可用,并不是服务 B 不可用
  • 为了避免问题服务请求过多导致正常服务无法访问, Hystrix 不是采用增加线程数,而是单独的为每一个控制方法创建一个线程池的方式,这种模式叫做 舱壁模式 ,也是线程隔离的手段。
  • @HystrixCommand 配置线程池相关属性(见上面示例)即可开启 舱壁模式

Hystrix 工作流程与高级应用

img

  1. 当调用出现问题时,开启一个时间窗( 10s )
  2. 在这个时间窗内,统计调用次数是否达到最小请求数?
    • 如果没有达到,则重置统计信息,回到第 1 步
    • 如果达到了,则统计失败的请求数占所有请求数的百分比,是否达到阈值?
    • 如果达到,则跳闸(不再请求对应服务)
    • 如果没有达到,则重置统计信息,回到第 1 步
  3. 如果跳闸,则会开启一个活动窗口(默认 5s ),每隔 5s , Hystrix 会让一个请求通过,到达那个问题服务,看看是否调用成功,如果成功,重置断路器回到第 1 步,如果失败,回到第 3 步

相关配置可以配置在注解上(见上面示例),也可以配置在配置文件中:

# 配置熔断策略:
hystrix:
  command:
    default:
      circuitBreaker:
        # 强制打开熔断器,如果该属性设置为true,强制断路器进⼊打开状态,将会拒绝所有的请求。 默认false关闭的
        forceOpen: false
        # 触发熔断错误⽐例阈值,默认值50%
        errorThresholdPercentage: 50
        # 熔断后休眠时⻓,默认值5秒
        sleepWindowInMilliseconds: 3000
        # 熔断触发最⼩请求次数,默认值是20
        requestVolumeThreshold: 2
  execution:
    isolation:
      thread:
        # 熔断超时设置,默认为1秒
        timeoutInMilliseconds: 2000

基于 SpringBoot 的健康检查观察跳闸状态:

# springboot中暴露健康检查等断点接⼝
management:
  endpoints:
    web:
      exposure:
        include: "*"
  # 暴露健康接⼝的细节
  endpoint:
    health:
      show-details: always

访问健康检查接口: http://localhost:8090/actuator/health

正常状态是 UP,跳闸是⼀种状态 CIRCUIT_OPEN

需要引入 actuator:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Hystrix Dashboard 断路监控仪表盘

如果我们想看到 Hystrix 相关数据,比如有多少请求、多少成功、多少失败、多少降级等,那么引入 SpringBoot 健康监控之后,访问 /actuator/hystrix.stream 接口可以获取到监控的文字信息,但是不直观,所以 Hystrix 官方还提供了基于图形化的 DashBoard (仪表板)监控平 台。 Hystrix 仪表板可以显示每个断路器(被 @HystrixCommand 注解的方法)的状态。

使用说明
  • 消费者端增加配置:

    /**
     * 在被监控的微服务中注册一个serlvet,后期我们就是通过访问这个servlet来获取该服务的Hystrix监控数据的
     * 前提:被监控的微服务需要引入springboot的actuator功能
     *
     * @return
     */
    @Bean
    public ServletRegistrationBean getServlet() {
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/actuator/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }
    

    访问 http://localhost:8090/actuator/hystrix.stream 后,可以看到数据流,但是不直观,所以需要仪表盘将数据图形化

  • 新增仪表盘模块,lagou-cloud-hystrix-dashboard-9000

    • 主启动类增加注解 @EnableHystrixDashboard

    • 增加依赖

      <!--hystrix-->
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
      </dependency>
      <!--hystrix 仪表盘-->
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
      </dependency>
      
    • 配置文件

      server:
        port: 9000
      spring:
        application:
          name: lagou-cloud-hystrix-dashboard
      eureka:
        client:
          serviceUrl: # eureka server的路径
            defaultZone: http://LagouCloudEurekaServerA:8761/eureka,http://LagouCloudEurekaServerB:8762/eureka
        instance:
          #使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip)
          prefer-ip-address: true
          #自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress
          instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
      
    • 启动后访问 http://localhost:9000/hystrix,将数据流的 url 添加到仪表盘中,http://localhost:8090/actuator/hystrix.stream,可以看到被监控的应用的 Hystrix 统计数据

Hystrix Turbine 聚合监控

  • 在微服务架构下,一个微服务的实例往往是多个(集群化)
  • Hystrix Turbine 聚合(聚合各个实例上的 Hystrix 监控数据)监控
  • 可以使用 Hystrix Turbine 进行聚合监控,它可以把相关微服务的监控数据聚合在一起,便于查看
使用说明
  • 新建 Module,lagou-cloud-hystrix-turbine-9001,启动类增加注解 @EnableTurbine

  • 增加依赖

    <!--hystrix turbine聚合监控-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-turbine</artifactId>
    </dependency>
    
    <!--
        引入eureka客户端的两个原因
        1、老师说过,微服务架构下的服务都尽量注册到服务中心去,便于统一管理
        2、后续在当前turbine项目中我们需要配置turbine聚合的服务,比如,我们希望聚合
           lagou-service-autodeliver这个服务的各个实例的hystrix数据流,那随后
           我们就需要在application.yml文件中配置这个服务名,那么turbine获取服务下具体实例的数据流的
           时候需要ip和端口等实例信息,那么怎么根据服务名称获取到这些信息呢?
              当然可以从eureka服务注册中心获取
    -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    
  • 配置文件

    server:
      port: 9001
    Spring:
      application:
        name: lagou-cloud-hystrix-turbine
    eureka:
      client:
        serviceUrl: # eureka server的路径
          defaultZone: http://LagouCloudEurekaServerA:8761/eureka,http://LagouCloudEurekaServerB:8762/eureka
      instance:
        #使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip)
        prefer-ip-address: true
        #自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress
        instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
        
    #turbine配置
    turbine:
      # appCofing配置需要聚合的服务名称,比如这里聚合自动投递微服务的hystrix监控数据
      # 如果要聚合多个微服务的监控数据,那么可以使用英文逗号拼接,比如 a,b,c
      appConfig: lagou-service-autodeliver
      clusterNameExpression: "'default'"   # 集群默认名称
    
  • 启动后访问 http://localhost:9001/turbine.stream,可以看到信息流,将这个链接放到仪表盘中可以图形化展示集群的 Hystrix 状态

Feign 远程调用组件

  • Feign 是 Netflix 开发的一个 轻量级 RESTful 的 HTTP 服务客户端(用它来发起请求,远程调用的) ,是以 Java 接口注解的方式调用 Http 请求,而不用像 Java 中通过封装 HTTP 请求报文的方式直接调用, Feign 被广泛应用在 Spring Cloud 的解决方案中。
  • 本质:封装了 Http 调用流程,面向接口编程,类似于 Dubbo 的服务调用
  • Feign 会引入 Ribbon 和 Hystrix 依赖
  • @FeignClient 注解的 name 属性用于指定要调用的服务提供者名称,和服务提供者 yml 文件中 spring.application.name 保持一致

使用说明

  • 新增 Module,lagou-service-autodeliver-feign-8096,主启动类增加注解 @EnableFeignClients

  • 增加依赖

    <!-- openfeign -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
    <!--eureka client 客户端依赖引入-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    
    <!--熔断器Hystrix-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    
  • 配置文件

    server:
      port: 8096
    #注册到Eureka服务中心
    eureka:
      client:
        service-url:
          # 注册到集群,就把多个Eurekaserver地址使用逗号连接起来即可;注册到单实例(非集群模式),那就写一个就ok
          defaultZone: http://LagouCloudEurekaServerA:8761/eureka,http://LagouCloudEurekaServerB:8762/eureka
      instance:
        prefer-ip-address: true  #服务实例中显示ip,而不是显示主机名(兼容老的eureka版本)
        # 实例名称: 192.168.1.103:lagou-service-resume:8080,我们可以自定义它
        instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
    spring:
      application:
        name: lagou-service-autodeliver
    
  • Feign 远程接口

    // 原来:http://lagou-service-resume/resume/openstate/ + userId;
    // @FeignClient表明当前类是一个Feign客户端,value指定该客户端要请求的服务名称(登记到注册中心上的服务提供者的服务名称)
    @FeignClient(value = "lagou-service-resume", path = "/resume")
    //@RequestMapping("/resume")
    public interface ResumeServiceFeignClient {
    
    
        // Feign要做的事情就是,拼装url发起请求
        // 我们调用该方法就是调用本地接口方法,那么实际上做的是远程请求
        @GetMapping("/openstate/{userId}")
        public Integer findDefaultResumeState(@PathVariable("userId") Long userId);
    
    }
    

Feign 对 Ribbon 负载均衡的支持

  • 可以通过 ribbon.xx 来进 行全局配置,也可以通过 服务名.ribbon.xx 来对指定服务进行细节配置配置
  • Feign 默认的请求处理超时时长 1s ,如果配置 Ribbon 的超时,则会以 Ribbon 的为准
#针对的被调⽤⽅微服务名称,不加就是全局⽣效
lagou-service-resume:
  ribbon:
    #请求连接超时时间
    #ConnectTimeout: 2000
    #请求处理超时时间
    #ReadTimeout: 5000
    #对所有操作都进⾏重试
    OkToRetryOnAllOperations: true
    ####根据如上配置,当访问到故障请求的时候,它会再尝试访问⼀次当前实例(次数由MaxAutoRetries配置),
    ####如果不⾏,就换⼀个实例进⾏访问,如果还不⾏,再换⼀次实例访问(更换次数由MaxAutoRetriesNextServer配置),
    ####如果依然不⾏,返回失败信息。
    MaxAutoRetries: 0 #对当前选中实例重试次数,不包括第⼀次调⽤
    MaxAutoRetriesNextServer: 0 #切换实例的重试次数
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载策略调整

Feign 对 Hystrix 熔断器的支持

  • 增加配置

    # 开启Feign的熔断功能
    feign:
      hystrix:
        enabled: true
    hystrix:
      command:
        default:
          execution:
            isolation:
              thread:
                ##########################################Hystrix的超时时长设置
                timeoutInMilliseconds: 15000
    
  • 设置 @FeignClientfallback 属性

    @FeignClient(value = "lagou-service-resume", fallback = ResumeFallback.class, path = "/resume")
    
  • 回退实现类

    /**
     * 降级回退逻辑需要定义一个类,实现FeignClient接口,实现接口中的方法
     */
    @Component
    public class ResumeFallback implements ResumeServiceFeignClient {
        @Override
        public Integer findDefaultResumeState(Long userId) {
            return -6;
        }
    }
    

Feign 对请求压缩和响应压缩的支持

  • Feign 对请求压缩和响应压缩的支持
# 开启Feign的熔断功能
feign:
  hystrix:
    enabled: true
  compression:
    request:
      enabled: true # 开启请求压缩
      mime-types: text/html,application/xml,application/json # 设置压缩的数据类型,此处也是默认值
      min-request-size: 2048 # 设置触发压缩的⼤⼩下限,此处也是默认值
    response:
      enabled: true # 开启响应压缩

Feign 的日志级别配置

  • 默认情况下 Feign 的日志没有开启

  • 增加配置

    logging:
      level:
        com.lagou.edu.service.ResumeServiceFeignClient: debug
    
  • 增加配置类

    /**
     * Feign的日志级别(Feign请求过程信息)
     *      NONE:   默认的,不显示任何日志----性能最好
     *      BASIC:  仅记录请求方法、 URL、响应状态码以及执⾏时间----⽣产问题追踪
     *      HEADERS:在 BASIC 级别的基础上,记录请求和响应的 header
     *      FULL:   记录请求和响应的 header、 body 和元数据----适⽤于开发及测试环境定位问题
     */
    @Configuration
    public class FeignLog {
        @Bean
        Logger.Level feignLevel() {
            return Logger.Level.FULL;
        }
    }
    

GateWay 网关组件

  • Spring Cloud GateWay 是 Spring Cloud 的一个全新项目,目标是取代 Netflix Zuul ,它基于 Spring5.0 + SpringBoot 2.x + WebFlux (基于高性能的 Reactor 模式响应式通信框架 Netty ,异步非阻塞模型)等技术开发,性能高于 Zuul ,官方测试, GateWay 是 Zuul 的 1.6 倍,旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式
  • Spring Cloud GateWay 不仅提供统一的路由方式(反向代理)并且基于 Filter (定义过滤器对请求过滤,完成一些功能) 链的方式提供了网关基本的功能,例如:鉴权、流量控制、熔断、路径重写、日志监控等。
  • GateWay 的高可用很简单: 可以启动多个 GateWay 实例来实现高可用,在 GateWay 的上游使用 Nginx 等负载均衡设备进行负载转发以达到高可用的目的。
  • GateWay 核心概念
    • 路由( route ): 网关最基础的部分,也是网关比较基础的工作单元。路由由一个 ID 、一个目标 URL (最终路由到的地址)、一系列的断言(匹配条件判断)和 Filter 过滤器(精细化控制)组成。如果断言为 true ,则匹配该路由。
    • 断言( predicates ):参考了 Java8 中的断言 java.util.function.Predicate ,开发人员可以匹配 Http 请求中的所有内容(包括请求头、请求参数等)(类似于 nginx 中的 location 匹配一样),如果断言与请求相匹配则路由。
    • 过滤器( filter ):一个标准的 Spring WebFilter ,使用过滤器,可以在请求之前或者之后执行业务逻辑。

img

Gateway 工作流程

img

  • 客户端向 Spring Cloud GateWay 发出请求,然后在 GateWay Handler Mapping 中找到与请求相匹配的路由,将其发送到 GateWay Web Handler,Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前( pre )或者之后( post )执行业务逻辑。
  • Filter 在 pre 类型过滤器中可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在 post 类型的过滤器中可以做响应内容、响应头的修改、日志的输出、流量监控等。
  • GateWay 核心逻辑:路由转发 + 执行过滤器链

使用说明

  • 新建 Module,lagou-cloud-gateway-9002,不继承父 pom,继承 spring-boot-starter-parent

  • 增加依赖,不要引入 spring-boot-starter-web 模块

    <!--GateWay 网关-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    
  • 配置文件

    server:
      port: 9002
    eureka:
      client:
        serviceUrl: # eureka server的路径
          defaultZone: http://LagouCloudEurekaServerA:8761/eureka,http://LagouCloudEurekaServerB:8762/eureka
      instance:
        #使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip)
        prefer-ip-address: true
        #自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress
        instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
    
    spring:
      application:
        name: lagou-cloud-gateway
      cloud:
        gateway:
          routes: # 路由可以有多个
            - id: service-autodeliver-router # 我们自定义的路由 ID,保持唯一
              #uri: http://127.0.0.1:8096  # 目标服务地址  自动投递微服务(部署多实例)  动态路由:uri配置的应该是一个服务名称,而不应该是一个具体的服务实例的地址
              uri: lb://lagou-service-autodeliver                                                                    # gateway网关从服务注册中心获取实例信息然后负载后路由
              predicates:                                         # 断言:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默 认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
                - Path=/autodeliver/**
            - id: service-resume-router      # 我们自定义的路由 ID,保持唯一
              #uri: http://127.0.0.1:8081       # 目标服务地址
              #http://localhost:9002/resume/openstate/1545132
              #http://127.0.0.1:8081/openstate/1545132
              uri: lb://lagou-service-resume
              predicates:                                         # 断言:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默 认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
                - Path=/resume/**
              filters:
                - StripPrefix=1
    
  • 访问 http://localhost:9002/autodeliver/checkState/1545132

GateWay 路由规则详解

  • Spring Cloud GateWay 帮我们内置了很多 Predicates 功能,实现了各种路由匹配规则(通过 Header 、请求参数等作为条件)匹配到对应的路由。
  • 官网文档

img

GateWay 动态路由详解

  • GateWay 支持自动从注册中心中获取服务列表并访问,即所谓的动态路由
  • 注意:动态路由设置时, uri 以 lb:// 开头( lb 代表从注册中心获取服务),后面是需要转发到的服务名称
spring:
  cloud:
    gateway:
      routes:
        - id: service-autodeliver-router	# 我们自定义的路由 ID,保持唯一
          uri: lb://lagou-service-autodeliver	# gateway网关从服务注册中心获取实例信息然后负载后路由
          predicates:
            - Path=/autodeliver/**

GateWay 过滤器

从过滤器生命周期(影响时机点)的角度来说,主要有两个 pre 和 post :

生命周期 时机点 作用
pre 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
post 这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header 、收集统计信息和指标、将响应从微服务发送给 客户端等。

从过滤器类型的角度, Spring Cloud GateWay 的过滤器分为 GateWayFilterGlobalFilter 两种:

过滤器类型 影响范围
GateWayFilter 应用到单个路由路由上
GlobalFilter 应用到所有的路由上

内置 GateWayFilter ,参考org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory 及其实现类

自定义全局过滤器实现 IP 访问限制(黑白名单)

/**
 * 定义全局过滤器,会对所有路由生效
 */
@Slf4j
@Component  // 让容器扫描到,等同于注册了
public class BlackListFilter implements GlobalFilter, Ordered {

    // 模拟黑名单(实际可以去数据库或者redis中查询)
    private static List<String> blackList = new ArrayList<>();

    static {
        blackList.add("0:0:0:0:0:0:0:1");  // 模拟本机地址
    }

    /**
     * 过滤器核心方法
     * @param exchange 封装了request和response对象的上下文
     * @param chain 网关过滤器链(包含全局过滤器和单路由过滤器)
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 思路:获取客户端ip,判断是否在黑名单中,在的话就拒绝访问,不在的话就放行
        // 从上下文中取出request和response对象
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        // 从request对象中获取客户端ip
        String clientIp = request.getRemoteAddress().getHostString();
        // 拿着clientIp去黑名单中查询,存在的话就决绝访问
        if(blackList.contains(clientIp)) {
            // 决绝访问,返回
            response.setStatusCode(HttpStatus.UNAUTHORIZED); // 状态码
            log.debug("=====>IP:" + clientIp + " 在黑名单中,将被拒绝访问!");
            String data = "Request be denied!";
            DataBuffer wrap = response.bufferFactory().wrap(data.getBytes());
            return response.writeWith(Mono.just(wrap));
        }

        // 合法请求,放行,执行后续的过滤器
        return chain.filter(exchange);
    }


    /**
     * 返回值表示当前过滤器的顺序(优先级),数值越小,优先级越高
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

测试访问 http://localhost:9002/autodeliver/checkState/1545132,被拦截

Spring Cloud Config 分布式配置中心

  • Spring Cloud Config 是一个分布式配置管理方案,包含了 Server 端和 Client 端两个部分。
    • Server 端:提供配置文件的存储、以接口的形式将配置文件的内容提供出去,通过使用 @EnableConfigServer 注解在 Spring boot 应用中非常简单的嵌入
    • Client 端:通过接口获取配置数据并初始化自己的应用
  • Config Server 是集中式的配置服务,用于集中管理应用程序各个环境下的配置。 默认使用 Git 存储配置文件内容,也可以 SVN 。

Config Server 端使用说明

  • 新建 Module,lagou-cloud-configserver-9006,主启动类增加注解 @EnableConfigServer

  • 增加依赖

    <!--config配置中心服务端-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-server</artifactId>
    </dependency>
    
  • 配置文件

    server:
      port: 9006
    #注册到Eureka服务中心
    eureka:
      client:
        service-url:
          # 注册到集群,就把多个Eurekaserver地址使用逗号连接起来即可;注册到单实例(非集群模式),那就写一个就ok
          defaultZone: http://LagouCloudEurekaServerA:8761/eureka,http://LagouCloudEurekaServerB:8762/eureka
      instance:
        prefer-ip-address: true  #服务实例中显示ip,而不是显示主机名(兼容老的eureka版本)
        # 实例名称: 192.168.1.103:lagou-service-resume:8080,我们可以自定义它
        instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
    
    spring:
      application:
        name: lagou-cloud-configserver
      cloud:
        config:
          server:
            git:
              uri: https://gitee.com/liuxing5yu/lagou-config-repo.git #配置git服务地址
    #          username: 517309804@qq.com #配置git用户名
    #          password: yingdian12341 #配置git密码
              search-paths:
                - lagou-config-repo
          # 读取分支
          label: master
    
  • 测试访问 GET http://localhost:9006/master/lagou-service-resume-dev.yml当 Git 库中配置文件的内容变化时,Config Server 始终能够获取到最新的

Config Client 端使用说明

  • 增加依赖

    <!--Config 客户端依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-client</artifactId>
    </dependency>
    
  • 增加配置,在 bootstrap.xml 中配置

    spring:
      cloud:
        # config客户端配置,和ConfigServer通信,并告知ConfigServer希望获取的配置信息在哪个文件中
        config:
          name: lagou-service-resume  #配置文件名称
          profile: dev  #后缀名称
          label: master #分支名称
          uri: http://localhost:9006    #ConfigServer配置中心地址
    
  • 增加 Controller

    @RestController
    @RequestMapping("/config")
    public class ConfigController {
    
        // 和取本地配置信息一样
        @Value("${lagou.message}")
        private String lagouMessage;
        @Value("${mysql.url}")
        private String mysqlUrl;
    
    
        // 内存级别的配置信息
        // 数据库,redis配置信息
    
        @GetMapping("/viewconfig")
        public String viewconfig() {
            return "lagouMessage==>" + lagouMessage  + " mysqlUrl=>" + mysqlUrl;
        }
    }
    
  • 测试访问,GET http://localhost:8080/config/viewconfig,可以获取到配置,但是 Git 中的配置文件变化时无法获取到最新配置

Config 配置手动刷新

  • 客户端增加依赖 springboot-starter-actuator

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    
  • 客户端增加配置

    management:
      endpoints:
        web:
          exposure:
            include: refresh
            
    或者
    
    management:
      endpoints:
        web:
          exposure:
            include: "*"
    
  • 配置类上增加注解 @RefreshScope

  • 修改 Git 上配置后,访问 POST http://localhost:8080/actuator/refresh,可以刷新配置

Config 配置自动更新

  • 可以结合消息总线( Bus )实现分布式配置的自动更新( Spring Cloud Config+Spring Cloud Bus )
  • 所谓消息总线 Bus ,即我们经常会使用 MQ 消息代理构建一个共用的 Topic ,通过这个 Topic 连接各个微服务实例, MQ 广播的消息会被所有在注册中心的微服务实例监听和消费。 换而言之就是通过一个主题连接各个微服务,打通脉络。
  • Spring Cloud Bus (基于 MQ 的,支持 RabbitMq/Kafka ) 是 Spring Cloud 中的消息总线方案, Spring Cloud Config + Spring Cloud Bus 结合可以实现配置信息的自动更新。

实现方式:

  • Config Server 服务端添加消息总线支持

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bus-amqp</artifactId>
    </dependency>
    
  • Config Server 添加配置

    spring:
      rabbitmq:
        host: 127.0.0.1
        port: 5672
        username: guest
        password: guest
    
  • Config Server 暴露端口

    management:
      endpoints:
        web:
          exposure:
            include: bus-refresh
            
    或者
    
    management:
      endpoints:
        web:
          exposure:
            include: "*"
    
  • 重启各个服务,更改配置之后,向配置中心服务端发送 post 请求 http://localhost:9003/actuator/bus-refresh,各个客户端配置即可自动刷新

  • 定向更新:发起刷新请求 http://localhost:9006/actuator/bus-refresh/lagou-serviceresume:8080 ,即为最后面跟上要定向刷新的实例的 服务名:端口号 即可

Spring Cloud Stream 消息驱动组件

  • Spring Cloud Stream 消息驱动组件帮助我们更快速,更方便,更友好的去构建消息驱动微服务。
  • 不同的 MQ 消息中间件内部机制包括使用方式都会有所不同, Spring Cloud Stream 进行了很好的上层抽象,可以让我们与具体消息中间件解耦合,屏蔽掉了底层具体 MQ 消息中间件的细节差异,就像 Hibernate 屏蔽掉了具体数据库( MySQL/Oracle 一样)
  • 目前 Spring Cloud Stream 支持 RabbitMQ 和 Kafka
  • 本质: 屏蔽掉了底层不同 MQ 消息中间件之间的差异,统一了 MQ 的编程模型,降低了学习、开发、维护 MQ 的成本
  • Binder 绑定器 是 Spring Cloud Stream 中非常核心的概念,就是通过它来屏蔽底层不同 MQ 消息中间件的细节差异,当需要更换为其他消息中间件时,我们需要做的就是更换对应的 Binder 绑定器而不需要修改任何应用逻辑

常见问题及解决方案

Eureka 服务发现慢的原因

  • Eureka 服务发现慢的原因主要有两个,一部分是因为服务缓存导致的,另一部分是因为客户端缓存导致的。
  • 服务注册到注册中心后,服务实例信息是存储在注册表中的,也就是内存中。但 Eureka 为了提高响应速度,在内部做了优化,加入了 两层的缓存结构,将 Client 需要的实例信息,直接缓存起来,获取的时候直接从缓存中拿数据然后响应给 Client 。
    • 第一层缓存是 readOnlyCacheMapreadOnlyCacheMap 是采用 ConcurrentHashMap 来存储数据的,主要负责定时与 readWriteCacheMap 进行数据同步,默认同步时间为 30 秒一次
    • 第二层缓存是 readWriteCacheMapreadWriteCacheMap 采用 Guava 来实现缓存。缓存过期时间默认为 180 秒,当服务下线、过期、注册、状态变更等操作都会清除此缓存中的数据。
  • Client 获取服务实例数据时,会先从一级缓存中获取,如果一级缓存中不存在,再从二级缓存中获取,如果二级缓存也不存在,会触发缓存的加载,从存储层拉取数据到缓存中,然后再返回给 Client 。
  • Eureka 之所以设计二级缓存机制,也是为了提高 Eureka Server 的响应速度,缺点是缓存会导致 Client 获取不到最新的服务实例信息,然后导致无法快速发现新的服务和已下线的服务。
  • 了解了服务端的实现后,想要解决这个问题就变得很简单了,我们可以缩短只读缓存的更新时间( eureka.server.response-cache-update-interval-ms )让服务发现变得更加及时,或者直接将只读缓存关闭( eureka.server.use-read-onlyresponse-cache = false ),多级缓存也导致 C 层面(数据一致性)很薄弱。
  • Eureka Server 中会有定时任务去检测失效的服务,将服务实例信息从注册表中移除,也可以将这个失效检测的时间缩短,这样服务下线后就能够及时从注册表中清除。
  • 客户端缓存主要分为两块内容,一块是 Eureka Client 缓存,一块是 Ribbon 缓存
  • 将这些服务端缓存和客户端缓存的时间全部缩短后,跟默认的配置时间相比,快了很多。我们通过调整参数的方式来尽量加快服务发现的速度,但是还是不能完全解决报错的问题,间隔时间设置为 3 秒,也还是会有间隔。所以我们一般都会 开启重试功能,当路由的服务出现问题时,可以重试到另一个服务来保证这次请求的成功。

Eureka Client 缓存

  • Eureka Client 负责跟 Eureka Server 进行交互,在 Eureka Client 中的 com.netflix.discovery.DiscoveryClient.initScheduledTasks() 方法中,初始化了一个 CacheRefreshThread 定时任务专门用来拉取 Eureka Server 的实例信息到本地。
  • 所以我们需要缩短这个定时拉取服务信息的时间间隔( eureka.client.registryFetchIntervalSeconds )来快速发现新的服务

Ribbon 缓存

  • Ribbon 会从 Eureka Client 中获取服务信息, ServerListUpdater 是 Ribbon 中负责服务实例更新的组件,默认的实现是 PollingServerListUpdater ,通过线程定时去更新实例信息。定时刷新的时间间隔默认是 30 秒,当服务停止或者上线后,这边最快也需要 30 秒才能将实例信息更新成最新的。我们可以将这个时间调短一点,比如 3 秒。
  • 刷新间隔的参数是通过 getRefreshIntervalMs 方法来获取的,方法中的逻辑也是从 Ribbon 的配置中进行取值的。

Spring Cloud 各组件超时

  • 在 Spring Cloud 中,应用的组件较多,只要涉及通信,就有可能会发生请求超时。那么如何设置超时时间? 在 Spring Cloud 中,超时时间只需要重点关注 Ribbon 和 Hystrix 即可。
  • Ribbon 如果采用的是服务发现方式,就可以通过服务名去进行转发,需要配置 Ribbon 的超时。 Ribbon 的超时可以配置全局的 ribbon.ReadTimeout 和 ribbon.ConnectTimeout 。也可以在前面指定服务名,为每个服务单独配置,比如 user-service.ribbon.ReadTimeout
  • 其次是 Hystrix 的超时配置, Hystrix 的超时时间要大于 Ribbon 的超时时间,因为 Hystrix 将请求包装了起来,特别需要注意的是,如果 Ribbon 开启了重试机制,比如重试 3 次, Ribbon 的超时为 1 秒,那么 Hystrix 的超时时间应该大于 3 秒,否则就会出现 Ribbon 还在重试中,而 Hystrix 已经超时的现象
  • Hystrix 全局超时配置就可以用 default 来代替具体的 command 名称。 Hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds = 3000 如果想对具体的 command 进行配置,那么就需要知道 command 名称的生成规则,才能准确的配置。
  • 如果我们使用 @HystrixCommand 的话,可以自定义 commandKey 。如果使用 FeignClient 的话,可以为 FeignClient 来指定超时时间: Hystrix.command.UserRemoteClient.execution.isolation.thread.timeoutInMilliseconds = 3000
  • 如果想对 FeignClient 中的某个接口设置单独的超时,可以在 FeignClient 名称后加上具体的方法: Hystrix.command.UserRemoteClient#getUser(Long).execution.isolation.thread.timeoutInMilliseconds = 3000
  • Feign 本身也有超时时间的设置,如果此时设置了 Ribbon 的时间就以 Ribbon 的时间为准,如果没设置 Ribbon 的时间但配置了 Feign 的时间,就以 Feign 的时间为准。 Feign 的时间同样也配置了连接超时时间( Feign.client.config.服务名称.connectTimeout )和读取超时时间( Feign.client.config.服务名称.readTimeout )。
  • 建议配置 Ribbon 超时时间和 Hystrix 超时时间即可。

Spring Cloud 高级进阶

微服务监控之分布式链路追踪技术 Sleuth + Zipkin

  • 分布式链路追踪技术已然成熟,产品也不少,国内外都有,比如:
    • Spring Cloud Sleuth + Twitter Zipkin
    • 阿里巴巴的“鹰眼”
    • 大众点评的“CAT”
    • 美团的“Mtrace”
    • 京东的“Hydra”
    • 新浪的“Watchman”
    • Apache Skywalking。

分布式链路追踪技术核心思想

  • 当下主流的的分布式链路追踪技术 / 系统所基于的理念都来自于 Google 的一篇论文《 Dapper , a Large-Scale Distributed Systems TracingInfrastructure 》
  • 一条链路通过 TraceId 唯一标识, span 标识发起的请求信息,各 span 通过 parrentId 关联起来

img

  • Trace : 服务追踪的追踪单元是从客户发起请求( request )抵达被追踪系统的边界开始,到被追踪系统向客户返回响应( response )为止的过程

  • Trace ID : 为了实现请求跟踪,当请求发送到分布式系统的入口端点时,只需要服务跟踪框架为该请求创建一个唯一的跟踪标识 Trace ID ,同时在分布式系统内部流转的时候,框架失踪保持该唯一标识,直到返回给请求方一个 Trace 由一个或者多个 Span 组成,每一个 Span 都有一个 SpanId , Span 中会记录 TraceId ,同时还有一个叫做 ParentId ,指向了另外一个 Span 的 SpanId ,表明父子关系,其实本质表达了依赖关系

  • Span ID : 为了统计各处理单元的时间延迟,当请求到达各个服务组件时,也是通过一个唯一标识 Span ID 来标记它的开始,具体过程以及结束。对每一个 Span 来说,它必须有开始和结束两个节点,通过记录开始 Span 和结束 Span 的时间戳,就能统计出该 Span 的时间延迟,除了时间戳记录之外,它还可以包含一些其他元数据,比如时间名称、请求信息等。

  • 每一个 Span 都会有一个唯一跟踪标识 Span ID ,若干个有序的 Span 就组成了一个 Trace 。

  • Span 可以认为是一个日志数据结构,在一些特殊的时机点会记录了一些日志信息,比如有时间戳、 spanId、 TraceId , ParentId 等, Span 中也抽象出了另外一个概念,叫做事件,核心事件如下:

    • CS : client send/start 客户端 / 消费者发出一个请求,描述的是一个 span 开始
    • SR/SS : server received/start 服务端 / 生产者接收请求 SR-CS 属于请求发送的网络延迟
    • SS : server send/finish 服务端 / 生产者发送应答 SS-SR 属于服务端消耗时间
    • CR/CF : client received/finished 客户端 / 消费者接收应答 CR-SS 表示回复需要的时间(响应的网络延迟)
  • Spring Cloud Sleuth (追踪服务框架)可以追踪服务之间的调用, Sleuth 可以记录一个服务请求经过哪些服务、服务处理时长等,根据这些,我们能够理清各微服务间的调用关系及进行问题追踪分析。

    • 耗时分析:通过 Sleuth 了解采样请求的耗时,分析服务性能问题(哪些服务调用比较耗时)
    • 链路优化:发现频繁调用的服务,针对性优化等,Sleuth 就是通过记录日志的方式来记录踪迹数据的
  • 注意:我们往往把 Spring Cloud Sleuth 和 Zipkin 一起使用,把 Sleuth 的数据信息发送给 Zipkin 进行聚合,利用 Zipkin 存储并展示数据。

使用说明

集成 Sleuth
  • 每一个需要被追踪踪迹的微服务工程都引入依赖坐标

    <!--链路追踪-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>
    
  • 每一个需要被追踪踪迹的微服务都修改 application.yml 配置文件,添加日志级别

    #分布式链路追踪
    logging:
      level:
        org.springframework.web.servlet.DispatcherServlet: debug
        org.springframework.cloud.sleuth: debug
    

    请求到来时,我们在控制台可以观察到 Sleuth 输出的日志(全局 TraceId、 SpanId 等)。

    2021-01-20 17:14:14.818 DEBUG [lagou-cloud-gateway,7d14fd5b74c6794e,7d14fd5b74c6794e,false] 23640 --- [ctor-http-nio-7]
    
    2021-01-20 17:14:14.815 DEBUG [lagou-service-autodeliver,7d14fd5b74c6794e,61c2d85f7bf33b31,false] 8640 --- [nio-8090-exec-5] o.s.web.servlet.DispatcherServlet        : Completed 200 OK
        
    2021-01-20 17:14:14.813 DEBUG [lagou-service-resume,7d14fd5b74c6794e,68397c677661323b,false] 19856 --- [nio-8081-exec-5] o.s.web.servlet.DispatcherServlet        : Completed 200 OK
    

    这样的日志首先不容易阅读观察,另外日志分散在各个微服务服务器上,接下来我们使用 zipkin 统一聚合轨迹日志并进行存储展示

Zipkin Server
  • 在 MySQL 库中初始化表,初始化脚本地址

  • 新建 Module,lagou-cloud-zipkin-server-9411,主启动类增加注解 @EnableZipkinServer

  • 增加依赖

    <!--zipkin-server的依赖坐标-->
    <dependency>
        <groupId>io.zipkin.java</groupId>
        <artifactId>zipkin-server</artifactId>
        <version>2.12.3</version>
        <exclusions>
            <!--排除掉log4j2的传递依赖,避免和springboot依赖的日志组件冲突-->
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-log4j2</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
    <!--zipkin-server ui界面依赖坐标-->
    <dependency>
        <groupId>io.zipkin.java</groupId>
        <artifactId>zipkin-autoconfigure-ui</artifactId>
        <version>2.12.3</version>
    </dependency>
    
    
    <!--zipkin针对mysql持久化的依赖-->
    <dependency>
        <groupId>io.zipkin.java</groupId>
        <artifactId>zipkin-autoconfigure-storage-mysql</artifactId>
        <version>2.12.3</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    <!--操作数据库需要事务控制-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
    </dependency>
    
  • 配置文件

    server:
      port: 9411
    management:
      metrics:
        web:
          server:
            auto-time-requests: false # 关闭自动检测
    spring:
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/lagou?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=UTC
        username: root
        password: 123456
        druid:
          initialSize: 10
          minIdle: 10
          maxActive: 30
          maxWait: 50000
    # 指定zipkin持久化介质为mysql
    zipkin:
      storage:
        type: mysql
    
  • 页面访问:http://localhost:9411/zipkin/

集成 Zipkin
  • 每一个需要被追踪踪迹的微服务工程都引入依赖坐标

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zipkin</artifactId>
    </dependency>
    
  • application.yml 中添加对 Zipkin Server 的引用

    spring:
      zipkin:
        base-url: http://127.0.0.1:9411 # zipkin server的请求地址
        sender:
          # web 客户端将踪迹日志数据通过网络请求的方式传送到服务端,另外还有配置
          # kafka/rabbit 客户端将踪迹日志数据传递到mq进行中转
          type: web
      sleuth:
        sampler:
          # 采样率 1 代表100%全部采集 ,默认0.1 代表10% 的请求踪迹数据会被采集
          # 生产环境下,请求量非常大,没有必要所有请求的踪迹数据都采集分析,对于网络包括server端压力都是比较大的,可以配置采样率采集一定比例的请求的踪迹数据进行分析即可
          probability: 1
    

微服务统一认证方案 Spring Cloud OAuth2 + JWT

  • 微服务架构下统一认证思路
    • 基于 Session 的认证方式
      • 在分布式的环境下,基于 Session 的认证会出现一个问题,每个应用服务都需要在 Session 中存储用户身份信息,通过负载均衡将本地的请求分配到另一个应用服务需要将 Session 信息带过去,否则会重新认证。我们可以使用 Session 共享、 Session 黏贴等方案。
      • Session 方案也有缺点,比如基于 Cookie ,移动端不能有效使用等
    • 基于 token 的认证方式
      • 基于 token 的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可以把 token 存在任意地方,并且可以实现 web 和 app 统一认证机制。
      • 其缺点也很明显, token 由于自包含信息,因此一般数据量较大,而且每次请求都需要传递,因此比较占带宽。另外, token 的签名验签操作也会给 CPU 带来额外的处理负担。

OAuth2 开放授权协议/标准

  • OAuth (开放授权)是一个开放协议 / 标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。
  • 允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容
  • OAuth2 是 OAuth 协议的延续版本,但不向后兼容 OAuth1,即完全废止了 OAuth1 。
  • 什么情况下需要使用 OAuth2?
    • 第三方授权登录 的场景:比如,我们经常登录一些网站或者应用的时候,可以选择使用第三方授权登录的方式,比如:微信授权登录、 QQ 授权登录、微博授权登录等,这是典型的 OAuth2 使用场景。
    • 单点登录 的场景:如果项目中有很多微服务或者公司内部有很多服务,可以专门做一个认证中心(充当认证平台角色),所有的服务都要到这个认证中心做认证,只做一次登录,就可以在多个授权范围内的服务中自由串行。

img

  • 资源所有者( Resource Owner ):可以理解为用户自己
  • 客户端( Client ):我们想登陆的网站或应用,比如拉勾网
  • 认证服务器( Authorization Server ):可以理解为微信或者 QQ
  • 资源服务器( Resource Server ):可以理解为微信或者 QQ

OAuth2 的颁发 Token 授权方式 :

  • 授权码( authorization-code )
  • 密码式( password ):提供用户名 + 密码换取 token 令牌
  • 隐藏式( implicit )
  • 客户端凭证( client credentials )

授权码模式使用到了回调地址,是最复杂的授权方式,微博、微信、 QQ 等第三方登录就是这种模式。接下来重点讲解接口对接中常使用的 password 密码模式(提供用户名 + 密码换取 token )。

  • Spring Cloud OAuth2 是 Spring Cloud 体系对 OAuth2 协议的实现,可以用来做多个微服务的统一认证(验证身份合法性)授权(验证权限)。通过向 OAuth2 服务(统一认证授权服务)发送某个类型的 grant_type 进行集中认证和授权,从而获得 access_token (访问令牌),而这个 token 是受其他微服务信任的。
  • 注意:使用 OAuth2 解决问题的本质是,引入了一个认证授权层,认证授权层连接了资源的拥有者,在授权层里面,资源的拥有者可以给第三方应用授权去访问我们的某些受保护资源。
  • 注意:在我们统一认证的场景中, Resource Server 其实就是我们的各种受保护的微服务,微服务中的各种 API 访问接口就是资源,发起 http 请求的浏览器就是 Client 客户端(对应为第三方应用)

img

搭建认证服务器(Authorization Server)

  • 新建 Module,lagou-cloud-oauth-server-9999

  • 增加依赖

    <!--导入Eureka Client依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    
    
    <!--导入spring cloud oauth2依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.security.oauth.boot</groupId>
                <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.security.oauth.boot</groupId>
        <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        <version>2.1.11.RELEASE</version>
    </dependency>
    <!--引入security对oauth2的支持-->
    <dependency>
        <groupId>org.springframework.security.oauth</groupId>
        <artifactId>spring-security-oauth2</artifactId>
        <version>2.3.4.RELEASE</version>
    </dependency>
    
  • 配置文件

    server:
      port: 9999
    eureka:
      client:
        serviceUrl: # eureka server的路径
          defaultZone: http://LagouCloudEurekaServerA:8761/eureka,http://LagouCloudEurekaServerB:8762/eureka
      instance:
        #使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip)
        prefer-ip-address: true
        #自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress
        instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
    
  • 增加配置类,com.lagou.edu.config.OauthServerConfigurer

    /**
     * 当前类为Oauth2 server的配置类(需要继承特定的父类 AuthorizationServerConfigurerAdapter)
     */
    @Configuration
    @EnableAuthorizationServer  // 开启认证服务器功能
    public class OauthServerConfigurer extends AuthorizationServerConfigurerAdapter {
    
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        /**
         * 认证服务器最终是以api接口的方式对外提供服务(校验合法性并生成令牌、校验令牌等)
         * 那么,以api接口方式对外的话,就涉及到接口的访问权限,我们需要在这里进行必要的配置
         *
         * @param security
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            // 相当于打开endpoints 访问接口的开关,这样的话后期我们能够访问该接口
            security
                    // 允许客户端表单认证
                    .allowFormAuthenticationForClients()
                    // 开启端口/oauth/token_key的访问权限(允许)
                    .tokenKeyAccess("permitAll()")
                    // 开启端口/oauth/check_token的访问权限(允许)
                    .checkTokenAccess("permitAll()");
        }
    
        /**
         * 客户端详情配置,
         * 比如client_id,secret
         * 当前这个服务就如同QQ平台,拉勾网作为客户端需要qq平台进行登录授权认证等,提前需要到QQ平台注册,QQ平台会给拉勾网
         * 颁发client_id等必要参数,表明客户端是谁
         *
         * @param clients
         * @throws Exception
         */
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            super.configure(clients);
    
    
            // 从内存中加载客户端详情
    
            clients.inMemory()// 客户端信息存储在什么地方,可以在内存中,可以在数据库里
                   .withClient("client_lagou")  // 添加一个client配置,指定其client_id
                   .secret("abcxyz")                   // 指定客户端的密码/安全码
                   .resourceIds("autodeliver")         // 指定客户端所能访问资源id清单,此处的资源id是需要在具体的资源服务器上也配置一样
                   // 认证类型/令牌颁发模式,可以配置多个在这里,但是不一定都用,具体使用哪种方式颁发token,需要客户端调用的时候传递参数指定
                   .authorizedGrantTypes("password", "refresh_token")
                   // 客户端的权限范围,此处配置为all全部即可
                   .scopes("all");
    
        }
    
        /**
         * 认证服务器是玩转token的,那么这里配置token令牌管理相关(token此时就是一个字符串,当下的token需要在服务器端存储,
         * 那么存储在哪里呢?都是在这里配置)
         *
         * @param endpoints
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.tokenStore(tokenStore())  // 指定token的存储方法
                     .tokenServices(authorizationServerTokenServices())   // token服务的一个描述,可以认为是token生成细节的描述,比如有效时间多少等
                     .authenticationManager(authenticationManager) // 指定认证管理器,随后注入一个到当前类使用即可
                     .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
        }
    
    
        /*
            该方法用于创建tokenStore对象(令牌存储对象)
            token以什么形式存储
         */
        public TokenStore tokenStore() {
            return new InMemoryTokenStore();
        }
    
        /**
         * 该方法用户获取一个token服务对象(该对象描述了token有效期等信息)
         */
        public AuthorizationServerTokenServices authorizationServerTokenServices() {
            // 使用默认实现
            DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
            defaultTokenServices.setSupportRefreshToken(true); // 是否开启令牌刷新
            defaultTokenServices.setTokenStore(tokenStore());
    
            // 设置令牌有效时间(一般设置为2个小时)
            defaultTokenServices.setAccessTokenValiditySeconds(20); // access_token就是我们请求资源需要携带的令牌
            // 设置刷新令牌的有效时间
            defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
    
            return defaultTokenServices;
        }
    }
    
  • 增加配置类,com.lagou.edu.config.SecurityConfigurer

    /**
     * 该配置类,主要处理用户名和密码的校验等事宜
     */
    @Configuration
    public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        /**
         * 注册一个认证管理器对象到容器
         */
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
    
        /**
         * 密码编码对象(密码不进行加密处理)
         *
         * @return
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
            return NoOpPasswordEncoder.getInstance();
        }
    
        /**
         * 处理用户名和密码验证事宜
         * 1)客户端传递username和password参数到认证服务器
         * 2)一般来说,username和password会存储在数据库中的用户表中
         * 3)根据用户表中数据,验证当前传递过来的用户信息的合法性
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // 在这个方法中就可以去关联数据库了,当前我们先把用户信息配置在内存中
            // 实例化一个用户对象(相当于数据表中的一条用户记录)
            UserDetails user = new User("admin", "123456", new ArrayList<>());
            auth.inMemoryAuthentication().withUser(user).passwordEncoder(passwordEncoder);
        }
    }
    
  • 测试访问

    • 获取 token:http://localhost:9999/oauth/token?client_secret=abcxyz&grant_type=password&username=admin&password=123456&client_id=client_lagou
    • 校验token:http://localhost:9999/oauth/check_token?token=edc4f2cf-89d9-4407-b962-08c1d773beb8
    • 刷新token:http://localhost:9999/oauth/token?grant_type=refresh_token&client_id=client_lagou&client_secret=abcxyz&refresh_token=ef3a58b4-04d3-4421-a79b-fd7904519eb8
  • 资源服务器(希望访问被认证的微服务) Resource Server 配置,这里配置在 autodeliver 微服务上

    <!--导入spring cloud oauth2依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.security.oauth.boot</groupId>
                <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.security.oauth.boot</groupId>
        <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        <version>2.1.11.RELEASE</version>
    </dependency>
    <!--引入security对oauth2的支持-->
    <dependency>
        <groupId>org.springframework.security.oauth</groupId>
        <artifactId>spring-security-oauth2</artifactId>
        <version>2.3.4.RELEASE</version>
    </dependency>
    
    @Configuration
    @EnableResourceServer  // 开启资源服务器功能
    @EnableWebSecurity  // 开启web访问安全
    public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {
    
        /**
         * 该方法用于定义资源服务器向远程认证服务器发起请求,进行token校验等事宜
         * @param resources
         * @throws Exception
         */
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    
            // 设置当前资源服务的资源id
            resources.resourceId("autodeliver");
            // 定义token服务对象(token校验就应该靠token服务对象)
            RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
            // 校验端点/接口设置
            remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token");
            // 携带客户端id和客户端安全码
            remoteTokenServices.setClientId("client_lagou");
            remoteTokenServices.setClientSecret("abcxyz");
    
            // 别忘了这一步
            resources.tokenServices(remoteTokenServices);
    
        }
    
    
        /**
         * 场景:一个服务中可能有很多资源(API接口)
         *    某一些API接口,需要先认证,才能访问
         *    某一些API接口,压根就不需要认证,本来就是对外开放的接口
         *    我们就需要对不同特点的接口区分对待(在当前configure方法中完成),设置是否需要经过认证
         *
         * @param http
         * @throws Exception
         */
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http    // 设置session的创建策略(根据需要创建即可)
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                    .and()
                    .authorizeRequests()
                    .antMatchers("/autodeliver/**").authenticated() // autodeliver为前缀的请求需要认证
                    .antMatchers("/demo/**").authenticated()  // demo为前缀的请求需要认证
                    .anyRequest().permitAll();  //  其他请求不认证
        }
    
    }
    
  • 测试访问:

    • http://localhost:8096/autodeliver/checkState/1545132 返回错误

      {
          "error": "unauthorized",
          "error_description": "Full authentication is required to access this resource"
      }
      
    • http://localhost:8096/autodeliver/checkState/1545132?access_token=a5afb525-4c8d-4879-9592-b33375b956a1 返回正确结果

AuthorizationServerConfigurerAdapter 的三个 configure 方法

  • configure(ClientDetailsServiceConfigurer clients)

    用来配置客户端详情服务( ClientDetailsService ),客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息

  • configure(AuthorizationServerEndpointsConfigurer endpoints)

    用来配置令牌( token )的访问端点和令牌服务( token services )

  • configure(AuthorizationServerSecurityConfigurer security)

    用来配置令牌端点的安全约束

关于 TokenStore

  • InMemoryTokenStore

    默认采用,它可以完美的工作在单服务器上(即访问并发量压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行尝试,你 可以在开发的时候使用 它来进行管理,因为不会被保存到磁盘中,所以更易于调试。

  • JdbcTokenStore

    这是一个基于 JDBC 的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时, 你 可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把 spring-jdbc 这个依赖加入到你的 classpath 当中。

  • JwtTokenStore

这个版本的全称是 JSON Web Token ( JWT ),它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息, JwtTokenStore 不会保存任何数据。

JWT 改造统一认证授权中心的令牌存储机制

  • 通过上边的测试我们发现,当资源服务和授权服务不在一起时资源服务使用 RemoteTokenServices 远程请求授权 服务验证 token ,如果访问量较大将会影响系统的性能。

  • 解决上边问题: 令牌采用 JWT 格式即可解决上边的问题,用户认证通过会得到一个 JWT 令牌, JWT 令牌中已经包括了用户相关的信息,客户端只需要携带 JWT 访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权。

  • JSON Web Token ( JWT )是一个开放的行业标准( RFC 7519 ),它定义了一种简介的、自包含的协议格式,用于在通信双方传递 JSON 对象,传递的信息经过数字签名可以被验证和信任。 JWT 可以使用 HMAC 算法或使用 RSA 的公钥 / 私钥对来签名,防止被篡改。

  • JWT 令牌由三部分组成,每部分中间使用点( . )分隔,比如: xxxxx.yyyyy.zzzzz

    • Header

      头部包括令牌的类型(即 JWT )及使用的哈希算法(如 HMAC SHA256 或 RSA ),例如

      {
        "alg": "HS256",
        "typ": "JWT"
      }
      

      将上边的内容使用 Base64 Url 编码,得到一个字符串就是 JWT 令牌的第一部分。

    • Payload

      第二部分是负载,内容也是一个 json 对象,它是存放有效信息的地方,它可以存放 jwt 提供的现成字段,比 如: iss (签发者) , exp (过期时间戳) , sub (面向的用户)等,也可自定义字段。 此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。 最后将第二部分负载使用 Base64 Url 编码,得到一个字符串就是 JWT 令牌的第二部分。 一个例子:

      {
        "aud": [
          "autodeliver"
        ],
        "user_name": "admin",
        "scope": [
          "all"
        ],
        "clientIp": "0:0:0:0:0:0:0:1",
        "exp": 1611207733,
        "jti": "43194709-e626-41c9-8d36-b3ec5d864361",
        "client_id": "client_lagou"
      }
      
    • Signature

      第三部分是签名,此部分用于防止 JWT 内容被篡改。 这个部分使用 Base64 Url 将前两部分进行编码,编码后使用点( . )连接组成字符串,最后使用 Header 中声明 签名算法进行签名。

    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      [secret]
    )
    
    • base64UrlEncode(header): jwt 令牌的第一部分。
    • base64UrlEncode(payload): jwt 令牌的第二部分。
    • secret:签名所使用的密钥。
  • 关于JWT令牌我们需要注意

    • JWT 令牌就是一种可以被验证的数据组织格式,它的玩法很灵活,我们这里是基于 Spring Cloud Oauth2 创建、校验 JWT 令牌
    • 我们也可以自己写工具类生成、校验 JWT 令牌
    • JWT 令牌中不要存放过于敏感的信息,因为我们知道拿到令牌后,我们可以解码看到载荷部分的信息
    • JWT 令牌每次请求都会携带,内容过多,会增加网络带宽占用
JWT 改造
  • 认证服务器端 JWT 改造

    /**
     * 当前类为Oauth2 server的配置类(需要继承特定的父类 AuthorizationServerConfigurerAdapter)
     */
    @Configuration
    @EnableAuthorizationServer  // 开启认证服务器功能
    public class OauthServerConfigurer extends AuthorizationServerConfigurerAdapter {
    
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private LagouAccessTokenConvertor lagouAccessTokenConvertor;
    
    
        private String sign_key = "lagou123"; // jwt签名密钥
    
    
        /**
         * 认证服务器最终是以api接口的方式对外提供服务(校验合法性并生成令牌、校验令牌等)
         * 那么,以api接口方式对外的话,就涉及到接口的访问权限,我们需要在这里进行必要的配置
         *
         * @param security
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            // 相当于打开endpoints 访问接口的开关,这样的话后期我们能够访问该接口
            security
                    // 允许客户端表单认证
                    .allowFormAuthenticationForClients()
                    // 开启端口/oauth/token_key的访问权限(允许)
                    .tokenKeyAccess("permitAll()")
                    // 开启端口/oauth/check_token的访问权限(允许)
                    .checkTokenAccess("permitAll()");
        }
    
        /**
         * 客户端详情配置,
         * 比如client_id,secret
         * 当前这个服务就如同QQ平台,拉勾网作为客户端需要qq平台进行登录授权认证等,提前需要到QQ平台注册,QQ平台会给拉勾网
         * 颁发client_id等必要参数,表明客户端是谁
         *
         * @param clients
         * @throws Exception
         */
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            super.configure(clients);
    
    
            // 从内存中加载客户端详情
            clients.inMemory()// 客户端信息存储在什么地方,可以在内存中,可以在数据库里
                   .withClient("client_lagou")  // 添加一个client配置,指定其client_id
                   .secret("abcxyz")                   // 指定客户端的密码/安全码
                   .resourceIds("autodeliver")         // 指定客户端所能访问资源id清单,此处的资源id是需要在具体的资源服务器上也配置一样
                   // 认证类型/令牌颁发模式,可以配置多个在这里,但是不一定都用,具体使用哪种方式颁发token,需要客户端调用的时候传递参数指定
                   .authorizedGrantTypes("password", "refresh_token")
                   // 客户端的权限范围,此处配置为all全部即可
                   .scopes("all");
    
            // 从数据库中加载客户端详情
            // clients.withClientDetails(createJdbcClientDetailsService());
    
        }
    
    /*    @Autowired
        private DataSource dataSource;
    
        @Bean
        public JdbcClientDetailsService createJdbcClientDetailsService() {
            JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
            return jdbcClientDetailsService;
        }*/
    
    
        /**
         * 认证服务器是玩转token的,那么这里配置token令牌管理相关(token此时就是一个字符串,当下的token需要在服务器端存储,
         * 那么存储在哪里呢?都是在这里配置)
         *
         * @param endpoints
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.tokenStore(tokenStore())  // 指定token的存储方法
                     .tokenServices(authorizationServerTokenServices())   // token服务的一个描述,可以认为是token生成细节的描述,比如有效时间多少等
                     .authenticationManager(authenticationManager) // 指定认证管理器,随后注入一个到当前类使用即可
                     .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
        }
    
    
        /*
            该方法用于创建tokenStore对象(令牌存储对象)
            token以什么形式存储
         */
        public TokenStore tokenStore() {
            // return new InMemoryTokenStore();
            // 使用jwt令牌
            return new JwtTokenStore(jwtAccessTokenConverter());
        }
    
        /**
         * 返回jwt令牌转换器(帮助我们生成jwt令牌的)
         * 在这里,我们可以把签名密钥传递进去给转换器对象
         *
         * @return
         */
        public JwtAccessTokenConverter jwtAccessTokenConverter() {
            JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
            jwtAccessTokenConverter.setSigningKey(sign_key);  // 签名密钥
            jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key));  // 验证时使用的密钥,和签名密钥保持一致
            jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);
    
            return jwtAccessTokenConverter;
        }
    
    
        /**
         * 该方法用户获取一个token服务对象(该对象描述了token有效期等信息)
         */
        public AuthorizationServerTokenServices authorizationServerTokenServices() {
            // 使用默认实现
            DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
            defaultTokenServices.setSupportRefreshToken(true); // 是否开启令牌刷新
            defaultTokenServices.setTokenStore(tokenStore());
    
            // 针对jwt令牌的添加
            defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
    
            // 设置令牌有效时间(一般设置为2个小时)
            defaultTokenServices.setAccessTokenValiditySeconds(20); // access_token就是我们请求资源需要携带的令牌
            // 设置刷新令牌的有效时间
            defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
    
            return defaultTokenServices;
        }
    }
    
    @Component
    public class LagouAccessTokenConvertor extends DefaultAccessTokenConverter {
    
    
        @Override
        public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
            // 获取到request对象
            HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getRequest();
            // 获取客户端ip(注意:如果是经过代理之后到达当前服务的话,那么这种方式获取的并不是真实的浏览器客户端ip)
            String remoteAddr = request.getRemoteAddr();
            Map<String, String> stringMap = (Map<String, String>) super.convertAccessToken(token, authentication);
            stringMap.put("clientIp", remoteAddr);
            return stringMap;
        }
    }
    
    
  • 资源服务器校验 JWT 令牌

    @Configuration
    @EnableResourceServer  // 开启资源服务器功能
    @EnableWebSecurity  // 开启web访问安全
    public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {
    
        private String sign_key = "lagou123"; // jwt签名密钥
    
        @Autowired
        private LagouAccessTokenConvertor lagouAccessTokenConvertor;
    
        /**
         * 该方法用于定义资源服务器向远程认证服务器发起请求,进行token校验等事宜
         * @param resources
         * @throws Exception
         */
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    
            // 设置当前资源服务的资源id
            resources.resourceId("autodeliver");
            // 定义token服务对象(token校验就应该靠token服务对象)
            RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
            // 校验端点/接口设置
            remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token");
            // 携带客户端id和客户端安全码
            remoteTokenServices.setClientId("client_lagou");
            remoteTokenServices.setClientSecret("abcxyz");
    
            // 别忘了这一步
            resources.tokenServices(remoteTokenServices);
    
    
            // jwt令牌改造
            resources.resourceId("autodeliver").tokenStore(tokenStore()).stateless(true);// 无状态设置
        }
    
    
        /**
         * 场景:一个服务中可能有很多资源(API接口)
         *    某一些API接口,需要先认证,才能访问
         *    某一些API接口,压根就不需要认证,本来就是对外开放的接口
         *    我们就需要对不同特点的接口区分对待(在当前configure方法中完成),设置是否需要经过认证
         *
         * @param http
         * @throws Exception
         */
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http    // 设置session的创建策略(根据需要创建即可)
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                    .and()
                    .authorizeRequests()
                    .antMatchers("/autodeliver/**").authenticated() // autodeliver为前缀的请求需要认证
                    .antMatchers("/demo/**").authenticated()  // demo为前缀的请求需要认证
                    .anyRequest().permitAll();  //  其他请求不认证
        }
    
    
    
    
        /*
           该方法用于创建tokenStore对象(令牌存储对象)
           token以什么形式存储
        */
        public TokenStore tokenStore(){
            // return new InMemoryTokenStore();
    
            // 使用jwt令牌
            return new JwtTokenStore(jwtAccessTokenConverter());
        }
    
        /**
         * 返回jwt令牌转换器(帮助我们生成jwt令牌的)
         * 在这里,我们可以把签名密钥传递进去给转换器对象
         * @return
         */
        public JwtAccessTokenConverter jwtAccessTokenConverter() {
            JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
            jwtAccessTokenConverter.setSigningKey(sign_key);  // 签名密钥
            jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key));  // 验证时使用的密钥,和签名密钥保持一致
            jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);
            return jwtAccessTokenConverter;
        }
    
    }
    
    @Component
    public class LagouAccessTokenConvertor extends DefaultAccessTokenConverter {
    
    
        @Override
        public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
    
            OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
            oAuth2Authentication.setDetails(map);  // 将map放入认证对象中,认证对象在controller中可以拿到
            return oAuth2Authentication;
        }
    }
    
  • 测试访问

    • http://localhost:9999/oauth/token?client_secret=abcxyz&grant_type=password&username=admin&password=123456&client_id=client_lagou

      获得 JWT 格式的 token

    • http://localhost:8096/autodeliver/checkState/1545132?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiYXV0b2RlbGl2ZXIiXSwidXNlcl9uYW1lIjoiYWRtaW4iLCJzY29wZSI6WyJhbGwiXSwiY2xpZW50SXAiOiIwOjA6MDowOjA6MDowOjEiLCJleHAiOjE2MTEyMDg0NjIsImp0aSI6ImQ0ODZiZmNkLTg2MjEtNDUyYS04ODcwLWZhNGEyZTdmYjYzMyIsImNsaWVudF9pZCI6ImNsaWVudF9sYWdvdSJ9.fyu6RwAteMLkbn678f6A8ZTxtopeS3nWiwjH2yMAvII

      带 token 访问受保护资源

从数据库获取用户数据验证合法性

基于 Oauth2 的 JWT 令牌信息扩展

如何在 OAuth2 环境下向 JWT 令牌中存如扩展信息?

继承 DefaultAccessTokenConverter 类,重写 convertAccessToken 方法存入扩展信息

  • 服务端存入信息
@Component
public class LagouAccessTokenConvertor extends DefaultAccessTokenConverter {


    @Override
    public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
        // 获取到request对象
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getRequest();
        // 获取客户端ip(注意:如果是经过代理之后到达当前服务的话,那么这种方式获取的并不是真实的浏览器客户端ip)
        String remoteAddr = request.getRemoteAddr();
        Map<String, String> stringMap = (Map<String, String>) super.convertAccessToken(token, authentication);
        stringMap.put("clientIp",remoteAddr);
        return stringMap;
    }
}

  • 客户端取出信息
@Component
public class LagouAccessTokenConvertor extends DefaultAccessTokenConverter {


    @Override
    public OAuth2Authentication extractAuthentication(Map<String, ?> map) {

        OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
        oAuth2Authentication.setDetails(map);  // 将map放入认证对象中,认证对象在controller中可以拿到
        return oAuth2Authentication;
    }
}

参考资料

posted @ 2021-01-10 17:04  流星<。)#)))≦  阅读(176)  评论(0编辑  收藏  举报