拉勾教育学习-笔记分享の"偷袭"SpringCloud(中卷)

589 阅读29分钟

【文章内容输出来源:拉勾教育Java高薪训练营】
--- 所有脑图均本人制作,未经允许请勿滥用 ---
需要深刻把握各组件的职责和使用方式,多写多练


夫学须静也,才须学也,非学无以广才,非志无以成学

一、初代 Spring Cloud 核心组件(II)

Part 5 - GateWay 网关组件

「GateWay 简述」

Spring Cloud GateWay 是 Spring Cloud 的⼀个全新项目,目标是取代 Netflix Zuul,它基于 Spring5.0+SpringBoot2.0+WebFlux(基于高性能的 Reactor模式 响应式通信框架 Netty,异步非阻塞模型)等技术开发,性能高于Zuul,官方测试,GateWay 是 Zuul 的1.6倍,旨在为微服务架构提供⼀种简单有效的统⼀的API路由管理方式

Spring Cloud GateWay不仅提供统⼀的路由方式(反向代理)并且基于 Filter(定义过滤器对请求过滤,完成⼀些功能) 链的方式提供了网关基本的功能,例如:鉴权、流量控制、熔断、路径重写、日志监控等。

网关在微服务架构中的位置

【核心概念】

  1. 路由(route)
    由 一个ID + 一个目标URL(最终路由地址) + 一系列断言(匹配条件) + 过滤器(精细化控制) 组成;
    断言为 true 时,路由匹配 √
  2. 断言(predicates)
    匹配 Http 请求中的所有内容(包括请求头、请求参数等)
  3. 过滤器(filter)
    在请求 前/后 执行业务逻辑

【核心逻辑】 规则匹配 + 路由转发 + 执行过滤器链

【常用业务】

  1. 限流
  2. 日志
  3. 黑名单

「GateWay 应用」

!GateWay 不要使用 web 模块,它引入的是WebFlux(类似于SpringMVC)
所以我们直接建立一个新的模块,而不是在 父工程下建立
新建 Module cloud-hystrix-turbine-9003

  1. 导入依赖 (除了webflux,其他依赖都和父工程一样)
<!--spring boot 父启动器依赖-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
    </parent>

    <dependencies>
        <!-- spring cloud commons 依赖引入 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-commons</artifactId>
        </dependency>
        <!-- Eureka Client 客户端依赖引入-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--GateWay 网关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--引入webflux-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <!--日志依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <!--测试依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Actuator可以帮助你监控和管理Spring Boot应用-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

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

    <build>
        <plugins>
            <!--编译插件-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                    <encoding>utf-8</encoding>
                </configuration>
            </plugin>
            <!--打包插件-->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
  1. yml 配置
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   # 目标服务地址
          # 但是 -> 动态路由:uri配置的应该是一个服务名称,而不应该是一个具体的服务实例的地址
          uri: lb://lagou-service-resume
          predicates:  # 断言:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默 认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
            - Path=/resume/**
          filters:
            - StripPrefix=1

【说明】 其中使用的 uri 是动态路由。lb 代表 => 从注册中心获取服务;后面加上 目标服务名称

上面这段配置的意思是,配置了⼀个 id 为 service-autodeliver-router 的路由规则,当向网关发起请求 http://localhost:9003/autodeliver/checkState/1545132 请求会被分发路由到对应的微服务上

「GateWay 路由」

  • 路由匹配规则

    1. DateTime 时间断言
    2. Cookie 断言
    3. Header 请求头断言
    4. Host 请求主机断言
    5. Method 请求方法断言
    6. Path 请求路径断言
    7. QueryParam 请求参数断言
    8. RemoteAddr 远程地址断言
  • 简单使用

  1. 时间点后匹配
spring:
  cloud:
 	gateway:
 		routes:
 		- id: after_route
 		uri: https://example.org
 		predicates:
 			- After=2021-01-20T17:42:47.789-07:00[America/Denver]
  1. 时间点前匹配
spring:
  cloud:
 	gateway:
 		routes:
 		- id: before_route
 		uri: https://example.org
 		predicates:
 			- Before=2021-01-20T17:42:47.789-07:00[America/Denver]
  1. 时间区间匹配
spring:
  cloud:
 	gateway:
 		routes:
 		- id: between_route
 		uri: https://example.org
 		predicates:
 			- Between=2021-01-20T17:42:47.789-07:00[America/Denver],2021-01-21T17:42:47.789-07:00[America/Denver]
  1. 指定正则Cookie匹配
spring:
  cloud:
 	gateway:
 		routes:
 		- id: cookie_route
 		uri: https://example.org
 		predicates:
 			- Cookie=chocolate, ch.p
  1. 指定Header匹配
spring:
  cloud:
 	gateway:
 		routes:
 		- id: header_route
 		uri: https://example.org
 		predicates:
 			- Header=X-Request-Id, \d+
  1. 请求Host匹配
spring:
  cloud:
 	gateway:
 		routes:
 		- id: host_route
 		uri: https://example.org
 		predicates:
 			-  Host=**.somehost.org,**.anotherhost.org
  1. 请求Method匹配
spring:
  cloud:
 	gateway:
 		routes:
 		- id: method_route
 		uri: https://example.org
 		predicates:
 			-  Method=GET,POST
  1. 请求路径正则匹配
spring:
  cloud:
 	gateway:
 		routes:
 		- id: path_route
 		uri: https://example.org
 		predicates:
 			-  Path=/red/{segment},/blue/{segment}
  1. 请求包含参数匹配
spring:
  cloud:
 	gateway:
 		routes:
 		- id: query_route
 		uri: https://example.org
 		predicates:
 			-  Query=green
  1. 请求包含参数并且参数值匹配正则
spring:
  cloud:
 	gateway:
 		routes:
 		- id: query_route
 		uri: https://example.org
 		predicates:
 			- Query=red, gree.
  1. 远程地址匹配
spring:
  cloud:
 	gateway:
 		routes:
 		- id: remoteaddr__route
 		uri: https://example.org
 		predicates:
 			-  RemoteAddr=192.168.1.1/24

「GateWay 过滤器」

【影响时机分类】 两个 prepost

  1. pre过滤器:在请求被路由之前调用
    • 身份验证
    • 集群选择微服务
    • 记录调试信息
  2. post过滤器:微服务执行完调用
    • 为响应 添加标注你的 HTTP Header
    • 收集统计信息和指标
    • 将响应从微服务分发到客户端

【类型分类】

  1. GateWayFilter:影响单个路由
  2. GlobalFilter:影响所有路由

以下语句的功能就属于GateWay Filter,可以去掉url中的占位后转发路由

predicates:  
  - Path=/resume/**
filters:
  - StripPrefix=1

「GateWay 过滤器实例(黑名单)」

我们常用的都是 GlobalFilter,下面就以黑名单功能为例实现功能

@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) {
        // 1. 获取客户端ip,判断是否在黑名单中
        // yes --> 拒绝访问
        //  no --> 放行
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
    
        String clientIP = request.getRemoteAddress().getHostString(); // 客户端ip拿到
        if (blackList.contains(clientIP)) {
            // 拒绝访问
            response.setStatusCode(HttpStatus.UNAUTHORIZED); // 状态码拿到
            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;
    }
}

「GateWay 高可用」

网关如果挂掉了,后续的所有功能都会失效;所以!网关必须 高可用

可以启动多个GateWay实例来实现高可用,在GateWay的上游使用 Nginx等负载均衡设备 进行负载转发以达到高可用的目的

nginx.conf

#配置多个GateWay实例
upstream gateway {
 server 127.0.0.1:9002;
 server 127.0.0.1:9003;
}
location / {
 proxy_pass http://gateway;
}

Part 6 - Spring Cloud Config 分布式配置中心

「应用场景」

  • ⼀个微服务架构中可能有成百上千个微服务,所以集中配置管理是很重要的
  • 不同环境不同配置,比如数据源配置在不同环境(开发dev, 测试test, 生产prod)
  • 运行期间可动态调整。例如,可根据各个微服务的负载情况,动态调整数据源连接池大小等配置修改后可自动更新
  • 如配置内容发生变化,微服务可以自动更新配置

「Spring Cloud Config 综述」

  1. Server端
    提供配置文件的存储、以接口的形式将配置文件的内容提供出去,使用 @EnableConfigServer 注解嵌入SpringBoot
  2. Client端
    通过接口获取配置数据并初始化自己的应用

「实现」

现在,我们对 “简历微服务” 的application.yml进行配置管理(开发环境、测试环境、生成环境)

  1. 在自己的git或者码云中创建项目
  2. 上传yml配置文件,命名规则如下:
    • {application}-{profile}.yml
    • {application}-{profile}.properties
    • 说明 --> application:应用名称 / profile:环境
  3. pom 依赖引入
  <dependency>
      <artifactId>service-common</artifactId>
      <groupId>com.archie</groupId>
      <version>1.0-SNAPSHOT</version>
  </dependency>
  <!-- Eureka Client 客户端依赖引入-->
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  </dependency>
  <!-- config配置中心服务端 -->
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-config-server</artifactId>
  </dependency>
  1. 启动类用注解声明
@SpringBootApplication
@EnableDiscoveryClient
@EnableConfigServer // 开启配置管理
public class ConfigApplication9006 {
    
    public static void main(String[] args) {
        SpringApplication.run(ConfigApplication9006.class, args);
    }
    
}
  1. yml 配置管理
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/5173098004/lagou-config-repo.git #配置git服务地址
          username: 517309804@qq.com #配置git用户名
          password: '******' #配置git密码
          search-paths:
            - lagou-config-repo
      name: config-server #配置文件名称
      uri: http://localhost:9006 #ConfigServer配置中心地址
      label: master #分支名称
      profile: dev #后缀名称
  1. 访问文件测试

http://127.0.0.1:9006/master/lagou-service-resume-dev.yml

  1. 在简历微服务中添加依赖坐标
  <!-- Config 客户端 -->
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-config-client</artifactId>
  </dependency>
  1. application.yml 修改为 bootstrap.yml 配置文件并添加 config坐标

bootstrap.yml是系统级别的,优先级比 application.yml 高,应用启动时会检查这个配置文件,在这个配置文件中指定配置中心的服务地址,会自动拉取所有应用配置并且启用

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

「Config 配置手动刷新」

  1. Client客户端添加依赖 springboot-starter-actuator
  <!-- Actuator可以帮助你监控和管理Spring Boot应用-->
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>
  1. Client客户端 bootstrap.yml 中添加配置(暴露通信端点)
# 暴露所有的端⼝
management:
  endpoints:
    web:
      exposure:
        include: "*" # 也可以指定暴露断点,e.g. include: refresh
  1. Client客户端使用到配置信息的类上添加 @RefreshScope
@RestController
@RequestMapping("/config")
@RefreshScope
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;
    }
}
  1. 手动向 Client客户端 发起 POST请求 http://localhost:8080/actuator/refresh

「Config 配置自动更新」

RabbitMQ 的安装请 Learning online

⼀次通知处处生效
结合消息总线Bus)实现分布式配置的自动更新(Spring Cloud Config + Spring Cloud Bus)

所谓消息总线Bus,即我们经常会使用 MQ消息代理 构建⼀个共用的Topic,通过这个 Topic连接各个微服务实例,MQ 广播的消息会被所有在注册中心的微服务实例监听和消费。
换言之就是通过⼀个主题连接各个微服务,打通脉络。

Spring Cloud Bus(基于MQ的,支持 RabbitMq/Kafka) 是Spring Cloud中的消息总线方案

  1. Config Server服务端 添加消息总线支持
  <!-- 消息总线 bus ⽀持 -->
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-bus-amqp</artifactId>
  </dependency>
  1. ConfigServer 添加配置
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
  1. 微服务暴露端口
# 暴露所有的端⼝
management:
  endpoints:
    web:
      exposure:
        include: "*" # 也可以指定暴露断点,e.g. include: refresh
  1. 重启各个服务,更改配置之后,向配置中心服务端发送 post请求 http://localhost:9003/actuator/bus-refresh 各个客户端配置即可自动刷新

若想定向更新而不是全部更新:
在发起刷新请求的时候 http://localhost:9006/actuator/bus-refresh/lagou-service-resume:8081 即为最后面跟上要定向刷新的实例的 服务名:端口号即可

Part 7 - Spring Cloud Stream 消息驱动组件

Spring Cloud Stream 消息驱动组件帮助我们更快速,更方便,更友好的去构建消息驱动微服务的。

  • MQ:消息队列/消息中间件/消息代理,产品有很多,ActiveMQ RabbitMQ RocketMQ Kafka

「Stream 解决的痛点问题」

  • 不同的MQ消息中间件内部机制包括使用方式都会有所不同,比如RabbitMQ中有 Exchange(交换机/交换器)这⼀概念,kafka 有 Topic、Partition分区 这些概念,
  • MQ消息中间件的差异性不利于我们上层的开发应用,当我们的系统希望从原有的 RabbitMQ 切换到 Kafka 时,我们会发现比较困难,很多要操作可能重来(因为应用程序和具体的某⼀款MQ消息中间件耦合在⼀起了)
  • Spring Cloud Stream 进行了很好的上层抽象,可以让我们与具体消息中间件解耦合,屏蔽掉了底层具体MQ消息中间件的细节差异,就像Hibernate 屏蔽掉了具体数据库(Mysql/Oracle⼀样)
    如此⼀来,我们学习、开发、维护MQ都会变得轻松。目前 Spring Cloud Stream 支持 RabbitMQ 和 Kafka

【本质】
屏蔽掉了底层不同 MQ消息中间件之间的差异,统⼀了MQ的编程模型,降低了学习、开发、维护MQ的成本

「Stream 重要概念」

【SC Cloud 定义】
是⼀个构建消息驱动微服务的框架
通过 inputs / outputs 来与 SC Cloud 中的 binder对象 交互(binder对象用于屏蔽底层MQ细节)

  • 每个input关联一个 消费者
  • 每个output关联一个 生产者
  • Stream中提供了不同的Binder,当我们需要切换MQ的时候,我们只切换Binder即可
  • "Middleware" ≈ RabbitMQ / Kafka

「传统 MQ模型 与 Stream消息驱动模型」

「Stream消息通信方式 及 编程模型」

Stream中的消息通信方式遵循了 发布—订阅模式

当⼀条消息被投递到消息中间件之后,它会通过共享的 Topic 主题进行广播,消息消费者在订阅的主题中收到它并触发自身的业务逻辑处理。

【"Topic"】:
是Spring Cloud Stream中的⼀个抽象概念,用来代表发布共享消息给消费者的地方

  • RabbitMQ中的"Topic" ==> Exchange
  • Kafka中的"Topic" ==> Topic

【Stream中的相关注解】

  1. @Input 标识输入通道,接受到的消息 ==输入通道==> 应用程序
  2. @Output 标识输出通道,应用程序 ==输出通道==> 发布的消息
  3. @StreamListner 监听队列
  4. @EnableBinding 绑定 Channel 和 Exchange

以上注解无非在做⼀件事:
把我们结构图中那些组成部分上下关联起来,打通通道(这样的话⽣产者的 message数据才能进⼊mq,mq中数据才能进⼊消费者工程)

「Stream 使用事例」

建立三个module

  • cloud-stream-producer-9090, 作为生产者端发消息
  • cloud-stream-consumer-9091,作为消费者端接收消息
  • cloud-stream-consumer-9092,作为消费者端接收消息
生产者端
  1. pom 引入依赖
    <!--eureka client 客户端依赖引入-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    <!--spring cloud stream 依赖(rabbit)-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </dependency>
  1. application.yml 配置
server:
  port: 9090
spring:
  application:
    name: cloud-stream-producer
  cloud:
    stream:
      binders: # 绑定MQ服务信息(此处我们是RabbitMQ)
        lagouRabbitBinder: # 给Binder定义的名称,用于后面的关联
          type: rabbit # MQ类型,如果是Kafka的话,此处配置kafka
          environment: # MQ环境配置(用户名、密码等)
            spring:
              rabbitmq:
                host: localhost
                port: 5672
                username: guest
                password: guest
      bindings: # 关联整合通道和binder对象
        output: # output是我们定义的通道名称,此处不能乱改
          destination: lagouExchange # 要使用的Exchange名称(消息队列主题名称)
          content-type: text/plain # application/json # 消息类型设置,比如json
          binder: lagouRabbitBinder # 关联MQ服务
# 注册到 Eureka 服务中心
eureka:
  client:
    service-url:
      # 注册到集群,用 [,] 分隔
      defaultZone: http://myEurekaA:8761/eureka/,http://myEurekaB:8762/eureka/
  instance:
    # 服务实例中显示ip,而非主机名(为了兼容老版本)
    prefer-ip-address: true
    # 可以对实例名称自定义
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
  1. 启动入口
@SpringBootApplication
@EnableDiscoveryClient
public class StreamProducerApplication9090 {

    public static void main(String[] args) {
        SpringApplication.run(StreamProducerApplication9090.class,args);
    }
}
  1. 业务类开发
public interface IMessageProducer {
    public void sendMessage(String content);
}
// Source.class里面就是对输出通道的定义(这是Spring Cloud Stream内置的通道封装)
@EnableBinding(Source.class)
public class MessageProducerImpl implements IMessageProducer {

    // 将MessageChannel的封装对象Source注入到这里使用
    @Autowired
    private Source source;

    @Override
    public void sendMessage(String content) {
        // 向mq中发送消息(并不是直接操作mq,应该操作的是spring cloud stream)
        // 使用通道向外发出消息(指的是Source里面的output通道)
        source.output().send(MessageBuilder.withPayload(content).build());
    }
}
消费者端
  1. 同生产者 pom

(略)

  1. application.yml 配置

唯二不同处:

  • server.port 改动
  • spring.cloud.stream.bindings.input 改 spring.cloud.stream.bindings.output
  1. 启动入口

(略)

  1. 消息消费者监听
@EnableBinding(Sink.class)
public class MessageConsumerService {

    @StreamListener(Sink.INPUT)
    public void recevieMessages(Message<String> message) {
        System.out.println("=========接收到的消息:" + message);
    }

}

「Stream 高级之 自定义消息通道」

Stream 内置了两种接口 SourceSink 。分别定义了 binding 为 “input” 的输入流和“output” 的输出流,我们也可以自定义各种输入输出流(通道)

但实际我们会在我们的服务中使用多个binder、多个输入通道和输出通道,现在仅默认⼀个input的输入通道和⼀个output的输出通道,怎么办?

自消息通道! 学着Source和Sink的样子,给你的通道定义个自己的名字,多个输入通道和输出通道是可以写在⼀个类中的。

  1. 定义接口
interface CustomChannel {
	String INPUT_LOG = "inputLog";
    String OUTPUT_LOG = "outputLog";
    
    @Input(INPUT_LOG)
    SubscribableChannel inputLog();
    
    @Output(OUTPUT_LOG)
    MessageChannel outputLog();
}
  1. 使用
  • 在 @EnableBinding 注解中,绑定自定义的接口
  • 使用 @StreamListener 做监听的时候,需要指定 CustomChannel.INPUT_LOG
bindings:
 	inputLog:
 		destination: AExchange
 	outputLog:
 		destination: BExchange

二、Spring Cloud 高级实战

Part 1 - 微服务控制之 Turbine 聚合监控

参考 上篇 的 Hytrix 章节

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

微服务架构中,一个功能往往涉及到多个乃至十几个微服务的调用,于是产生了以下问题:

  1. 如何动态展示服务的链路?
  2. 如何分析服务链路的瓶颈并进行调优?
  3. 如何快速迅速发现服务链路中的问题?

「分布式链路追踪」

一个请求的各个链路节点中,都记录日志,并将这些日志集中可视化

「市场上的分布式链路最终方案」

  • Spring Cloud Sleuth + Twitter Zipkin
  • 阿里 の “鹰眼”
  • 大众点评 の “CAT”
  • 美团 の “Mtrace”
  • 京东 の “Hydra”
  • 新浪 の “Watchman”
  • Apache Skywalking

「核心思想」

【本质】:记录日志

  • 所有的分布式链路系统都基于一个理念,这个理论来自Google的一篇论文——《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》,其中的核心概念:
    • Trace : 以网关为边界,从request进入网关,直到response离开网关,这样一个回合,称之为 Trace;
    • Trace ID :对每一个Trace进行追踪的标识。
    • Span :封装日志数据的一个“跨度”,存在 开始 & 结束 节点;使用时间戳计算出 时间延迟,还有一些元数据等;
    • Trace ID :对每一个Span进行追踪的标识。
  1. 一个 Trace 由多个 Span 组成;
  2. Span 往往单位就是每一个微服务;
  3. 每个 Span 中还存在 一个 ParentId,指向上一个 Span,表明父子关系。

【Sleuth】
它能够记录一个服务请求经过了 哪些服务、时长多少等信息;然后我们根据这些信息,辨析各个微服务之间的调用关系并根据需求分析问题。
1. 耗时分析:使用采样请求,分析服务性能问题;
2. 链路优化:对于一些反复调用的服务进行性能优化。

Sleuth 将信息发送给 Zipkin 进行聚合,然后 Zipkin 存储并展示数据;

「Sleuth + Zipkin 应用」

  1. 每个要被追踪的微服务 引入 依赖坐标
  <!--链路追踪-->
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-sleuth</artifactId>
  </dependency>
  1. 每个要被追踪的微服务 修改 application.yml ,添加日志级别
logging:
  level:
    org.springframework.web.servlet.DispatcherServlet: debug
    org.springframework.cloud.sleuth: debug

  1. Zipkin Server 模块构建
  • 依赖坐标
  <!--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>
  • 入口创建
@SpringBootApplication
@EnableZipkinServer // 开启 Zipkin 服务器功能
public class ZipkinApplication9411 {
    
    public static void main(String[] args) {
        SpringApplication.run(ZipkinApplication9411.class, args);
    }
    
}
  • yml配置
server:
  port: 9411
spring:
  application:
    name: cloud-zipkin-server
management:
  metrics:
    web:
      server:
        auto-time-requests: false # 关闭自动检测
  • 访问 zipkin server 界面
  1. Zipkin Client 实现
  • 每个要被追踪的微服务 引入 client 依赖坐标
    <!--链路追踪 Zipkin-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zipkin</artifactId>
    </dependency>
  • 每个要被追踪的微服务的 yml 加入配置
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

「Zipkin server 的持久化」

一旦zipkin的服务下线了,之前的链路都将消失,这样会很不方便,所以可以使用持久化。(可以持久化到mysql等)

  1. 创建数据库,命名zipkin

  1. 创建数据库

表参考github

执行语句后:

  1. 引入 pom 依赖
  <!--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>
  1. yml 配置增加
spring:
  # 数据库连接信息
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/zipkin?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      initial-size: 10
      min-idle: 10
      max-active: 30
      max-wait: 50000
# 指定zipkin持久化介质为mysql
zipkin:
  storage:
    type: mysql
  1. 启动类 注入 配置管理器bean
@SpringBootApplication
@EnableZipkinServer // 开启 Zipkin 服务器功能
public class ZipkinApplication9411 {
    
    public static void main(String[] args) {
        SpringApplication.run(ZipkinApplication9411.class, args);
    }
    
    /**
     * 注入事务控制器
     * @param dataSource
     * @return
     */
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
    
}
  1. 重启并观察 zipkin 的三张表

Part 3 - 微服务统⼀认证方案 Spring Cloud OAuth2 + JWT

认证:只有当用户身份被验证合法时,才允许后续逻辑操作.

「微服务架构下的认证」

两种思路:

  1. 基于Session的认证
    • 分布式中,每个应用服务都需要在自己的session中存储用户信息,通过负载均衡将本地的请求分配到另一个应用时,需要将session信息带过去,避免重复登录;
    • 也可以使用session共享或者session粘贴;
    • 缺点:移动端无法有效使用.
  2. 基于token的认证
    • 服务端不用存储认证数据,而是由客户端将token令牌存储在任意地方,并且可以实现web和app的统一认证机制;
    • 缺点一:token属于 自包含信息,数据量比较大,而且每次存储都需要携带,占用带宽较高;
    • 缺点二:token的签名验证操作也给CPU带来了较大负担.

「OAuth2 开放授权协议/标准」

OAuth2 是一个开放协议,允许用户授权第三方访问其他服务提供者去获取部分基本内容;
e.g. 我们通过QQ登录未注册过的拉勾

  • QQ -> 其他服务者 / 拉勾 -> 第三方;
  • QQ平台颁发一些参数给拉勾,供后续上线登录授权用;
    1. 拉勾(相当于客户端)会拿到一个 client_id 当做临时账号
    2. 同时拉勾还会拿到一个 secret 当做临时密码

【OAuth2使用场景】

  1. 第三方授权;
  2. 单点登录;

「OAuth2 颁发token的授权方式」

  1. 授权码(authorization-code)☆☆
  2. 密码式(password)☆☆☆
  3. 隐藏式(implicit)
  4. 客户端凭证(client credentials)

授权码模式使用到了回调地址,比较麻烦;
接下来我们着重 说明接口对接中最常使用的 password 密码模式(用户名+密码 换取 token)

「Spring Cloud OAuth2 简介」

Spring Cloud OAuth2 是 Spring Cloud 体系对OAuth2协议的实现

通过向 OAuth2服务(统⼀认证授权服务)发送某个类型的 grant_type 进行集中认证和授权,从而获得 access_token(访问令牌),这个令牌是受其他微服务信任的

【OAuth2本质】: 引入了⼀个认证授权层,认证授权层连接了资源的拥有者,在授权层里面,资源的拥有者 可以给第三方应用授权去访问我们的某些受保护资源

「Spring Cloud OAuth2 应用实现」

【Demo 实现思路】

搭建认证Module : cloud-oauth-server-9999

  1. 引入 pom 依赖
  <dependency>
      <artifactId>service-common</artifactId>
      <groupId>com.archie</groupId>
      <version>1.0-SNAPSHOT</version>
  </dependency>
  <!-- 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>
  1. yml 配置
server:
  port: 9999
spring:
  application:
    name: cloud-oauth-server
# 注册到 Eureka 服务中心
# ... (同其他模块)...
  1. 入口
@SpringBootApplication
@EnableDiscoveryClient
public class OauthApplication {
    public static void main(String[] args) {
        SpringApplication.run(OauthApplication.class, args);
    }
}
  1. 认证服务器配置类

继承 AuthorizationServerConfigurerAdapter 并重写 三个config()

  • configure(ClientDetailsServiceConfigurer clients)
    配置客户端详情服务,可以将客户端信息存储在数据库中
  • configure(AuthorizationServerEndpointsConfigurer endpoints)
    配置令牌的访问端点和服务
  • configure(AuthorizationServerSecurityConfigurer oauthServer)
    配置令牌访问端点的安全约束
@Configuration
@EnableAuthorizationServer // 开启认证服务器功能
public class OauthServerConfigurer extends AuthorizationServerConfigurerAdapter {
    
    @Autowired(required = false)
    private AuthenticationManager authenticationManager;
    
    /**
     * ==> 接口的访问权限
     * 认证服务器最终是以api接口的方式对外提供服务(校验合法性并生成令牌、校验令牌等)
     * 那么,以api接口方式对外的话,就涉及到 接口的访问权限,我们需要在这里进行必要的配置
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        super.configure(security);
        // 相当于打开endpoints 访问接口的开关,这样的话后期我们能够访问该接口
        security
                // 允许客户端表单认证
                .allowFormAuthenticationForClients()
                // 开启端口/oauth/token_key的访问权限(允许)
                .tokenKeyAccess("permitAll()")
                // 开启端口/oauth/check_token的访问权限(允许)
                .checkTokenAccess("permitAll()");
    }
    
    /**
     *  ==> 客户端详情配置,
     *  当前这个服务就如同 QQ平台,拉勾网作为客户端需要qq平台进行登录授权认证等,需要提前在QQ平台注册,
     *  拿到QQ平台颁发的 client_id,secret等必要参数,表明客户端是谁
     * @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需要在服务器端存储,那么存储在哪里呢?都是在这里配置)
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        super.configure(endpoints);
        endpoints
                .tokenStore(tokenStore())  // 指定token的存储方法
                .tokenServices(authorizationServerTokenServices())  // token服务的一个描述,可以认为是token生成细节的描述,比如有效时间多少等
                .authenticationManager(authenticationManager) // 指定认证管理器,随后注入一个到当前类使用即可
                .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
    }
    
    /**
     * 该⽅法用于创建tokenStore对象(令牌存储对象)
     * @return
     */
    public TokenStore tokenStore(){
        return new InMemoryTokenStore();
    }
    
    /**
     * 该方法用户获取一个token服务对象(该对象描述了token有效期等信息)
     * @return
     */
    public AuthorizationServerTokenServices authorizationServerTokenServices() {
        // 使用默认实现
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setSupportRefreshToken(true); // 是否开启令牌刷新
        defaultTokenServices.setTokenStore(tokenStore());
        
        // 设置令牌有效时间(一般设置为2个小时)
        defaultTokenServices.setAccessTokenValiditySeconds(7200); // access_token 就是我们请求资源需要携带的令牌
        // 设置刷新令牌的有效时间
        defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
        
        return defaultTokenServices;
    }
}
  1. 认证服务器安全配置类
@Configuration
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    /**
     * 注册一个认证管理器对象到容器
     * @return
     * @throws Exception
     */
    @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);
    }
}

  1. 测试

访问 http://localhost:9999/oauth/token 效果:

实际路径 http://localhost:9999/oauth/token?client_secret=abcxyz&grant_type=password&username=admin&password=123456&client_id=client_lagou

路径分解

  1. endpoint: /oauth/token
  2. 获取token用参数:
    • client_id:客户端id
    • client_secret:客户单密码
    • grant_type:指定使用哪种颁发类型
    • username:用户名
    • password:密码

校验token: http://localhost:9999/oauth/check_token?token=a9979518-838c-49ff-b14a-ebdb7fde7d08

  1. 将之前的 任意微服务 改造成 资源微服务

引入aouth2依赖

  <!--导入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 ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
    
    /**
     * 用于定义资源服务器向远程认证服务器发起请求,进行token校验等事宜
     * @param resources
     * @throws Exception
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        // 设置资源服务id
        resources.resourceId("autodeliver");
        // 定义token服务对象
        RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
        // 校验端点/接口设置
        remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/token");
        // 携带客户端id、客户端安全码
        remoteTokenServices.setClientSecret("abcxyz");
        remoteTokenServices.setClientId("client_lagou");
        // 确认添加资源服务
        resources.tokenServices(remoteTokenServices);
    }
    
    /**
     * 区分对待 需要认证 & 不需要认证 的API接口
     * @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(); // 其他请求不认证
    }
}

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

为了分摊认证服务器的压力,于是有人提出:
令牌可以是一个 (包含了用户信息、过期信息的) 非简单字符串,每个微服务根据这些信息直接在自己 本机上进行认证

JWT定义

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

JWT令牌结构

三个部分,用 . 号分隔 ,如:xxxxxxx.yyyyyy.zzzzzzz

  1. Header 包含令牌的类型、使用的哈希算法
    {"arg":"HS256", "typ":"JWT"}

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

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

  2. Payload 负载信息,存放JWT提供的现成字段:iss(签发者)/exp(过期时间戳)/sub(面向的用户)等
    这部分不建议存放敏感信息,因为可以被解码还原;

    {
      "sub": "1234567890",
      "name": "Archie Wan",
      "iat": 1516239022
    }
    

    同样使用Base64编码后,得到JWT令牌的第二部分

  3. Signature 签名,用于防止JWT内容被篡改
    这个部分使用Base64url将前两部分进行编码,然后通过 . 连接成字符串,最后由 Header 中声明的 签名算法生成

认证服务器JWT令牌改造

  1. 令牌存储的改造
    /**
     * 该⽅法用于创建tokenStore对象(令牌存储对象)
     * @return
     */
    public TokenStore tokenStore(){
        // return new InMemoryTokenStore();
        // ↓ 改造为jwt令牌
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    
    private String sign_key = "lagou123"; // jwt签名密钥
    
    /**
     * 返回jwt令牌转换器(帮助我们生成jwt令牌的)
     * 在这里,我们可以把签名密钥传递进去给转换器对象
     * @return
     */
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(sign_key);  // 签名密钥
        jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key));  // 验证时使用的密钥,和签名密钥保持一致(MacSigner用于对称加密)
        return jwtAccessTokenConverter;
    }
  1. token中添加令牌服务

defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());

    /**
     * 该方法用户获取一个token服务对象(该对象描述了token有效期等信息)
     * @return
     */
    public AuthorizationServerTokenServices authorizationServerTokenServices() {
        // 使用默认实现
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setSupportRefreshToken(true); // 是否开启令牌刷新
        defaultTokenServices.setTokenStore(tokenStore());
        
        // ↓ 添加令牌服务(增强)
        defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
        
        // 设置令牌有效时间(一般设置为2个小时)
        defaultTokenServices.setAccessTokenValiditySeconds(7200); // access_token 就是我们请求资源需要携带的令牌
        // 设置刷新令牌的有效时间
        defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
        return defaultTokenServices;
    }
  1. 查看token

我们去 JWT官网 看看 编码/解码的情况

  1. 改造资源服务器的请求代码
  1. 测试对资源服务器的访问

http://localhost:XXXX/autodeliver/checkState/1545346?access_token=xxxx.yyyyy.zzzz

「从数据库加载Oauth2客户端信息」

我们使用 JdbcClientDetailsService 来操作数据库管理token,但是从这个类的源码可知,所有语句都是对 oauth_client_details 这张表的操作,所以表明不是由着我们随意的。

建表语句:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
 `client_id` varchar(48) NOT NULL,
 `resource_ids` varchar(256) DEFAULT NULL,
 `client_secret` varchar(256) DEFAULT NULL,
 `scope` varchar(256) DEFAULT NULL,
 `authorized_grant_types` varchar(256) DEFAULT NULL,
 `web_server_redirect_uri` varchar(256) DEFAULT NULL,
 `authorities` varchar(256) DEFAULT NULL,
 `access_token_validity` int(11) DEFAULT NULL,
 `refresh_token_validity` int(11) DEFAULT NULL,
 `additional_information` varchar(4096) DEFAULT NULL,
 `autoapprove` varchar(256) DEFAULT NULL,
 PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
BEGIN;
INSERT INTO `oauth_client_details` VALUES ('client_lagou123',
'autodeliver,resume', 'abcxyz', 'all', 'password,refresh_token',
NULL, NULL, 7200, 259200, NULL, NULL);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;

客户端详情配置改造:

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        super.configure(clients);
        /*// 1. 从内存中加载客户端详情
        clients.inMemory()// 客户端信息存储在什么地方,可以在内存中,可以在数据库里
                .withClient("client_lagou")  // 添加一个client配置,指定其client_id
                 .secret("abcxyz")
                // 指定客户端的密码/安全码
                .resourceIds("autodeliver") // 指定客户端所能访问资源id清单,此处的资源id需要在具体的资源服务器上也配置一样
                // 认证类型/令牌颁发模式,可以配置多个在这里,但是不一定都用,具体使用哪种方式颁发token,需要客户端调用的时候传递参数指定
                .authorizedGrantTypes("password","refresh_token")
                // 客户端的权限范围,此处配置为all全部即可
                .scopes("all");*/
        
        // 2. 从数据库中加载客户端详情
        clients.withClientDetails(createJdbcClientDetailsService());
    }
    
    @Autowired
    private DataSource dataSource;
    
    @Bean
    public JdbcClientDetailsService createJdbcClientDetailsService() {
        return new JdbcClientDetailsService(dataSource);
    }

「从数据库验证用户合法性」

  1. 创建一个 users 表(表明不唯一)
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `username` char(10) DEFAULT NULL,
 `password` char(100) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of users
-- ----------------------------
BEGIN;
INSERT INTO `users` VALUES (4, 'lagou-user', 'iuxyzds');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
  1. 创建 POJO + DAO
@Entity
@Table(name = "users")
public class Users {

    @Id
    private Long id;
    private String username;
    private String password;
    // set+get 略
}

public interface UsersRepository extends JpaRepository<Users, Long> {
    Users findByUsername(String username);
}

使用jpa的话 启动类记得加上 @EntityScan("XXX.XXX.XXX") 注解

  1. 建立一个实现 org.springframework.security.core.userdetails.UserDetailsService 的自定义实现类
@Service
public class JdbcUserDetailsService implements UserDetailsService {
    
    @Autowired
    private UsersRepository usersRepository;
    
    /**
     * 根据用户名加载出用户的相关信息,封装成 UserDetails类型的对象
     * 密码字段,框架会自动匹配
     * @param username 用户名
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Users users = usersRepository.findByUsername(username);
        return new User(users.getUsername(), users.getPassword(),new ArrayList<>());
    }
}
  1. SecurityConfigurer 中使用上述 Service 进行改造
    @Autowired
    private JdbcUserDetailsService jdbcUserDetailsService;
    
    /**
     * 处理用户名和密码验证事宜
     * 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);*/
        
        // ↓ 改造为从数据库匹配用户信息
        auth.userDetailsService(jdbcUserDetailsService).passwordEncoder(passwordEncoder);
    }