前言

SpringBoot

  • Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。

  • 简单来说,SpringBoot是一个基于Spring框架之上的开源框架,是Spring的一套快速搭建脚手架,它提供了一种更加简单快捷的方式来配置和运行web应用程序,从而不需要去设置整个Spring配置。其基本特征是:“约定优于配置”(提供基于常见开发中常见配置的默认处理)和“开箱即用”(Out Of The Box)

  • 其主要目标如下:

    • 为所有Spring开发提供一个从根本上更快和更广泛使用的入门经验
    • 开箱即用,但你可以通过不采用默认设置来摆脱这种方式
    • 提供一系列大型项目常用的非功能性特征(如内嵌服务器、安全等)
    • 绝不需要代码生成和XML配置

SpringCloud

  • Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。

  • Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。

  • 简单来说,SpringCloud是基于SpringBoot的一套微服务架构体系解决方案,用于整合微服务应用

  • 包括两个组成:

    1. 对现有的成熟框架SpringBoot的封装和抽象,相当于SpringBoot项目的集合
    2. 对一部分分布式系统的基础设施的实现,如配置中心,服务治理中心,监控中心等

SpringBoot & SpringCloud

  • SpringBoot是基于Spring的一套快速搭建脚手架,可以用于快速开发单个微服务应用。
  • SpringCloud是基于SpringBoot的一套微服务架构体系解决方案,用于整合微服务应用
  • SpringBoot专注于单个微服务应用,而SpringCloud专注于全局的微服务治理
  • SpringBoot可以离开SpringCloud独立开发应用,SpringCloud离不开SpringBoot

搭建SpringCloud的初始环境

  • Spring Cloud的版本:

  • 大多数Spring项目都是以“主版本号.次版本号.增量版本号.里程碑版本号”的形式命名版本号, 例如Spring Framework稳定版本4.3.5.RELEASE、里程碑版本5.0.0.M4等。其中,主版本号表示项目的重大重构;次版本号表示新特性的添加和变化;增量版本号一般表示bug修复;里程碑版本号表示某版本号的里程碑。

  • 本项目选用核心依赖版本说明:

    • JDK 11
    • Spring Cloud Hoxton.SR12
    • Spring Boot 2.2.RELEASE

创建父Maven项目

  • 原始pom.xml文件内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>pers.fulsun</groupId>
    <artifactId>springcloud-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    </project>
  • 引入基本依赖

    • 继承Spring Boot Starter,声明自身是SpringBoot项目., spring-boot-starter-parent是一个特殊的starter,它用来提供相关的Maven默认依赖。使用它之后,常用的包依赖可以省去version标签
    1
    2
    3
    4
    5
    6
    <!-- 引入spring boot starter的依赖 -->
    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.2.RELEASE</version>
    </parent>
  • 统一版本管理(可有可无)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
    <project.version>1.0-SNAPSHOT</project.version>

    <spring-boot.version>2.2.12.RELEASE</spring-boot.version>
    <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    <spring-platform.version>Cairo-SR8</spring-platform.version>
    </properties>

    <dependencyManagement>
    <dependencies>
    <!--统一管理spring boot相关依赖版本-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>${spring-boot.version}</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    <!--统一管理spring cloud相关依赖版本-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-dependencies</artifactId>
    <version>${spring-cloud.version}</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    <!--spring版本管理工具-->
    <!--使用这一依赖可以实现spring相关第三方依赖自动适配版本号而不需要填写version-->
    <dependency>
    <groupId>io.spring.platform</groupId>
    <artifactId>platform-bom</artifactId>
    <version>${spring-platform.version}</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    </dependencies>
    </dependencyManagement>
  • 安装Maven插件spring-boot-maven-plugin (作为父工程实际上可以没有,此处引入亦可)

    • 该插件可以以Maven的方式为应用提供Spring Boot的支持,即能够将Spring Boot应用打包为可执行的jar或war文件,然后以通常的方式运行Spring Boot应用
    1
    2
    3
    4
    5
    6
    7
    8
    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>
  • 强制使用阿里云仓库(覆盖maven的setting配置,可有可无)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <repositories>
    <repository>
    <id>aliyun</id>
    <name>aliyun</name>
    <url>https://maven.aliyun.com/repository/public</url>
    <releases>
    <enabled>true</enabled>
    </releases>
    <snapshots>
    <enabled>false</enabled>
    </snapshots>
    </repository>
    </repositories>
  • 完整pom.xml文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>pers.fulsun</groupId>
    <artifactId>springcloud-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!-- 引入spring boot starter的依赖 -->
    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.12.RELEASE</version>
    </parent>
    <properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>

    <spring-boot.version>2.2.12.RELEASE</spring-boot.version>
    <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    <spring-platform.version>Cairo-SR8</spring-platform.version>

    <druid.version>1.1.10</druid.version>
    <mysql.version>5.1.45</mysql.version>
    <hutool.version>4.4.5</hutool.version>
    <mybatis-plus.version>3.1.0</mybatis-plus.version>
    </properties>
    <dependencies>
    <!-- 引入Lombok -->
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
    </dependency>

    <!--引入测试依赖-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-test</artifactId>
    <scope>test</scope>
    </dependency>
    </dependencies>

    <dependencyManagement>
    <dependencies>
    <!--统一管理spring boot相关依赖版本-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>${spring-boot.version}</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    <!--统一管理spring cloud相关依赖版本-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-dependencies</artifactId>
    <version>${spring-cloud.version}</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    <!--spring版本管理工具-->
    <!--使用这一依赖可以实现spring相关第三方依赖自动适配版本号而不需要填写version-->
    <dependency>
    <groupId>io.spring.platform</groupId>
    <artifactId>platform-bom</artifactId>
    <version>${spring-platform.version}</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    </dependencies>
    </dependencyManagement>
    <!-- 添加spring-boot的maven插件,不能少,打jar包时得用 -->
    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>
    <!--强制使用阿里云仓库-->
    <repositories>
    <repository>
    <id>aliyun</id>
    <name>aliyun</name>
    <url>https://maven.aliyun.com/repository/public</url>
    <releases>
    <enabled>true</enabled>
    </releases>
    <snapshots>
    <enabled>false</enabled>
    </snapshots>
    </repository>
    </repositories>


    </project>

整合 Spring Cloud Eureka

服务端工程

  • 父工程下创建注册中心服务端工程:springcloud-eureka,并引入spring-cloud-starter-netflix-eureka-server依赖包

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
  • pom文件如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
    <artifactId>springcloud-demo</artifactId>
    <groupId>pers.fulsun</groupId>
    <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>springcloud-eureka</artifactId>
    <description>注册中心</description>
    <dependencies>
    <!--注册中心-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    </dependencies>
    <!-- 添加spring-boot的maven插件,不能少,打jar包时得用 -->
    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>
    </project>
  • 配置如下: 在resource目录创建bootstrap.yml文件(SpringBoot中bootstrap.yml比application.yml文件优先级更高)

    1
    server:  port: 8761 #暴露服务端口  spring:    application:      name: springcloud-eureka #服务id  eureka:    client:      # 是否要注册到其他Eureka Server实例      register-with-eureka: false      # 是否要从其他Eureka Server实例获取数据      fetch-registry: false      service-url:        defaultZone: http://localhost:8761/eureka/ #供Eureka客户端使用的注册路径    server:      # eureka server清理无效节点的时间间隔,默认60000毫秒,即60秒      eviction-interval-timer-in-ms: 4000      # 自我保护模式,当出现出现网络分区、eureka在短时间内丢失过多客户端时,会进入自我保护模式      # 即一个服务长时间没有发送心跳,eureka也不会将其删除,默认为true      enable-self-preservation: false      # Eureka Server 自我保护系数,当enable-self-preservation=true时,起作用      renewal-percent-threshold: 0.9
  • 创建spring boot启动类 SpringCloudEurekaApplication,添加服务发现注解@EnableEurekaServer,表明当前工程是eureka服务端

    1
    /** * @author fulsun * @create: 2021-08-15 16:01 */@SpringBootApplication@EnableEurekaServerpublic class SpringCloudEurekaApplication {    public static void main(String[] args){        SpringApplication.run(SpringCloudEurekaApplication.class, args);    }}

客户端工程

  • 创建Eureka客户端工程:springcloud-client,也就是我们平常的业务功能工程

  • 引入依赖包:spring-cloud-starter-netflix-eureka-client

    1
    <?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <parent>        <artifactId>springcloud-demo</artifactId>        <groupId>pers.fulsun</groupId>        <version>1.0-SNAPSHOT</version>    </parent>    <modelVersion>4.0.0</modelVersion>    <artifactId>springcloud-client</artifactId>    <description>业务模块</description>    <dependencies>        <!--eureka 客户端-->        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>        </dependency>        <!--作为web项目存在-->        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>    </dependencies>    <!-- 添加spring-boot的maven插件,不能少,打jar包时得用 -->    <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>            </plugin>        </plugins>    </build></project>
  • 配置eureka注册信息: 在客户端的resource目录下创建bootstrap.yml文件

    1
    server:  port: 7900 #服务端口spring:  profiles:    active: dev #当前生效环境  application:    name: springcloud-client #指定应用的唯一标识/服务名# 注册中心配置eureka:  instance:    prefer-ip-address: true #优先使用IP地址注册  client:    service-url:      defaultZone: http://127.0.0.1:8761/eureka/ #eureka的注册地址
  • 创建客户端启动类 SpringCloudClientApplication,使用@EnableEurekaClient注解表明当前模块是Eureka客户端

    1
    /** * @author fulsun * @create: 2021-08-15 16:05 */@SpringBootApplication@EnableEurekaClientpublic class SpringCloudClientApplication {        public static void main(String[] args){        SpringApplication.run(SpringCloudClientApplication.class, args);    }    }

测试

整合Spring Cloud Config

  • 配置其实是独立于程序的可配变量,同一份程序在不同的配置下会表现不同的行为
    • 单机单体应用:传统的开发模式,所有服务由一个应用提供,拥有一个配置,修改需要重启应用
    • 多机单体应用(集群部署):将同一单体应用多方部署,修改配置需要批量修改,全部重启
    • 微服务模式:整体应用由多个独立服务提供功能,修改配置可单独修改重启服务,不影响别的服务运行

什么是分布式配置中心

  • 分布式配置中心实际上就是**将所有分布式服务的配置集中管理的一个组件,**该组件至少提供了配置集中管理以及动态配置功能。

  • 分布式配置中心的特点

    1. 集中化管理配置,所有服务配置到一个中央组件中
    2. 运行时动态修改配置,运行时修改配置无需重启应用,实时下发配置到各个服务
    3. 实时刷新配置
  • 为什么需要分布式配置中心

    • 由上述配置演进的三个阶段,我们可以看到一些基本问题:

      1. 配置杂乱繁复。如集群或微服务,某个节点或服务一旦有环境或某些配置不一致,将产生难以估量的问题
      2. 不同环境的配置需要分开管理。(一般开发环境,测试环境,正式环境的配置都有所区别,而没有集中管理的配置导致每次部署到不同环境都需要更换或指向不同的环境配置)
      3. 对集群部署的应用,一次性需要修改多个节点的应用配置,极易出现故障
      4. 每次修改配置都需要重新启动服务/应用,影响用户体验
    • 综上所述,一个集统一配置,动态配置和实时刷新特性于一身的分布式配置中心就解决了上述问题。

  • 分布式配置中心工作原理

    • 如上所述,分布式配置中心必须具备三个特性:统一配置、动态配置、实时刷新

    • 目前市面上的分布式配置中心解决方案主要有百度的disconf, 阿里的diamond,携程的disconf等,以及Spring Cloud自家的Spring Cloud Config

    • 由于Spring Cloud Config能够与Spring Cloud无缝整合,此处以Spring Cloud Config为例了解分布式配置中心的工作原理:

      • Spring Cloud Config是一个基于http协议的远程配置实现方式,与Spring Cloud Eureka的策略有异曲同工之妙,
      • Spring Cloud Config组件内部也设立了Config Server(配置中心)和Config Client(客户端)两个角色
      • Config Server: 分布式配置中心,统一配置管理的地方。默认使用git仓库管理配置文件也可以使用本地文件,同时为客户端提供配置获取接口
      • Config Client: 客户端,也就是我们应用中的业务服务。应用启动/需要的时候从配置中心获取配置,同时可以本地缓存以提高性能

分布式配置中心工程

  • 为简单起见,本Demo使用本地文件作为配置管理文件

  • 在父工程下创建分布式配置中心工程:springcloud-config , 并引入依赖 spring-cloud-config-server

    1
    <?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <parent>        <artifactId>com.springcloud.demo</artifactId>        <groupId>com.springcloud</groupId>        <version>1.0-SNAPSHOT</version>    </parent>    <modelVersion>4.0.0</modelVersion>    <artifactId>springcloud-config</artifactId>    <description>配置中心</description>    <dependencies>        <!--配置服务端-->        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-config-server</artifactId>        </dependency>        <!--作为web项目存在-->        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>                <!--断路器依赖-->        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>        </dependency>                <!--eureka 客户端-->        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>        </dependency>    </dependencies>        <!-- 添加spring-boot的maven插件,不能少,打jar包时得用 -->    <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>            </plugin>        </plugins>    </build></project>
  • 配置文件: 在resource目录下创建 bootstrap.yml 配置文件,作为springcloud-config 工程的配置文件,同时创建 configs 文件夹,用于管理后续各业务服务的配置文件

    1
    server:  port: 8080 #端口spring:  application:    name: springcloud-config-server #应用名  profiles:    active: native #环境  #配置中心  cloud:    config:      server:        native:          #此处表明使用本地目录/configs下的配置文件作为配置仓库          search-locations: classpath:/configs/# 注册中心配置eureka:  instance:    prefer-ip-address: true  client:    service-url:      defaultZone: http://localhost:8761/eureka/
  • 创建启动类 SpringCloudConfigApplication ,使用@EnableConfigServer表明当前工程是配置中心

    1
    @SpringBootApplication@EnableConfigServerpublic class SpringCloudConfigApplication {    public static void main(String[] args){        SpringApplication.run(SpringCloudConfigApplication.class, args);    }}

测试

  • 客户端继续使用 springcloud-client工程,并引入依赖 spring-cloud-config-client

    1
    <!--配置客户端--><dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-config-client</artifactId></dependency><!--实时刷新配置文件--><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-actuator</artifactId></dependency>
  • 完善springcloud-client工程的bootstrap.yml文件,加入配置中心关联信息

    1
    spring:  profiles:    active: dev #当前生效环境  application:    name: springcloud-client #指定应用的唯一标识/服务名  cloud:    config:      fail-fast: true      #指定当前工程于config server中的应用名,此处直接引用spring.application.name属性      name: ${spring.application.name}      #指定当前工程于config server中的生效环境,此处直接引用spring.profiles.active属性      profile: ${spring.profiles.active}      #指定配置中心的ip和端口      uri: http://localhost:8080
  • 客户端对应于配置中心的配置文件名规则为:应用名-生效环境.yml,如 spirngcloud-client-dev.yml对应于springcloud-client模块在dev环境下的配置文件

  • 在配置中心的configs文件夹下创建 springcloud-client-dev.yml 文件,添加基本自定义配置,此处只配置基本用例测试

    1
    user:  name: fulsun
  • 在 springcloud-client 工程中创建 TestController,模拟访问配置项

    1
    @RestController@RequestMapping("/client")public class TestConfigController {    @Value("${user.name}") //获取配置文件中对应属性的值    private String userName;    @GetMapping("/getName")    public String getUserName(){        return userName;    }    @PostMapping("/postName")    public String postName(@RequestBody Map<String,String> param){        return param.get("name");    }}
  • 启动Eureka注册中心和配置中心

  • 启动springcloud-client工程,并访问/getName接口,返回如下,证明连接配置成功

获取配置文件内容

整合Spring Cloud Gateway

  • 微服务网关: 介于客户端和服务器之间的中间层,所有外部请求都会先经过微服务网关

网关

  • 传统无网关访问的访问模式(客户端直接访问服务接口)的弊端

    • 复杂性 —- 客户端完成一个业务可能需要调用多个微服务接口,然而分布式微服务可能对应多个不同的ip与端口
    • 访问方式不友好 – 某些服务可能使用了并不友好的协议,如使用RPC和AMQP等只适合内部使用的协议
    • 认证复杂 —– 每个微服务需要独立认证
    • 跨域处理困难
    • 重构困难——-如果后期需要服务拆分或者服务合并,这种模式的实施将会极为困难
  • 使用网关可以针对性的解决传统访问模式的弊端:

    • 减少客户端对微服务的交互次数,降低客户端与服务端的耦合度
    • 易于认证,只需要在网关统一认证
    • 便于监控,可以在网关收集交互数据进行监控
    • 便于重构,因为客户端对微服务的访问路径及端口都是统一而少量的

nginx作为网关?

  • nginx作为负载均衡的利刃确实可以带来很多的集约化便利,但是它的主要着力点是对ip及端口的统一转发及分配,而不是api。
  • 这里介绍的微服务网关主要着力于微服务api的负载均衡和代理,如果使用nginx作为api网关,nginx需要为每一个api做代理配置,这个数量级是很高的,维护起来十分困难
  • 微服务网关必须可以做到api前缀适配和路由转发功能,所以nginx并不适合作为api网关,除此之外,网关还提供了负载均衡,限流降级等服务,有利于优化系统性能,分散压力

微服务网关

  • 目前,市面上比较常用的微服务网关解决方案主要是Nginx+ Lua、Kong、Spring Cloud Zuul,Spring Cloud Gateway等,其中Spring Cloud Gateway是Spring Cloud后期研发用于取待Zuul的一个网关组件,具有路由转发、权限校验、限流控制等功能

  • Spring Cloud Gateway的组成

    • Route(路由) — 网关的基本组件对象,指向一个api/服务,定义了一个id,一个目标uri,一组谓词和一组过滤器,如果谓词匹配为真,则匹配路由
    • Predicate(谓词) — 作为匹配路由的先行条件,是一个Java 8 Function Predicate,允许开发人员匹配HTTP请求的任何信息
    • Filter(过滤器) — 特定工程构建的Spring Framework GatewayFilter 的实例,用于在发送下游请求之前或之后处理请求与响应

Predicate

谓词工厂 谓词配置写法 谓词意义
After Route Predicate Factory predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
允许匹配指定日期时间之后发生的请求
Before Route Predicate Factory predicates:
- Before=2019-01-20T17:42:47.789-07:00[America/Denver]
允许匹配指定日期时间之前发生的请求
Between Route Predicate Factory predicates:
     - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2019-01-21T17:42:47.789-07:00[America/Denver]
允许匹配datetime1之后和datetime2之前发生的请求。datetime1必须在datetime2之前
Cookie Route Predicate Factory predicates:
- Cookie=chocolate, ch.p
允许匹配具有指定名称的cookie的请求
Header Route Predicate Factory predicates:
- Header=X-Request-Id, \d+
接收2个参数,一个header中的名称和一个正则表达式。允许匹配含有指定名称的header且其值符合正则表达式的请求
Host Route Predicate Factory predicates:
- Host=**.somehost.org
接收一个参数:主机名模式。该模式是一种 Ant 样式模式作为分隔符。允许匹配与该模式匹配的主机头部
Method Route Predicate Factory predicates:
- Method=GET
允许匹配指定请求方式的请求
Path Route Predicate Factory predicates:
- Path=/foo/{segment}
允许匹配指定路径前缀的请求
Query Route Predicate Factory predicates:
- Query=baz
接收2个参数: 一个必须的参数和一个可选的表达式。允许匹配请求参数中包含了指定参数的请求
RemoteAddr Route Predicate Factory predicates:
- RemoteAddr=192.168.1.1/24
允许匹配指定ip规则的请求

Filter

  • gateway过滤器的生命周期与其作用范围有关,其生命周期只有两个:“pre” 和 “post”。

  • “pre”和 “post” 分别会在请求被执行前调用和被执行后调用

  • Spring Cloud Gateway的源码包Filter中包含三个接口:GatewayFilterChain,GatewayFilter,GlobalFilter

    • GatewayFilterChain—–网关过滤链表接口,用于过滤器的链式调用

    • GatewayFilter—–网关路由过滤器, 有且只有一个方法filter,执行当前过滤器,并在此方法中决定过滤器链表是否继续往下执行。需要通过spring.cloud.route.filter在路由上配置或通过spring.cloud.default-filter将该过滤器作为全局过滤器

    • GlobalFilter—-请求业务以及路由的URI转换为真实业务服务的请求地址的核心过滤器,不需要配置,模式系统初始化时加载,并作用在每个路由上。

工作原理

  1. 客户端向网关发送请求

  2. 网关映射处理匹配谓词+匹配路由通过后,将请求转交到网关Web处理程序

  3. 网关Web处理程序请求通过一系列过滤器处理后发送到指定代理服务

  4. 指定代理服务处理完业务响应同样经过一系列过滤器处理后返回给客户端

网关服务工程

  • 在父工程下创建网关服务工程:springcloud-gateway ,并引入依赖spring-cloud-starter-gateway

  • spring-cloud-starter-gateway依赖使用的netty+webflux实现,已经是个网络工程,不要加入web依赖,否则会报错

    1
    <?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <parent>        <artifactId>springcloud-demo</artifactId>        <groupId>pers.fulsun</groupId>        <version>1.0-SNAPSHOT</version>    </parent>    <modelVersion>4.0.0</modelVersion>    <artifactId>springcloud-gateway</artifactId>    <description>网关处理模块</description>    <dependencies>        <!--微服务网关组件依赖-->        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-gateway</artifactId>        </dependency>        <!--健康检查监控-->        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-actuator</artifactId>        </dependency>                <!--配置中心客户端-->        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-config-client</artifactId>        </dependency>        <!--注册中心客户端-->        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>        </dependency>    </dependencies>        <!-- 添加spring-boot的maven插件,不能少,打jar包时得用 -->    <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>            </plugin>        </plugins>    </build></project>
  • 路由代理配置: 在resource目录下创建bootstrap.yml文件

    1
    server:  port: 9999spring:  profiles:    active: dev  application:    name: springcloud-gateway  cloud:    # 配置中心    config:      fail-fast: true      name: ${spring.application.name}      profile: ${spring.profiles.active}      uri: http://localhost:8080    #网关配置中心    gateway:      discovery:        #路由访问方式:http://Gateway_HOST:Gateway_PORT/大写的serviceId/**,其中微服务应用名默认大写访问。        locator:          #是否与服务发现组件进行结合,通过 serviceId(必须设置成大写) 转发到具体的服务实例。默认为false          # 设为true便开启通过服务中心的自动根据 serviceId 创建路由的功能。          enabled: true          #允许通过模块名小写代理          lower-case-service-id: true      routes:        - id: springcloud-client          uri: lb://springcloud-client #网关路由到springcloud-client模块,lb指向内部注册模块          predicates: #转发谓词,用于设置匹配路由的规则            - Path=/client/** #通过请求路径匹配  #            - Method=GET #通过请求方式匹配  #            - RemoteAddr=127.0.0.1/25 #通过请求id匹配,只有在某个 ip 区间号段的请求才会匹配路由,其中/后的是子网掩码  #            - After=2018-01-20T06:06:06+08:00[Asia/Shanghai] #根据时间进行匹配,在指定时间之后才会匹配路由  #            - Before=2018-01-20T06:06:06+08:00[Asia/Shanghai] #根据时间进行匹配,在指定时间之前才会匹配路由  #            - Between=2018-01-20T06:06:06+08:00[Asia/Shanghai], 2019-01-20T06:06:06+08:00[Asia/Shanghai] #根据时间段进行匹配,处于指定时间段才会匹配路由  main:    allow-bean-definition-overriding: true #是否允许同Bean覆盖# 注册中心配置eureka:  instance:    prefer-ip-address: true #优先使用IP地址注册  client:    service-url:      defaultZone: http://127.0.0.1:8761/eureka/
  • 编写启动类,Gateway并不需要特殊注解,使用十分便捷

    1
    @SpringBootApplication@EnableEurekaClientpublic class SpringCloudGatewayApplication {    public static void main(String[] args){        SpringApplication.run(SpringCloudGatewayApplication.class, args);    }}

测试

  • 客户端依旧使用springcloud-client工程,依序启动springcloud-eureka,springcloud-config,springcloud-client,springcloud-gateway工程, 通过7900端口访问Client模块,确保服务正常访问

    1
    D:\>curl "http://localhost:7900/client/getName"fulsun
  • 通过Gateway的9999端口访问Client接口,结果如下

    1
    D:\>curl "http://127.0.0.1:9999/client/getName"fulsun
  • 添加其他谓词测试路由匹配,只允许通过GET请求

    1
    spring:  cloud:    gateway:      routes:        - id: springcloud-client          predicates: #转发谓词,用于设置匹配路由的规则            - Path=/client/** #通过请求路径匹配            - Method=GET #通过请求方式匹配
  • 重启服务后,使用post方式访问client模块的postName接口, 访问成功

    1
    @PostMapping("/postName")public String postName(@RequestBody Map<String,String> param){    return param.get("name");}

  • 通过Gateway的9999端口访问client的postName接口,访问失败,报404

Filter

  • Spring Cloud gateway中的Filter从接口实现上分为两种一种是GatewayFilter,另外一种是GlobalFilter。
    • GatewayFilter与GlobalFilter的区别: 用英语可以总结如下: At a high level global filters are applied to all routes, while a gateway filter will be applied to an individual route(s)
    • 在一个高的角度来看,Global filters会被应用到所有的路由上,而Gateway filter将应用到单个路由上或者一个分组的路由上。
  • 在下面的案例中将会进行说明。

GatewayFilter

  • Contract for interception-style, chained processing of Web requests that may be used to implement cross-cutting, application-agnostic requirements such as security, timeouts, and others. Specific to a Gateway Copied from WebFilter

  • GatewayFilter是从WebFilter中Copy过来的,相当于一个Filter过滤器,可以对访问的URL过滤横切处理,应用场景比如超时,安全等。

  • 从Spring Cloud Gateway的源码中如下所示,可以看出GatewayFilter的使用场景:

    1
    /** * Contract for interception-style, chained processing of Web requests that may * be used to implement cross-cutting, application-agnostic requirements such * as security, timeouts, and others. Specific to a Gateway * * Copied from WebFilter * * @author Rossen Stoyanchev * @since 5.0 */public interface GatewayFilter extends ShortcutConfigurable {    String NAME_KEY = "name";    String VALUE_KEY = "value";    /**     * Process the Web request and (optionally) delegate to the next     * {@code WebFilter} through the given {@link GatewayFilterChain}.     * @param exchange the current server exchange     * @param chain provides a way to delegate to the next filter     * @return {@code Mono<Void>} to indicate when request processing is complete     */    Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);}

自定义GatewayFilter

  • 编写自定义网关路由过滤器-RequestTimeFilter,实现GatewayFilter接口,用于记录路由请求时间

    1
    @Slf4j@Componentpublic class RequestTimeFilter implements GatewayFilter, Ordered {    @Override    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {        exchange.getAttributes().put("startTime", System.currentTimeMillis());        log.info("===================pre阶段====================");        return chain.filter(exchange).then(                Mono.fromRunnable(() -> {                    Long startTime = exchange.getAttribute("startTime");                    if (startTime != null) {                        log.info("=========post阶段==============,执行路径:{},所耗时间:{}",exchange.getRequest().getURI().getRawPath(),(System.currentTimeMillis() - startTime) + "ms");                    }                }));    }    /**     * 设置当前过滤器的优先级,值越大,优先级越低     * @return     */    @Override    public int getOrder() {        return 100;    }}
  • 这种写法无法实现在配置文件中进行过滤器配置,需要手动关联路由,如下创建配置类

    1
    package pers.fulsun.demo.springcloud.config;import org.springframework.cloud.gateway.route.RouteLocator;import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import pers.fulsun.demo.springcloud.filter.RequestTimeFilter;/** * 网关配置 * * @author fulsun * @create: 2021-08-16 21:52 */@Configurationpublic class GatewayConfig {    /**     * 添加路由并给路由添加过滤器     *     * @param     * @return     */    @Bean    public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {        return builder.routes()            .route(            // 自定义了访问前缀,在真正的Gateway进行路由转发的时候,会用过f.stripPrefix(2)把前缀去掉。            // 使用场景:可以把对外暴露的URL通过加前缀分组打标。            r -> r.path("/test/prefix/client/**")            .filters(f -> f.stripPrefix(2)                     // 把自定义的Filter加到Filter链里面执行 GlobalFilter不需要加进去                     .filter(new RequestTimeFilter())                     .addResponseHeader("X-Response-Default-Foo", "Default-Bar"))            // 对spring.application.name等于springcloud-client源服务应用中的URL进行协议适配转发。            .uri("lb://springcloud-client")            .order(0)            .id("springcloud-client")        )            .build();    }}

过滤器工厂配置

  • 很多时候我们还是希望自定义的网关路由过滤器可以通过配置文件的形式添加到路由上,这时候需要通过过滤器工厂来实现

  • 编写过滤器工厂-RequestTimeGatewayFilterFactory,实现自定义过滤器配置

    1
    package pers.fulsun.demo.springcloud.filter;import lombok.extern.slf4j.Slf4j;import org.springframework.cloud.gateway.filter.GatewayFilter;import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;import org.springframework.stereotype.Component;import reactor.core.publisher.Mono;import java.util.Arrays;import java.util.List;/** * 自定义网关路由过滤器工厂 * * @author fulsun * @create: 2021-08-17 08:09 */@Slf4j@Component //将过滤器工厂注册为beanpublic class RequestTimeGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestTimeGatewayFilterFactory.Config> {    public RequestTimeGatewayFilterFactory() {        super(Config.class);    }    @Override    public List<String> shortcutFieldOrder() {        return Arrays.asList("enabled");    }    //这个name方法 用来在yml配置中指定对应的过滤器名称    @Override    public String name() {        return "RequestTime2";    }    /**     * 过滤器执行体     *     * @param config     * @return     */    @Override    public GatewayFilter apply(RequestTimeGatewayFilterFactory.Config config) {        return (exchange, chain) -> {            if (!config.isEnabled()) { //如果没有启用过滤器,则自动跳过                return chain.filter(exchange);            }            exchange.getAttributes().put("startTime", System.currentTimeMillis());            log.info("===================pre阶段====================");            return chain.filter(exchange).then(                Mono.fromRunnable(() -> {                    Long startTime = exchange.getAttribute("startTime");                    if (startTime != null) {                        log.info("=========post阶段==============");                        log.info("执行路径:{},所耗时间:{}",                                 exchange.getRequest().getURI().getRawPath(),                                 (System.currentTimeMillis() - startTime) + "ms");                    }                }));        };    }    /** * 过滤器工厂在配置文件中的配置属性 */    public static class Config {        // 控制是否开启认证        private boolean enabled;        public Config() {        }        public boolean isEnabled() {            return enabled;        }        public void setEnabled(boolean enabled) {            this.enabled = enabled;        }    }}
  • 默认名字为RequestTimeGatewayFilterFactory,可以重写name()方法修改,在配置文件中进行路由过滤器配置:filters

    1
    spring:  cloud:    #网关配置中心    gateway:      discovery:        #路由访问方式:http://Gateway_HOST:Gateway_PORT/大写的serviceId/**,其中微服务应用名默认大写访问。        locator:          #是否与服务发现组件进行结合,通过 serviceId(必须设置成大写) 转发到具体的服务实例。默认为false          # 设为true便开启通过服务中心的自动根据 serviceId 创建路由的功能。          enabled: true          #允许通过模块名小写代理          lower-case-service-id: true      routes:        # 路由的id,没有规定规则但要求唯一,建议配合服务名        - id: springcloud-client          #网关路由到springcloud-client模块,lb指向内部注册模块          uri: lb://springcloud-client          predicates: #转发谓词,用于设置匹配路由的规则            - Path=/client/** #通过请求路径匹配            - Method=GET #通过请求方式匹配  #            - RemoteAddr=127.0.0.1/25 #通过请求id匹配,只有在某个 ip 区间号段的请求才会匹配路由,其中/后的是子网掩码  #            - After=2018-01-20T06:06:06+08:00[Asia/Shanghai] #根据时间进行匹配,在指定时间之后才会匹配路由  #            - Before=2018-01-20T06:06:06+08:00[Asia/Shanghai] #根据时间进行匹配,在指定时间之前才会匹配路由  #            - Between=2018-01-20T06:06:06+08:00[Asia/Shanghai], 2019-01-20T06:06:06+08:00[Asia/Shanghai] #根据时间段进行匹配,处于指定时间段才会匹配路由          filters:            #此处仅需要配置过滤器工厂前缀及其属性值启动即可(注意“-”后面一定要跟空格,否则报错)            - RequestTime2=true
  • 访问http://localhost:9999/client/getName?token=fulsun

    1
    2021-08-17 20:59:23.231  INFO 12192 --- [ctor-http-nio-3] .f.d.s.f.RequestTimeGatewayFilterFactory : ===================pre阶段====================2021-08-17 20:59:23.242  INFO 12192 --- [ctor-http-nio-4] .f.d.s.f.RequestTimeGatewayFilterFactory : =========post阶段==============2021-08-17 20:59:23.242  INFO 12192 --- [ctor-http-nio-4] .f.d.s.f.RequestTimeGatewayFilterFactory : 执行路径:/client/getName,所耗时间:11ms

全局过滤器

  • 编写全局过滤器 GlobalTokenFilter,实现GlobalFilter接口,模拟进行登陆认证。注意要加上@Component注解

    1
    package pers.fulsun.demo.springcloud.filter;import lombok.extern.slf4j.Slf4j;import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.GlobalFilter;import org.springframework.core.Ordered;import org.springframework.core.io.buffer.DataBuffer;import org.springframework.http.HttpStatus;import org.springframework.http.server.reactive.ServerHttpResponse;import org.springframework.stereotype.Component;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Flux;import reactor.core.publisher.Mono;/** * @author fulsun * @create: 2021-08-16 00:33 */@Component //将GlobalTokenFilter注册成bean方可生效@Slf4jpublic class GlobalTokenFilter implements GlobalFilter, Ordered {    /**     * 过滤器执行体     */    @Override    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {        /**         * 获取token         */        String token = exchange.getRequest().getQueryParams().getFirst("token");        if (token == null || token.isEmpty()) {            log.error("您尚未登陆,请登陆后重试");            ServerHttpResponse response = exchange.getResponse();            response.setStatusCode(HttpStatus.UNAUTHORIZED);            //指定编码,否则在浏览器中会中文乱码            response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");            DataBuffer buffer = response.bufferFactory().wrap("您尚未登陆,请登陆后重试".getBytes());            return response.writeWith(Mono.just(buffer));        }        /**         * 继续执行下一过滤器/调用接口         */        return chain.filter(exchange);    }    /**     * 设置当前过滤器的优先级,值越大,优先级越低     */    @Override    public int getOrder() {        return -100;    }}

整合数据库连接池-Druid

资源池

  • 一种设计模式,系统初始化的时候创建一组资源,放到一个池子里。需要的时候从资源池里面选一个出来工作,用完了放回去,而不是随时创建和销毁。
  • 资源的创建与销毁都是比较耗时的,资源池有效避免了频繁创建和销毁资源的消耗问题,能有效的提高程序的性能,常见的资源池有线程池,内存池,java对象池以及数据库连接池等。

数据库连接池

  • 负责分配、管理和释放数据库连接的资源池,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个

  • 可以通过设置数据库连接池的最大连接数来防止系统过量连接数据库,并根据数据库连接池的管理机制监视数据库连接

Druid

  • 目前市面上主流的数据库连接池主要是C3P0,DBCP,BoneCP及Druid等

  • Druid能提供强大的监控及扩展功能,是阿里巴巴开发的号称为监控而生的数据库连接池!

  • Druid具备以下功能:

    1. 充当数据库连接池
    2. 监控数据库/网络访问性能
    3. 获得SQL执行日志

整合Druid

  • 我们依旧使用springcloud-client工程试手, 引入依赖:druid-spring-boot-starter,该部分内容抽离作为工具模块供业务模块引入使用,所以新建了一个工程名为 common-data, 并 maven install 安装到本地供client工程使用

    1
    <artifactId>common-data</artifactId><description>作为数据的公共模块,如缓存以及数据库操作等</description><dependencies>    <!--mysql 驱动-->    <dependency>        <groupId>mysql</groupId>        <artifactId>mysql-connector-java</artifactId>    </dependency>    <!--druid连接池-->    <dependency>        <groupId>com.alibaba</groupId>        <artifactId>druid-spring-boot-starter</artifactId>    </dependency>    <!--配置客户端-->    <dependency>        <groupId>org.springframework.cloud</groupId>        <artifactId>spring-cloud-config-client</artifactId>    </dependency>    <!--spring boot 提供的jdbc支持-->    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-jdbc</artifactId>    </dependency>    <!--druid对spring监控需要的aop-->    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-aop</artifactId>    </dependency>    <!--作为web项目存在-->    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>    </dependency></dependencies>
  • 主依赖添加依赖管理

    1
    <properties>    <druid.version>1.2.6</druid.version>    <mysql.version>8.0.25</mysql.version>    <hutool.version>4.4.5</hutool.version>    <mybatis-plus.version>3.1.0</mybatis-plus.version></properties><dependencies>    <!--druid连接池-->    <dependency>        <groupId>com.alibaba</groupId>        <artifactId>druid-spring-boot-starter</artifactId>        <version>${druid.version}</version>    </dependency>    <!-- 数据库连接包:mysql -->    <dependency>        <groupId>mysql</groupId>        <artifactId>mysql-connector-java</artifactId>        <version>${mysql.version}</version>    </dependency>    <!--基本数据操作模块-->    <dependency>        <groupId>pers.fulsun</groupId>        <artifactId>common-data</artifactId>        <version>${project.version}</version>    </dependency></dependencies>
  • springcloud-client 中引入基本数据操作模块

    1
    <!--引入基本数据操作模块--><dependency>    <groupId>pers.fulsun</groupId>    <artifactId>common-data</artifactId></dependency>
  • 在config配置中心的configs目录下添加 datasource-dev.yml 文件,专门配置统一数据库信息

    1
    spring: # 配置数据库信息  datasource:    # 数据源配置    username: root    password: 123456    # 设置时区    url: jdbc:mysql://192.168.56.101:3306/test?serverTimezone=GMT%2B8&characterEncoding=UTF-8&useSSL=false    driver-class-name: com.mysql.cj.jdbc.Driver    type: com.alibaba.druid.pool.DruidDataSource   # 指定数据源为druid    # 数据库连接池配置    druid:      # 初始化 最小 最大      initial-size: 5      min-idle: 5      max-active: 20      # 配置获取连接等待超时的时间      max-wait: 60000      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒      time-between-eviction-runs-millis: 60000      # 配置一个连接在池中最小生存的时间,单位是毫秒      min-evictable-idle-time-millis: 300000      validation-query: SELECT 'x'      test-while-idle: true      test-on-borrow: false      test-on-return: false      # 打开PSCache,并且指定每个连接上PSCache的大小      poolPreparedStatements: true      maxPoolPreparedStatementPerConnectionSize: 20      # 配置多个英文逗号分隔      filters: stat,wall
  • 在启动springcloud-client模块时初始化数据库连接,所以应该在springcloud-client模块的配置文件指向datasource配置文件

    1
    server:  port: 7900 #服务端口spring:  profiles:    active: dev #当前生效环境  application:    name: springcloud-client #指定应用的唯一标识/服务名  cloud:    config:      fail-fast: true      # 指定当前工程于config server中的应用名,此处直接引用spring.application.name属性      # 此处包括datasource,届时启动初始化环境会包含datasource-{spring.profiles.active}.yml文件-     name: ${spring.application.name}+     name: ${spring.application.name},datasource      #指定当前工程于config server中的生效环境,此处直接引用spring.profiles.active属性      profile: ${spring.profiles.active}      #指定配置中心的ip和端口      uri: http://localhost:8080# 注册中心配置eureka:  instance:    prefer-ip-address: true #优先使用IP地址注册  client:    service-url:      defaultZone: http://127.0.0.1:8761/eureka/ #eureka的注册地址
  • 启动springcloud-client模块,验证数据库是否配置成功,部分启动日志如下:

    1
    # 加载配置文件2021-08-17 21:13:54.393  INFO 9584 --- [           main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://localhost:80802021-08-17 21:13:55.201  INFO 9584 --- [           main] c.c.c.ConfigServicePropertySourceLocator : Located environment: name=springcloud-client,datasource, profiles=[dev], label=null, version=null, state=null2021-08-17 21:13:55.203  INFO 9584 --- [           main] b.c.PropertySourceBootstrapConfiguration : Located property source: [BootstrapPropertySource {name='bootstrapProperties-classpath:/configs/datasource-dev.yml'}, BootstrapPropertySource {name='bootstrapProperties-classpath:/configs/springcloud-client-dev.yml'}]2021-08-17 21:13:55.209  INFO 9584 --- [           main] p.f.d.s.SpringCloudClientApplication     : The following profiles are active: dev。。。。# 数据库初始化2021-08-17 21:50:46.892  INFO 2660 --- [           main] c.a.d.s.b.a.DruidDataSourceAutoConfigure : Init DruidDataSource2021-08-17 21:50:47.956  INFO 2660 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
  • 可以看到,当限定dev环境下启动springcloud-client模块时,同时读取的是springcloud-client-dev.yml及datasource-dev.yml文件的内容进行初始化,其中的{dataSource-1} inited 表示数据库初始化完成,也就是数据库连接成功, 至此,数据库连接池Druid与Spring Boot/Spring Cloud的基本整合完成。

Druid的SQL监控

  • Druid的监控功能主要围绕StatFilter展开,包含SQL执行监控,Web关联监控以及Spring关联监控等功能。

    • 配置视图Servlet—–StatViewServlet
    • Druid内置提供了一个StatViewServlet用于展示Druid的统计信息
  • 这个StatViewServlet的用途包括:

    • 提供监控信息展示的html页面

    • 提供监控信息的JSON API

    1
    @Configurationpublic class DruidConfiguration {    @Bean    public ServletRegistrationBean druidServlet() {        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean();        //stat视图设置        servletRegistrationBean.setServlet(new StatViewServlet());        servletRegistrationBean.addUrlMappings("/druid/*"); //对应路径映射        Map<String, String> initParameters = new HashMap();        initParameters.put("loginUsername", "admin"); // 登录监控页面的用户名        initParameters.put("loginPassword", "admin"); // 登录监控页面的密码        initParameters.put("resetEnable", "false"); // 禁用HTML页面上的“Reset All”功能        initParameters.put("allow", ""); // IP白名单 (没有配置或者为空,则允许所有访问)        initParameters.put("deny", "192.168.20.38");// IP黑名单 (存在共同时,deny优先于allow),如果满足deny的话提示:Sorry, you are not permitted to view this page.        servletRegistrationBean.setInitParameters(initParameters);        return servletRegistrationBean;    }}
  • 访问http://localhost:7900/druid/,登录后我们可以看到SQL防火墙栏目记录了防火墙对SQL调用的统计指数

配置Web关联监控

  • 监控web请求通过webStatFilter

    1
    @Configurationpublic class DruidConfiguration {    @Bean    public FilterRegistrationBean filterRegistrationBean() {        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();        WebStatFilter webStatFilter = new WebStatFilter();        webStatFilter.setProfileEnable(true);         webStatFilter.setSessionStatEnable(true);         filterRegistrationBean.setFilter(webStatFilter);         filterRegistrationBean.addUrlPatterns("/*");        Map<String, String> initParameters = new HashMap();        initParameters.put("sessionStatMaxCount", "1000");        initParameters.put("principalCookieName", "USER_COOKIE");        initParameters.put("principalSessionName", "USER_SESSION");        initParameters.put("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); //设置忽略资源        filterRegistrationBean.setInitParameters(initParameters);        return filterRegistrationBean;    }}
  • 请求访问Client的getName接口 http://localhost:7900/client/getName, 再去查看URI监控栏目信息,可以看到Druid的监控可以记录对系统的URI请求

配置Spring关联监控

  • 相当于创建切面对Spring请求的controller及Service等进行监控

    1
    package pers.fulsun.demo.springcloud;import com.alibaba.druid.support.spring.stat.DruidStatInterceptor;import org.aspectj.lang.annotation.Aspect;import org.springframework.aop.Advisor;import org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor;import org.springframework.aop.support.JdkRegexpMethodPointcut;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Scope;@Aspect@Configurationpublic class DruidSpringConfiguration {    /**     * 拦截器     *     * @return     */    @Bean    public DruidStatInterceptor druidStatInterceptor() {        return new DruidStatInterceptor();    }    /**     * 切点     *     * @return     */    @Bean    @Scope("prototype")    public JdkRegexpMethodPointcut druidStatPointcut() {        JdkRegexpMethodPointcut jdkRegexpMethodPointcut = new JdkRegexpMethodPointcut();        //指明需要监控的类        jdkRegexpMethodPointcut.setPatterns("pers.fulsun.demo.springcloud.controller.*", "pers.fulsun.demo.springcloud.service.*");        return jdkRegexpMethodPointcut;    }    /**     * 通知     *     * @return     */    @Bean    public Advisor druidAdviceAdvisor() {        DefaultBeanFactoryPointcutAdvisor defaultBeanFactoryPointcutAdvisor = new DefaultBeanFactoryPointcutAdvisor();        defaultBeanFactoryPointcutAdvisor.setAdvice(druidStatInterceptor());        defaultBeanFactoryPointcutAdvisor.setPointcut(druidStatPointcut());        return defaultBeanFactoryPointcutAdvisor;    }}
  • 一样去请求访问Client模块的getName接口,然后打开Spring监控模块,可以看到Druid监控具体到了Spring系统中的类接口访问的详细信息

整合Mybatis Plus工具

Mybatis Plus

  • 一个开源的,国内开发的Mybais增强工具,在Mybatis的基础上只做增强,不做改变,为简化开发、提高效率而生。

  • Mybatis Plus的核心功能

    • 代码生成器:AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率
  • 通用CRUD接口: Mybatis Plus在Mapper层(BaseMapper)及Service(IService)层都默认封装了通用的CRUD接口,可以极大的减少数据库操作业务代码

  • 条件构造器: Wrapper 实体包装器,用于处理 sql 拼接,排序,实体参数查询等!

  • 可扩展插件:

    • 逻辑删除—-为了方便数据恢复和保护数据本身价值等等的一种方案,并不从数据库移除记录,但实际就是删除
    • 分页插件—-更方便的分页查询
    • 自动填充—-数据创建或更新时触发自动填充字段

实践整合Mybatis Plus

  • 我们依旧使用springcloud-client工程试手,引入依赖 mybatis-plus-boot-starter,结合上一篇文章的common-data的pom文件如下:

    1
    <?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <parent>        <artifactId>springcloud-common</artifactId>        <groupId>com.springcloud</groupId>        <version>1.0-SNAPSHOT</version>    </parent>    <modelVersion>4.0.0</modelVersion>    <artifactId>common-data</artifactId>    <description>作为数据的公共模块,如缓存以及数据库操作等</description>    <dependencies>        <!--mysql 驱动-->        <dependency>            <groupId>mysql</groupId>            <artifactId>mysql-connector-java</artifactId>        </dependency>        <!--druid连接池-->        <dependency>            <groupId>com.alibaba</groupId>            <artifactId>druid-spring-boot-starter</artifactId>        </dependency>        <!--spring boot 提供的jdbc支持-->        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-jdbc</artifactId>        </dependency>        <!--druid对spring监控需要的aop-->        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-aop</artifactId>        </dependency>        <!--mybatis plus-->        <dependency>            <groupId>com.baomidou</groupId>            <artifactId>mybatis-plus-boot-starter</artifactId>        </dependency>        <!--mybatis plus extension,包含了mybatis plus core-->        <dependency>            <groupId>com.baomidou</groupId>            <artifactId>mybatis-plus-extension</artifactId>        </dependency>    </dependencies></project>
  • 添加配置,修改config模块中的datasource-dev.yml配置,增加mybatis-plus配置

    1
    # mybatis-plus 配置mybatis-plus:  # mybatis的mapper文件扫描路径  # 如果是放在src/main/java目录下 classpath:/com/yourpackage/*/mapper/*Mapper.xml  # 如果是放在resource目录 classpath:/mapper/*Mapper.xml  #  config-location: classpath:/mybatis/mybatis-config.xml  mapper-locations: classpath:/mapper/*Mapper.xml  #实体扫描,多个package用逗号或者分号分隔  typeAliasesPackage: pers.fulsun.demo.springcloud.entity  global-config:    #主键类型  0:"数据库ID自增", 1:"用户输入ID",2:"全局唯一ID (数字类型唯一ID)", 3:"全局唯一ID UUID";    id-type: 3    # 热加载mapper文件    refresh: true    db-config:      db-type: mysql      #逻辑删除默认设置      logic-delete-value: 1 #删除后的状态 默认值1      logic-not-delete-value: 0 #逻辑前的值 默认值0

代码生成器

  • 整合代码生成器,并使用代码生成器生成基本代码。

    • Mybatis Plus使用AutoGenerator作为代码生成器,实际上与Mybatis 的逆向工程是一个概念,只不过针对Mybatis Plus做了一次封装,通过Mybatis的逆向工程与模板文件实现代码生成功能

      1
      SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;-- ------------------------------ Table structure for client-- ----------------------------DROP TABLE IF EXISTS `client`;CREATE TABLE `client`  (  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',  `client_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '客户端名称',  `client_path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '客户端路径',  `del_flag` int(10) NULL DEFAULT NULL COMMENT '逻辑删除标志',  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',  `update_time` datetime NULL DEFAULT NULL COMMENT '最后更新时间',  PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;SET FOREIGN_KEY_CHECKS = 1;
    • 引入模板依赖,本项目使用的是velocity模板(mybatis plus默认使用的模板)生成代码,故引入如下依赖:

      1
      <!--模板文件依赖--><dependency>    <groupId>org.apache.velocity</groupId>    <artifactId>velocity-engine-core</artifactId>    <version>2.1</version></dependency><dependency>    <groupId>com.baomidou</groupId>    <artifactId>mybatis-plus-generator</artifactId>	<!--<version>${mybatis-plus.version}</version>--></dependency>
    • 在comm-data模块中添加代码生成器工具类CodeGenerator(代码摘自官网示例,稍作更改):

      1
      package com.springcloud.demo.util;import com.baomidou.mybatisplus.annotation.DbType;import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;import com.baomidou.mybatisplus.core.toolkit.StringPool;import com.baomidou.mybatisplus.core.toolkit.StringUtils;import com.baomidou.mybatisplus.generator.AutoGenerator;import com.baomidou.mybatisplus.generator.InjectionConfig;import com.baomidou.mybatisplus.generator.config.*;import com.baomidou.mybatisplus.generator.config.po.TableInfo;import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;import java.io.File;import java.util.ArrayList;import java.util.List;import java.util.Scanner;public class CodeGenerator {        /**         * 读取控制台内容         */        public static String scanner(String tip) {            Scanner scanner = new Scanner(System.in);            StringBuilder help = new StringBuilder();            help.append("请输入" + tip + ":");            System.out.println(help.toString());            if (scanner.hasNext()) {                String ipt = scanner.next();                if (StringUtils.isNotEmpty(ipt)) {                    return ipt;                }            }            throw new MybatisPlusException("请输入正确的" + tip + "!");        }        public static void main(String[] args) {            // 代码生成器            AutoGenerator mpg = new AutoGenerator();            //设置模板引擎为FreeMarker,默认引擎为Velocity            //mpg.setTemplateEngine(new FreemarkerTemplateEngine());            /** ======================================全局策略配置start=============================== **/            String moudleName = scanner("模块名");            String tableName = scanner("表名,多个英文逗号分割");            //整体路径,默认所有自动生成代码归属于业务模块moudle总目录下的目标模块            String projectPath = System.getProperty("user.dir")+StringPool.SLASH+"springcloud-moudle"+StringPool.SLASH+moudleName;            //核验项目目录,如果项目不存在,不予创建生成代码            File file = new File(projectPath);            if(!file.exists()){                throw new MybatisPlusException("请核验正确目录,当前项目不存在/n");            }            GlobalConfig gc = new GlobalConfig();            //设置输出文件位置            gc.setOutputDir(projectPath+ "/src/main/java");            gc.setAuthor("lai.guanfu"); //作者            gc.setOpen(false); //是否打开输出目录,默认为true            gc.setFileOverride(false);// 是否覆盖同名文件,默认是false            gc.setEnableCache(false);// XML 二级缓存            gc.setIdType(IdType.AUTO);//ID自增            // 自定义文件命名,注意 %s 会自动填充表实体属性!            //自定义Service的名称,如果不设置,默认Freemarker模板生成的Service类格式是:I%sService             gc.setServiceName("%sService");             gc.setBaseResultMap(true);            mpg.setGlobalConfig(gc);            /** ======================================全局策略配置end=============================== **/            /** ======================================数据源配置【通过该配置,指定需要生成代码的具体数据库】start=============================== **/            DataSourceConfig dsc = new DataSourceConfig();            dsc.setUrl("jdbc:mysql://localhost:3306/spring?useUnicode=true&useSSL=false&characterEncoding=utf8");            dsc.setDriverName("com.mysql.jdbc.Driver");            dsc.setUsername("spring");            dsc.setPassword("spring");            dsc.setDbType(DbType.MYSQL);            mpg.setDataSource(dsc);            /** ======================================数据源配置end=============================== **/            /** ======================================数据库表配置【通过该配置,可指定需要生成哪些表或者排除哪些表】start=============================== **/            StrategyConfig strategy = new StrategyConfig();            //数据库表映射到实体的命名策略,此处指定下划线转小驼峰            strategy.setNaming(NamingStrategy.underline_to_camel);            //数据库表字段映射到实体的命名策略,此处指定下划线转小驼峰            strategy.setColumnNaming(NamingStrategy.underline_to_camel);            //自定义继承的Entity类全称,带包名            //strategy.setSuperEntityClass("com.springcloud.entity.BaseEntity");            //是否为lombok模型            strategy.setEntityLombokModel(true);            //生成 @RestController 控制器            strategy.setRestControllerStyle(true);            //需要包含的表名,允许正则表达式(与exclude二选一配置),多个表格以英文逗号分隔            strategy.setInclude(tableName.split(","));            //strategy.setSuperEntityColumns("id");            //驼峰转连字符            strategy.setControllerMappingHyphenStyle(true);            //是否生成实体时,生成字段注解            strategy.setEntityTableFieldAnnotationEnable(true);            //strategy.setTablePrefix(pc.getModuleName() + "_");            mpg.setStrategy(strategy);            /** ======================================数据库表配置end=============================== **/            /** ======================================包名配置【通过该配置,指定生成代码的包路径】start=============================== **/            PackageConfig pc = new PackageConfig();            //父模块名,会拼接到父包名后面作为包            //pc.setModuleName(scanner("模块名"));            //父包名,如果为空,将下面子包名必须写全部, 否则就只需写子包名            pc.setParent("com.springcloud.demo");            mpg.setPackageInfo(pc);            /** ======================================包名配置end=============================== **/            /** ======================================注入配置【通过该配置,可注入自定义参数等操作以实现个性化操作】start=============================== **/            InjectionConfig cfg = new InjectionConfig() {                @Override                public void initMap() {                    // to do nothing                }            };            // 模板路径,如果模板引擎是 freemarker            //String templatePath = "/templates/mapper.xml.ftl";            // 模板路径,如果模板引擎是 velocity             String templatePath = "/templates/mapper.xml.vm";            // 自定义输出配置文件(mapper.xml)            List<FileOutConfig> focList = new ArrayList<>();            // 自定义配置会被优先输出            focList.add(new FileOutConfig(templatePath) {                @Override                public String outputFile(TableInfo tableInfo) {                    // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!                    return projectPath + "/src/main/resources/mapper/"                            + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;                }            });            cfg.setFileOutConfigList(focList);            mpg.setCfg(cfg);            /** ======================================注入配置end=============================== **/            /** ======================================模板配置【可自定义代码生成的模板,实现个性化操作】start=============================== **/            // 配置模板            TemplateConfig templateConfig = new TemplateConfig();            // 配置自定义输出模板            //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别,此处使用自带Freemarker模板即可            // templateConfig.setEntity("templates/entity2.java");            // templateConfig.setService();            // templateConfig.setController();            templateConfig.setXml(null);            mpg.setTemplate(templateConfig);            /** ======================================模板配置end=============================== **/            mpg.execute();        }}
  • 运行输入后执行结果:

    1
    请输入模块名:springcloud-client请输入表名,多个英文逗号分割:student17:34:53.006 [main] DEBUG com.baomidou.mybatisplus.generator.AutoGenerator - ==========================准备生成文件...==========================Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.17:34:53.993 [main] WARN org.apache.velocity.deprecation - configuration key 'input.encoding' has been deprecated in favor of 'resource.default_encoding'17:34:53.996 [main] WARN org.apache.velocity.deprecation - configuration key 'file.resource.loader.class' has been deprecated in favor of 'resource.loader.file.class'17:34:53.997 [main] WARN org.apache.velocity.deprecation - configuration key 'file.resource.loader.path' has been deprecated in favor of 'resource.loader.file.path'17:34:53.997 [main] WARN org.apache.velocity.deprecation - configuration key 'file.resource.loader.unicode' has been deprecated in favor of 'resource.loader.file.unicode'17:34:53.999 [main] DEBUG com.baomidou.mybatisplus.generator.engine.AbstractTemplateEngine - 创建目录: [C:\Users\fsun7\Desktop\springcloud-demo//springcloud-client/src/main/java\pers\fulsun\demo\springcloud\entity]17:34:54.002 [main] DEBUG com.baomidou.mybatisplus.generator.engine.AbstractTemplateEngine - 创建目录: [C:\Users\fsun7\Desktop\springcloud-demo//springcloud-client/src/main/java\pers\fulsun\demo\springcloud\service]17:34:54.004 [main] DEBUG com.baomidou.mybatisplus.generator.engine.AbstractTemplateEngine - 创建目录: [C:\Users\fsun7\Desktop\springcloud-demo//springcloud-client/src/main/java\pers\fulsun\demo\springcloud\mapper]17:34:54.006 [main] DEBUG com.baomidou.mybatisplus.generator.engine.AbstractTemplateEngine - 创建目录: [C:\Users\fsun7\Desktop\springcloud-demo//springcloud-client/src/main/java\pers\fulsun\demo\springcloud\service\impl]17:34:54.010 [main] DEBUG org.apache.velocity - Initializing Velocity, Calling init()...17:34:54.011 [main] DEBUG org.apache.velocity - Starting Apache Velocity v2.317:34:54.018 [main] DEBUG org.apache.velocity - Default Properties resource: org/apache/velocity/runtime/defaults/velocity.properties17:34:54.037 [main] DEBUG org.apache.velocity - ResourceLoader instantiated: org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader17:34:54.039 [main] DEBUG org.apache.velocity - initialized (class org.apache.velocity.runtime.resource.ResourceCacheImpl) with class java.util.Collections$SynchronizedMap cache map.17:34:54.041 [main] DEBUG org.apache.velocity - Loaded System Directive: org.apache.velocity.runtime.directive.Stop17:34:54.042 [main] DEBUG org.apache.velocity - Loaded System Directive: org.apache.velocity.runtime.directive.Define17:34:54.044 [main] DEBUG org.apache.velocity - Loaded System Directive: org.apache.velocity.runtime.directive.Break17:34:54.045 [main] DEBUG org.apache.velocity - Loaded System Directive: org.apache.velocity.runtime.directive.Evaluate17:34:54.047 [main] DEBUG org.apache.velocity - Loaded System Directive: org.apache.velocity.runtime.directive.Macro17:34:54.049 [main] DEBUG org.apache.velocity - Loaded System Directive: org.apache.velocity.runtime.directive.Parse17:34:54.051 [main] DEBUG org.apache.velocity - Loaded System Directive: org.apache.velocity.runtime.directive.Include17:34:54.052 [main] DEBUG org.apache.velocity - Loaded System Directive: org.apache.velocity.runtime.directive.Foreach17:34:54.083 [main] DEBUG org.apache.velocity.parser - Created '20' parsers.17:34:54.117 [main] DEBUG org.apache.velocity.macro - "velocimacro.library.path" is not set. Trying default library: velocimacros.vtl17:34:54.118 [main] DEBUG org.apache.velocity.loader.file - Could not load resource 'velocimacros.vtl' from ResourceLoader org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader17:34:54.118 [main] DEBUG org.apache.velocity.macro - Default library velocimacros.vtl not found. Trying old default library: VM_global_library.vm17:34:54.118 [main] DEBUG org.apache.velocity.loader.file - Could not load resource 'VM_global_library.vm' from ResourceLoader org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader17:34:54.118 [main] DEBUG org.apache.velocity.macro - Old default library VM_global_library.vm not found.17:34:54.118 [main] DEBUG org.apache.velocity.macro - allowInline = true: VMs can be defined inline in templates17:34:54.118 [main] DEBUG org.apache.velocity.macro - allowInlineToOverride = false: VMs defined inline may NOT replace previous VM definitions17:34:54.119 [main] DEBUG org.apache.velocity.macro - allowInlineLocal = false: VMs defined inline will be global in scope if allowed.17:34:54.119 [main] DEBUG org.apache.velocity.macro - autoload off: VM system will not automatically reload global library macros17:34:54.143 [main] DEBUG org.apache.velocity.loader - ResourceManager: found /templates/mapper.xml.vm with loader org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader17:34:54.162 [main] DEBUG com.baomidou.mybatisplus.generator.engine.AbstractTemplateEngine - 模板:/templates/mapper.xml.vm;  文件:C:\Users\fsun7\Desktop\springcloud-demo//springcloud-client/src/main/resources/mapper//StudentMapper.xml17:34:54.188 [main] DEBUG org.apache.velocity.loader - ResourceManager: found /templates/entity.java.vm with loader org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader17:34:54.194 [main] DEBUG com.baomidou.mybatisplus.generator.engine.AbstractTemplateEngine - 模板:/templates/entity.java.vm;  文件:C:\Users\fsun7\Desktop\springcloud-demo//springcloud-client/src/main/java\pers\fulsun\demo\springcloud\entity\Student.java17:34:54.197 [main] DEBUG org.apache.velocity.loader - ResourceManager: found /templates/mapper.java.vm with loader org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader17:34:54.199 [main] DEBUG com.baomidou.mybatisplus.generator.engine.AbstractTemplateEngine - 模板:/templates/mapper.java.vm;  文件:C:\Users\fsun7\Desktop\springcloud-demo//springcloud-client/src/main/java\pers\fulsun\demo\springcloud\mapper\StudentMapper.java17:34:54.205 [main] DEBUG org.apache.velocity.loader - ResourceManager: found /templates/service.java.vm with loader org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader17:34:54.211 [main] DEBUG com.baomidou.mybatisplus.generator.engine.AbstractTemplateEngine - 模板:/templates/service.java.vm;  文件:C:\Users\fsun7\Desktop\springcloud-demo//springcloud-client/src/main/java\pers\fulsun\demo\springcloud\service\StudentService.java17:34:54.216 [main] DEBUG org.apache.velocity.loader - ResourceManager: found /templates/serviceImpl.java.vm with loader org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader17:34:54.220 [main] DEBUG com.baomidou.mybatisplus.generator.engine.AbstractTemplateEngine - 模板:/templates/serviceImpl.java.vm;  文件:C:\Users\fsun7\Desktop\springcloud-demo//springcloud-client/src/main/java\pers\fulsun\demo\springcloud\service\impl\StudentServiceImpl.java17:34:54.223 [main] DEBUG org.apache.velocity.loader - ResourceManager: found /templates/controller.java.vm with loader org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader17:34:54.227 [main] DEBUG com.baomidou.mybatisplus.generator.engine.AbstractTemplateEngine - 模板:/templates/controller.java.vm;  文件:C:\Users\fsun7\Desktop\springcloud-demo//springcloud-client/src/main/java\pers\fulsun\demo\springcloud\controller\StudentController.java17:34:54.227 [main] DEBUG com.baomidou.mybatisplus.generator.AutoGenerator - ==========================文件生成完成!!!==========================
  • 默认情况下,Mybatis Plus代码生成工具生成的代码里面,xxxMapper.java和xxxEntity.java实体类是不具备@Mappery以及@TableName注解的,必须手动去添加,而我们的工程是需要这两个注解的去扫描Bean的。

    1
    /** * <p> *  Mapper 接口 * </p> * * @author fulsun * @since 2021-08-18 */public interface StudentMapper extends BaseMapper<Student> {}
    1
    @Data@EqualsAndHashCode(callSuper = false)@Accessors(chain = true)public class Student implements Serializable {    private static final long serialVersionUID=1L;    @TableId(value = "id", type = IdType.AUTO)    private Integer id;    @TableField("name")    private String name;    @TableField("age")    private String age;    @TableField("createdate")    private LocalDate createdate;    @TableField("updatedate")    private LocalDate updatedate;}
  • 我们可以看到mybatis plus generator的默认模板文件位于其jar包的templates目录下

  • 解决方案:修改生成这些实体类的模板文件,拷贝其中的entity.java.vm以及mapper.java.vm文件到common-data工程目录下,进行修改

    • 修改后的Entity.java.vm,将@TableName(“${table.name}”) 的限制去除掉,默认会添加注解

      1
      +import com.baomidou.mybatisplus.annotation.TableName;-#if(${table.convert})@TableName("${table.name}")-#end -#if(${entitySerialVersionUID})-    private static final long serialVersionUID=1L;-#end+private static final long serialVersionUID=1L;
    • 修改后的Mapper.java.vm,直接在原基础上添加@Mapper注解

      1
      package ${package.Mapper};import ${package.Entity}.${entity};import ${superMapperClassPackage};+import org.apache.ibatis.annotations.Mapper;/** * <p> * $!{table.comment} Mapper 接口 * </p> * * @author ${author} * @since ${date} */#if(${kotlin})interface ${table.mapperName} : ${superMapperClass}<${entity}>#else+@Mapperpublic interface ${table.mapperName} extends ${superMapperClass}<${entity}> {}#end
    • 然后更改CodeGenerator 类的TemplateConfig配置,指定生成Entity以及Mapper类的模板文件

      1
      // 配置模板TemplateConfig templateConfig = new TemplateConfig();// 配置自定义输出模板//指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别,此处使用自带Freemarker模板即可templateConfig.setEntity("/template/entity.java");templateConfig.setMapper("/template/mapper.java");
    • 重新运行CodeGenerator 后生成的Client以及ClientMapper都有了注解。

整合逻辑删除插件

  • 现实开发中可能涉及到一些比较重要的业务数据,我们希望可以在实现删除功能的同时留有恢复或备份的余地,逻辑删除就是实现删除功能但并不移除数据记录的功能,与物理删除相反

  • 利用代码生成器生成的User类详情如下:

    1
    @Data@EqualsAndHashCode(callSuper = false)@Accessors(chain = true)@TableName("user" )public class User implements Serializable {private static final long serialVersionUID=1L;    @TableId(value = "id", type = IdType.AUTO)    private Integer id;    @TableField("name")    private String name;    @TableField("age")    private Integer age;    @TableField("is_delete")    private Boolean isDelete;}
  • 在controller中书写新增,获取及删除client记录的接口:

    1
    package pers.fulsun.demo.springcloud.controller;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import pers.fulsun.demo.springcloud.entity.User;import pers.fulsun.demo.springcloud.service.UserService;/** * <p> * 前端控制器 * </p> * * @author fulsun * @since 2021-08-18 */@RestController@RequestMapping("/user" )public class UserController {    @Autowired    private UserService userService;    /**     * 新增user     *     * @param user user对象     * @return     */    @PostMapping    public boolean adduser(@RequestBody User user) {        return this.userService.save(user);    }    /**     * 根据id获取user     *     * @param id user的id     * @return     */    @GetMapping("/{id}" )    public User getuserById(@PathVariable Integer id) {        return this.userService.getById(id);    }    /**     * 根据id删除user     *     * @param id user的id     * @return     */    @DeleteMapping("/{id}" )    public boolean removeuser(@PathVariable Integer id) {        return this.userService.removeById(id);    }}
  • 测试查询接口

  • 调用删除client记录接口, 数据库记录被移除

  • 加配置, 在common-data工程增加Mybatis Plus配置类,配置引入逻辑删除插件

    1
    mybatis-plus:  global-config:    db-config:      logic-delete-value: 1 #删除后的状态 默认值1      logic-not-delete-value: 0 #逻辑前的值 默认值0
  • 加注解,在实体类User的逻辑删除标志属性添加@Logic

    1
    /**     * 逻辑删除标志     */    @TableLogic    @TableField("is_delete")    private Boolean isDelete;
  • 重新添加记录后调用删除接口,打开 mybastis-plus的日志记录

    1
    mybatis-plus:  configuration:    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    1
    JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@20beaca8] will not be managed by Spring==>  Preparing: UPDATE user SET is_delete=1 WHERE id=? AND is_delete=0 ==> Parameters: 1(Integer)<==    Updates: 0
  • 注:使用mybatis plus 自带方法删除和查找都会附带逻辑删除功能 (自己写的xml不会)

    1
    ==>  Preparing: SELECT id,is_delete,name,age FROM user WHERE id=? AND is_delete=0 ==> Parameters: 1(Integer)<==      Total: 0

整合分页插件

  • mybatis plus默认整合PaginationInterceptor作为分页插件,使用也是十分方便, 在common-data模块中引入分页插件

    1
    /** * mybatis plus 配置 * * @author fulsun * @create: 2021-08-19 13:55 */@Configurationpublic class MPConfiguration {    /**     * 分页插件     */    @Bean    public PaginationInterceptor paginationInterceptor() {        return new PaginationInterceptor();    }}
  • IServive接口提供了四个关于分页的接口 page / pageMap供我们直接调用

  • 在controller编写一个分页接口,调用Service的分页方法

    1
    @GetMapping("/page")public IPage getClientPage(Page page, User user){    return this.userService.page(page, new QueryWrapper<>(user));}

自定义分页

  • 如果不用mybatis plus提供的封装接口,我们又应该怎么使用分页插件实现分页呢?(如何自定义分页方法)

  • 解答:如官方所述,等同于编写一个普通 list 查询,mybatis-plus 自动替你分页

  • 定义接口: Page参数一定要放在第一位

    1
    @Mapperpublic interface UserMapper extends BaseMapper<User> {    /**     * 自定义分页方法(注:Page参数一定要放在第一位,否则会失效)     * @param page 分页对象     * @param user 传入对象参数     * @return     */    IPage<User> selectUserPage(Page page, @Param("user") User user);}
  • 编写mapper.xml文件

    1
    <?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="pers.fulsun.demo.springcloud.mapper.UserMapper">    <!-- 通用查询映射结果 -->    <resultMap id="BaseResultMap" type="pers.fulsun.demo.springcloud.entity.User">        <id column="id" property="id" />        <result column="name" property="name" />        <result column="age" property="age" />        <result column="is_delete" property="isDelete" />        <result column="create_date" property="createDate" />        <result column="update_date" property="updateDate" />    </resultMap>        <select id="selectUserPage" resultMap="BaseResultMap">        SELECT *        FROM user        <if test="user.name!=null">            WHERE client_name LIKE CONCAT('%',#{user.name},'%')        </if>    </select></mapper>
  • controller调用实现

    1
    @GetMapping("/page2")public IPage<User> getClientPage2(Page page, User user){    return this.userMapper.selectUserPage(page,user);}
  • 调用结果如下

  • 由于我们并没有过滤掉逻辑删除的数据,所以会导致已经被删除的数据也被查询出来了, 修改ClientMapper.xml文件,增加过滤条件

    1
    <select id="selectUserPage" resultMap="BaseResultMap">    SELECT *    FROM user    WHERE    <include refid="noDelSQL"></include>    <if test="user.name!=null">        client_name LIKE CONCAT('%',#{user.name},'%')    </if></select><!--未被删除条件模板--><sql id="noDelSQL">    is_delete = 0</sql>
  • 再次运行,已经过滤掉逻辑删除的内容

实现自动填充字段

  • 很多时候,在业务数据表中总会存在业务无关的必需字段,用以维护系统,比如上述代码中的create_time与update_time两个字段,实际上按照阿里开发规约,还应该存在create_by(创建人)以及update_by(最后更新人)两个字段。

  • 而很明显,我们并不希望每次新增或更改数据的时候还要执行额外的操作去添加/修改这几个字段,也不想依赖数据库去辅助完成,这时候,Mybatis Plus的自动填充字段功能可以帮助我们在执行业务操作的时候同步完成这些非业务操作,即自动填充非业务字段

  • 数据库增加create_by,update_by两个字段,用于记录创建人及最后修改人,修改entity下的实体类

    1
    @TableField("create_by")    private String createBy;        @TableField("update_by")    private String updateBy;
  • 加配置,编写填充处理器MPMetaObjectHandler

    1
    package pers.fulsun.demo.springcloud.handler;import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;import org.apache.ibatis.reflection.MetaObject;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;import java.time.LocalDateTime;/** * mybatisplus自定义填充公共字段 ,即没有传的字段自动填充 * * @author fulsun * @create: 2021-08-19 14:17 */@Componentpublic class MPMetaObjectHandler implements MetaObjectHandler {    /**     * 获取逻辑删除的未删除值作为默认初始值     */    @Value("${mybatis-plus.global-config.db-config.logic-not-delete-value}")    private Integer isDeleteDefaultValue;    /**     * 插入填充     *     * @param metaObject 目标对象     */    @Override    public void insertFill(MetaObject metaObject) {        Object createBy = metaObject.getValue("createBy");        Object createDate = metaObject.getValue("createDate");        Object isDelete = metaObject.getValue("isDelete");        //判断是否存在该字段        if (null == createBy) {            this.setFieldValByName("createBy", "admin", metaObject);        }        if (null == createDate) {            this.setFieldValByName("createDate", LocalDateTime.now(), metaObject);        }        //填充isDelete属性        if (null == isDelete) {            this.setFieldValByName("isDelete", isDeleteDefaultValue, metaObject);        }        this.updateFill(metaObject);    }    /**     * 更新填充     *     * @param metaObject 目标对象     */    @Override    public void updateFill(MetaObject metaObject) {        Object updateBy = metaObject.getValue("updateBy");        Object updateDate = metaObject.getValue("updateDate");        //设置当前登录用户        if (null == updateBy) {            this.setFieldValByName("updateBy", "admin", metaObject);        }        if (null == updateDate) {            this.setFieldValByName("updateDate", LocalDateTime.now(), metaObject);        }    }}
  • 加注解,字段注解中添加填充标志, 其中的FieldFill代表了填充字段的触发事件

    • 枚举解释如下:DEFAULT(默认不处理),INSERT(插入时填充字段),UPDATE(更新时填充字段),INSERT_UPDATE(插入和更新时填充字段)
    1
    /**     * 逻辑删除标志     */    @TableLogic    @TableField(value = "is_delete",fill = FieldFill.INSERT)    private Boolean isDelete;    /**     * 创建人     */    @TableField(value = "create_by", fill = FieldFill.INSERT)    private String createBy;    /**     * 创建时间     */    @TableField(value = "create_date", fill = FieldFill.INSERT)    private LocalDateTime createDate;    /**     * 最后更新人     */    @TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)    private String updateBy;    /**     * 最后更新时间     */    @TableField(value = "update_date", fill = FieldFill.INSERT_UPDATE)    private LocalDateTime updateDate;
  • 调用接口插入数据,可以看到五个字段都被注入了默认信息

    1
    JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@6e3d3ebf] will not be managed by Spring==>  Preparing: INSERT INTO user ( create_by, update_date, update_by, is_delete, name, age, create_date ) VALUES ( ?, ?, ?, ?, ?, ?, ? ) ==> Parameters: admin(String), 2021-08-19T14:49:58.188971800(LocalDateTime), admin(String), 0(Byte), ww(String), 134(Integer), 2021-08-19T14:49:58.188971800(LocalDateTime)<==    Updates: 1

整合 Redis 缓存

缓存

  • 缓存在不同场景下有不同的作用:

    • 操作系统的磁盘缓存:减少磁盘机械操作

    • 数据库缓存:减少文件系统I/O

    • 应用程序缓存:减少数据库查询操作

    • web服务器缓存:减少对服务器的请求

    • 客户端浏览器缓存:减少对网站的访问

  • 经典缓存策略:

    • 查询数据,先查缓存,如缓存无数据再进行数据库查询

    • 更新数据,先更新缓存,再更新数据库

Redis

  • 一个NoSql数据库,由C语言编写,数据模型是key-value

  • 为什么使用Redis作为缓存

    • NoSql 数据库结构简单,易扩展
    • Redis 支持多种数据类型,包括String,List,Set,zSet,hash等
    • Redis 支持数据持久化,在内存数据缓存的同时将数据备份在磁盘中,重启时可以重新加载使用,而不会丢失
    • Redis 单个value的最大限制为1GB,容量巨大

整合Redis缓存

  • 加依赖,在common-data模块添加缓存依赖 spring-boot-starter-data-redis

    1
    <!--缓存依赖--><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-data-redis</artifactId></dependency>
  • 加配置, 在config工程的/resource/configs目录下,加入redis-dev.yml配置文件,进行redis配置(由于仅本地测试运行,配置一个redis即可)

    1
    spring:  redis:    database: 1  # Redis数据库索引(默认为0)    host: 127.0.0.1  # Redis服务器地址    password:  # Redis服务器连接密码(默认为空)    port: 6379  # Redis服务器连接端口    timeout: 1000  # 连接超时时间(毫秒)    pool:      max-active: 200  # 连接池最大连接数(使用负值表示没有限制)      max-idle: 10  # 连接池中的最大空闲连接      min-idle: 0  # 连接池中的最小空闲连接      max-wait: -1  # 连接池最大阻塞等待时间(使用负值表示没有限制)
  • 编写工具类, 在common-data模块添加RedisHelper.java,用以封装提供更方便快捷的Redis操作(我们可以直接使用RedisTemplate)

    1
    package pers.fulsun.demo.springcloud.helper;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Component;import org.springframework.util.CollectionUtils;import java.util.List;import java.util.Map;import java.util.Set;import java.util.concurrent.TimeUnit;/** * @author fulsun * @create: 2021-08-19 14:59 */@Componentpublic class RedisHelper {    @Autowired    private StringRedisTemplate redisTemplate;    //=============================common============================    /**     * 指定缓存失效时间     *     * @param key  键     * @param time 时间(秒)     * @return     */    public boolean expire(String key, long time) {        try {            if (time > 0) {                redisTemplate.expire(key, time, TimeUnit.SECONDS);            }            return true;        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    /**     * 根据key 获取过期时间     *     * @param key 键 不能为null     * @return 时间(秒) 返回0代表为永久有效     */    public long getExpire(String key) {        return redisTemplate.getExpire(key, TimeUnit.SECONDS);    }    /**     * 判断key是否存在     *     * @param key 键     * @return true 存在 false不存在     */    public boolean hasKey(String key) {        try {            return redisTemplate.hasKey(key);        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    /**     * 删除缓存     *     * @param key 可以传一个值 或多个     */    public void del(String... key) {        if (key != null && key.length > 0) {            if (key.length == 1) {                redisTemplate.delete(key[0]);            } else {                redisTemplate.delete(CollectionUtils.arrayToList(key));            }        }    }    //============================String=============================    /**     * 普通缓存获取     *     * @param key 键     * @return 值     */    public String get(String key) {        return key == null ? null : redisTemplate.opsForValue().get(key);    }    /**     * 普通缓存放入     *     * @param key   键     * @param value 值     * @return true成功 false失败     */    public boolean set(String key, String value) {        try {            redisTemplate.opsForValue().set(key, value);            return true;        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    /**     * 普通缓存放入并设置时间     *     * @param key   键     * @param value 值     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期     * @return true成功 false 失败     */    public boolean set(String key, String value, long time) {        try {            if (time > 0) {                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);            } else {                set(key, value);            }            return true;        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    /**     * 递增     *     * @param key   键     * @param delta 要增加几(大于0)     * @return     */    public long incr(String key, long delta) {        if (delta < 0) {            throw new RuntimeException("递增因子必须大于0");        }        return redisTemplate.opsForValue().increment(key, delta);    }    /**     * 递减     *     * @param key   键     * @param delta 要减少几(小于0)     * @return     */    public long decr(String key, long delta) {        if (delta < 0) {            throw new RuntimeException("递减因子必须大于0");        }        return redisTemplate.opsForValue().increment(key, -delta);    }}
  • 使用Redis缓存, 在client模块的配置文件中,修改配置指向redis-dev配置

    1
    cloud:    config:      fail-fast: true      # 指定当前工程于config server中的应用名,此处直接引用spring.application.name属性      # 此处包括datasource,届时启动初始化环境会包含datasource-{spring.profiles.active}.yml文件-      name: ${spring.application.name},datasource+      name: ${spring.application.name},datasource,redis      #指定当前工程于config server中的生效环境,此处直接引用spring.profiles.active属性      profile: ${spring.profiles.active}      #指定配置中心的ip和端口      uri: http://localhost:8080
  • 添加controller方法

    1
    package pers.fulsun.demo.springcloud.controller;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import pers.fulsun.demo.springcloud.entity.User;import pers.fulsun.demo.springcloud.helper.RedisHelper;import pers.fulsun.demo.springcloud.service.UserService;/** * @author fulsun * @create: 2021-08-19 15:03 */@RestController@RequestMapping("/redis")public class RedisController {    @Autowired    private UserService userService;    //引入redis操作工具    @Autowired    private RedisHelper redisHelper;    /**     * 新增user     *     * @param user user对象     * @return     */    @PostMapping    public boolean addUser(@RequestBody User user) throws JsonProcessingException {        //填充缓存,key为user_${id}        return this.redisHelper.set("user_" + user.getId(), new ObjectMapper().writeValueAsString(user));    }    /**     * 根据id获取user     *     * @param id user的id     * @return     */    @GetMapping("/{id}")    public User getUserById(@PathVariable Integer id) throws JsonProcessingException {        //首先从缓存获取        String s = this.redisHelper.get("user_" + id);        return new ObjectMapper().readValue(s, User.class);    }    /**     * 根据id删除user     *     * @param id user的id     * @return     */    @DeleteMapping("/{id}")    public void removeUser(@PathVariable Integer id) {        //从缓存移除        this.redisHelper.del("user_" + id);    }}

接口生成工具 Swagger2

什么是RESTFUL接口?

  • REST 表述性状态转移(Representational State TransferT)

  • REST 是面向资源的,强调描述应用程序的事物和名词

    • 表述性( Representational ): REST 资源实际上可以用各种形式来进行表述,包括 XML 、 JSON ( JavaScript Object Notation )甚至HTML—— 最适合资源使用者的任意形式;
    • 状态( State ):当使用 REST 的时候,我们更关注资源的状态而不是对资源采取的行为;
    • 转移( Transfer ): REST 涉及到转移资源数据,它以某种表述性形式从一个应用转移到另一个应用。
  • 简单来讲:REST 就是将资源的状态以最适合客户端或服务端的形式从服务器端转移到客户端(或者反过来)

  • 表述是 REST 中很重要的一个方面。它是关于客户端和服务器端针对某一资源是如何通信的。任何给定的资源都几乎可以用任意的形式来进行表述。如果资源的使用者愿意使用 JSON ,那么资源就可以用 JSON 格式来表述。如果使用者喜欢尖括号,那相同的资源可以用 XML 来进行表述。同时,如果用户在浏览器中查看资源的话,可能更愿意以 HTML 的方式来展现(或者 PDF 、 Excel 及其他便于人类阅读的格式)。资源没有变化 —— 只是它的表述方式变化了

  • 对于大多数客户端来说,用 JSON 和 XML 来进行表述就足够了

RESTFUL

  • RESTFUL是一种架构的规范与约束、原则,符合这种规范的架构就是RESTful架构。

  • RESTFUL风格的接口针对资源的添加,删除,修改和列表查询进行设计

Swagger2

  • 是一款RESTFUL接口的、基于YAML、JSON语言的文档在线自动生成、代码自动生成的工具

  • 传统API文档基本上都是手写的,手写API文档的缺点:

  • 当接口文档需要更新时,需要重新发给前端人员,交流不及时,接口文档繁杂,不易管理。无法在线测试接口,需要借助其他工具

  • 而Swagger2可以解决上述问题,通过在线文档的形式,解决前后端接口对接的时延和误差,加快后端开发人员的接口开发及测试速度

Swagger2的常用api

注解 使用 描述
@Api 用于类 对当前类的描述说明,value:标识当前类,decriptioni:描述,
@ApiOperation 用于方法 表示一个http的请求
@ApiImplicitParam 用于方法 单独说明一个参数,
name:参数名;
value:参数的具体意义,作用;
required : 参数是否必填;
dataType:数据类型;
paramType:参数来源类型【path/query/body/header/form】;
@ApiModel 用于类 表示对类进行说明(用于将类作为参数)
@ApiModelProperty 用于方法,字段 表示对modle属性的说明
@ApiResponse 用于方法 表示操作返回的可能结果
@ApiResponses 用于方法 用于包含多个@ApiResponse
@ApiParam 用于方法 用于参数字段说明

整合Swagger2

  • 加依赖, 新建工程common-swagger,专门作为swagger配置及工具使用(将工程放在springcloud-common工程下)

  • 引入依赖:springfox-swagger2 和 springfox-swagger-ui

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
    <artifactId>springcloud-common</artifactId>
    <groupId>com.springcloud</groupId>
    <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>common-swagger</artifactId>
    <properties>
    <swagger.version>2.9.2</swagger.version>
    </properties>
    <dependencies>
    <!--swagger 依赖-->
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>${swagger.version}</version>
    </dependency>

    <!--一个无依赖的HTML、JS和CSS集合,可以为Swagger兼容API动态生成优雅文档。-->
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>${swagger.version}</version>
    </dependency>
    </dependencies>
    </project>
  • 加配置, 在common-swagger工程下新建Swagger2配置类 Swagger2Config.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    package pers.fulsun.demo.springcloud.config;

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.context.request.async.DeferredResult;
    import springfox.documentation.builders.ApiInfoBuilder;
    import springfox.documentation.builders.PathSelectors;
    import springfox.documentation.builders.RequestHandlerSelectors;
    import springfox.documentation.service.ApiInfo;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spring.web.plugins.Docket;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;

    @EnableSwagger2 //开启swagger
    @Configuration
    public class Swagger2Config {
    @Bean
    public Docket createRestApi() {
    return new Docket(DocumentationType.SWAGGER_2)
    .genericModelSubstitutes(DeferredResult.class)
    .useDefaultResponseMessages(false)
    .forCodeGeneration(true)
    .apiInfo(apiInfo()) // 增加api相关信息
    .pathMapping("/")// base,最终调用接口后会和paths拼接在一起
    .select() // 通过select函数可控制选择哪些接口允许暴露给swagger展示
    // .apis(RequestHandlerSelectors.basePackage("pers.fulsun.demo.springcloud.controller")) //指定扫描包进行接口展示限制
    // .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
    .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
    .paths(PathSelectors.any())
    .build();
    }
    private ApiInfo apiInfo() {
    return new ApiInfoBuilder() //
    .title("springboot利用swagger构建api文档") //标题
    .description("简单优雅的restfun风格") //描述
    .termsOfServiceUrl("https://github.com/springfox/springfox-demos") //服务条款网址
    .version("1.0") //
    .build();
    }
    }
  • 加注解, springcloud-client(业务工程)中,在Client实体添加注解

    1
    2
    3
    4
    5
    6
    7
    8
    +import io.swagger.annotations.ApiModel;
    +import io.swagger.annotations.ApiModelProperty;
    +@ApiModel(value = "User", description = "用户实体")
    public class User implements Serializable {
    + @ApiModelProperty(value = "用户id", dataType = "Integer", name = "id", required = true)
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    }
  • 在ClientController添加注解:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Api(value = "UserController", description = "用户端模块前端控制器", tags = {"client"})
    public class UserController {
    @Autowired
    private UserService userService;
    @Autowired
    private UserMapper userMapper;

    /**
    * 新增user
    *
    * @param user user对象
    * @return
    */
    @PostMapping
    @ApiOperation(value = "添加用户", notes = "添加用户", response = Boolean.class, produces = "application/json")
    @ApiImplicitParams({
    @ApiImplicitParam(name = "user", value = "用户实体", required = true, dataType = "User", paramType = "body"),
    })
    public boolean adduser(@RequestBody User user) {
    return this.userService.save(user);
    }
    }
  • 启动服务,从client的端口进入查看接口文档页面:http://localhost:7900/swagger-ui.html

OAuth

  • 一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而不需要将用户名和密码提供给第三方应用。
  • OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站在特定的时段内访问特定的资源。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。目前版本是2.0

  • OAuth 2.0 的一个简单解释 - 阮一峰的网络日志 (ruanyifeng.com)

    • 有没有一种办法,让快递员能够自由进入小区,又不必知道小区居民的密码,而且他的唯一权限就是送货,其他需要密码的场合,他都没有权限?

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      于是,我设计了一套授权机制。

      第一步,门禁系统的密码输入器下面,增加一个按钮,叫做"获取授权"。快递员需要首先按这个按钮,去申请授权。

      第二步,他按下按钮以后,屋主(也就是我)的手机就会跳出对话框:有人正在要求授权。系统还会显示该快递员的姓名、工号和所属的快递公司。

      我确认请求属实,就点击按钮,告诉门禁系统,我同意给予他进入小区的授权。

      第三步,门禁系统得到我的确认以后,向快递员显示一个进入小区的令牌(access token)。令牌就是类似密码的一串数字,只在短期内(比如七天)有效。

      第四步,快递员向门禁系统输入令牌,进入小区。

      有人可能会问,为什么不是远程为快递员开门,而要为他单独生成一个令牌?这是因为快递员可能每天都会来送货,第二天他还可以复用这个令牌。另外,有的小区有多重门禁,快递员可以使用同一个令牌通过它们。
  • 我们把上面的例子搬到互联网,就是 OAuth 的设计了。

    • 首先,居民小区就是储存用户数据的网络服务。比如,微信储存了我的好友信息,获取这些信息,就必须经过微信的”门禁系统”。

    • 其次,快递员(或者说快递公司)就是第三方应用,想要穿过门禁系统,进入小区。

    • 最后,我就是用户本人,同意授权第三方应用进入小区,获取我的数据。

    • 简单说,OAuth 就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。

  • 令牌与密码

    • 令牌(token)与密码(password)的作用是一样的,都可以进入系统,但是有三点差异。
      1. 令牌是短期的,到期会自动失效,用户自己无法修改。密码一般长期有效,用户不修改,就不会发生变化。
      2. 令牌可以被数据所有者撤销,会立即失效。以上例而言,屋主可以随时取消快递员的令牌。密码一般不允许被他人撤销。
      3. 令牌有权限范围(scope),比如只能进小区的二号门。对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。
    • 上面这些设计,保证了令牌既可以让第三方应用获得权限,同时又随时可控,不会危及系统安全。这就是 OAuth 2.0 的优点。
  • 注意,只要知道了令牌,就能进入系统。系统一般不会再次确认身份,所以令牌必须保密,泄漏令牌与泄漏密码的后果是一样的。 这也是为什么令牌的有效期,一般都设置得很短的原因。

  • OAuth 2.0 对于如何颁发令牌的细节,规定得非常详细。具体来说,一共分成四种授权类型(authorization grant),即四种颁发令牌的方式,适用于不同的互联网场景。

    • 授权码(authorization-code)
    • 隐藏式(implicit)
    • 密码式(password):
    • 客户端凭证(client credentials)

OAuth2

  • OAuth 2.0授权框架使第三方应用程序能够获得对HTTP服务的有限访问权限,代表资源所有者通过编排资源批准所有者和HTTP服务之间的交互,或允许第三方应用程序代表自己获取访问权限,这个规范取代并废弃OAuth 1.0协议【摘自RFC 6749】

  • 简单来说,OAuth2是一个授权框架,用于控制第三方应用对HTTP服务资源的访问权限, OAuth2.0是OAuth协议的延续版本,但不向前兼容OAuth 1.0。

为什么需要OAuth2?

  • 问题场景:

    • 我们在一个网站/论坛冲浪浏览,发现网站有个不错的内容想要收藏起来,点击收藏发现需要登录网站授权,网站提供几种登录方式,第一种是使用该网站的账号登录,第二种是使用如QQ,微信,微博等应用进行自动授权登录,我们并不希望手动注册该网站账号,所以选择第二种方式登录,此时会跳转到第三方应用授权页面,此时面临两种授权验证模式
  • 传统模式

    • 在传统的客户端 - 服务器身份验证模型中,客户端请求访问受限资源(受保护资源),服务器通过使用资源所有者的服务器进行身份验证。为了提供第三方应用程序访问受限资源权限,资源所有者必须与其共享凭据【摘自RFC 6749】

    • 也就是说,先对于QQ、微信或微博来说,我们浏览的网站属于第三方应用,而授权实际上就是允许对方访问QQ等资源所有者的资源,传统授权模式相当于给予对方我们登录QQ、微信或微博的账号密码,第三方通过该账号密码可以获取到QQ等资源所有者的信息,用以创建自己的账号进行其他操作。

    • 弊端:

      1. 需要第三方应用程序存储资源所有者的凭据以供将来使用,通常是密码明文。
      2. 服务器仍需要支持密码验证,密码中固有的安全漏洞。
      3. 第三方应用程序获得对资源所有者的受保护资源的过度广泛访问权限,使资源所有者无法控制第三方应用程序访问资源所有者有限子集资源的范围和时限。
      4. 资源所有者无法撤销对单个第三方的访问权限而不影响其他第三方,必须更改第三方的密码才可以做到。
      5. 与任何第三方应用的泄露导致对终端用户的密码及该密码所保护的所有数据的泄露。
  • OAuth2模式

    • OAuth通过引入授权层以及从资源所有者角色分离出客户端角色来解决这些问题。在OAuth中,客户端请求对受资源所有者控制且托管在资源服务器上的资源的访问权限,并授予一组不同于资源所有者所拥有的凭据。【摘自RFC 6749】

    • 以上描述可以理解为OAuth2将传统的第三方应用保存的明文密码等可以随意通行的长效凭据 替换成临时可控的凭据,该凭据决定了拥有该凭据的第三方应用所拥有的有限权限以及权限有效时间。如此一来,就解决了上述传统模式的问题。

Spring Security OAuth2的原理

OAuth2的角色

  • 资源所有者(Resource Owner):

    • 能够许可对受保护资源的访问权限的实体。当资源所有者是个人时,它被称为最终用户【上述示例的QQ/微信/微博】
  • 资源服务器(Resource server)

    • 托管受保护资源的服务器,能够接收和响应使用访问令牌对受保护资源的请求 【上述示例的QQ/微信/微博的服务器】
  • 客户端(Client )

    • 使用资源所有者的授权代表资源所有者发起对受保护资源的请求的应用程序 【上述示例的网站】
  • 授权服务器(Authorization server)

    • 在成功验证资源所有者且获得授权后颁发访问令牌给客户端的服务器

OAuth2处理流程

  • (A)客户端向资源所有者请求授权。授权请求可以直接对资源所有者(如图所示)进行,或者通过授权服务器作为中介进行间接访问(首选方案)。

  • (B)资源所有者允许授权,并返回凭证(如code)。

  • (C)客户端通过授权服务器进行身份验证,并提供授权凭证(如code),请求访问令牌(access token)。

  • (D)授权服务器对客户端进行身份验证,并验证授权凭证,如果有效,则发出访问令牌。

  • (E)客户端向资源服务器请求受保护的资源,并通过提供访问令牌来进行身份验证。

  • (F)资源服务器验证访问令牌,如果正确则返回受保护资源。

  • 简单来说,就是客户端向资源所有者申请授权,允许授权后获得授权凭证,该授权凭证可以用来向授权服务器请求访问令牌,获得访问令牌后,可以使用该令牌暂时访问资源服务器的权限内资源,令牌有效时间过后,令牌失效。

OAuth2的授权许可

  • 从上述流程中可以看得出来,想要获取到访问令牌(Access Token),首先得要拿到资源所有者授权许可(Authorization Grant),OAuth2的授权许可类型有四种【可自定义类型】,不同授权许可类型决定了资源所有者允许授权的操作方式:

  • 授权码

    授权码模式是使用最广泛的授权模式,授权码通过使用授权服务器做为客户端与资源所有者的中介而获得。客户端不是直接从资源所有者请求授权,而是引导资源所有者至授权服务器,授权服务器之后引导资源所有者带着授权码回到客户端。所以资源所有者的凭据不与客户端共享,仅共享授权码,安全防暴露

  • 隐式授权(简化授权)

    客户端不再接收授权码,而是直接被颁发一个访问令牌,可以说是简化版的授权类型,可以提高客户端的响应速度,因为减少了获取访问令牌的往返次数。

  • 资源所有者密码凭据

    资源所有者密码凭据(即用户名和密码),可以直接作为获取访问令牌的授权许可。但是资源所有者凭据仅被用于一次请求并被交换为访问令牌,访问令牌失效仅需要刷新令牌即可,客户端并不会保存该凭据,这是与传统模式区分开来的地方。

  • 客户端凭据

    该模式下,授权并不由用户进行访问,而是用户直接与客户端进行注册,客户端 以自身名义向资源所有者进行认证,要求提供资源。

OAuth2的访问令牌

  • 访问令牌是用于访问受保护资源的凭据,访问令牌是一个代表向客户端颁发的授权的字符串。该字符串通常对于客户端是不透明的。
  • 令牌代表了访问权限的由资源所有者许可并由资源服务器和授权服务器实施的具体范围和期限。

OAuth2的刷新令牌

  • 注意,刷新令牌是个名词,而不是动作
  • 刷新令牌是用于获取访问令牌的凭据。
  • 刷新令牌由授权服务器颁发给客户端,用于在当前访问令牌失效或过期时,获取一个新的访问令牌[而不是再次获取授权许可],或者获得相等或更窄范围的额外的访问令牌(访问令牌可能具有比资源所有者所授权的更短的生命周期和更少的权限)。

整合Spring Security OAuth2

项目准备

  • 基于前面文章整合鉴权体系,自然涉及用户及权限问题,所以应当构造完整的用户体系,以下是所用到的数据库表及相关记录:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    -- ----------------------------
    -- Table structure for sys_menu 菜单表
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_menu`;
    CREATE TABLE `sys_menu` (
    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
    `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜单编码',
    `p_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜单父编码',
    `p_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '父菜单ID',
    `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '名称',
    `url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求地址',
    `is_menu` int(11) NULL DEFAULT NULL COMMENT '是否是菜单(1.菜单。2.按钮)',
    `level` int(11) NULL DEFAULT NULL COMMENT '菜单层级',
    `sort` int(11) NULL DEFAULT NULL COMMENT '菜单排序',
    `status` int(11) NULL DEFAULT NULL COMMENT '菜单状态(是否激活)',
    `del_flag` int(255) NULL DEFAULT NULL COMMENT '是否删除',
    `icon` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜单图标',
    `create_by` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建者',
    `update_by` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '最后更新者',
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后更新时间',
    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE INDEX `FK_CODE`(`code`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
    SET FOREIGN_KEY_CHECKS = 1;
    -- ----------------------------
    -- Records of sys_menu
    -- ----------------------------

    -- ----------------------------
    -- Table structure for sys_privilege 角色权限表
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_privilege`;
    CREATE TABLE `sys_privilege` (
    `role_id` int(11) NOT NULL COMMENT '角色id',
    `menu_id` int(11) NOT NULL COMMENT '菜单id',
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (`role_id`, `menu_id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
    SET FOREIGN_KEY_CHECKS = 1;
    -- ----------------------------
    -- Records of sys_privilege
    -- ----------------------------

    -- ----------------------------
    -- Table structure for sys_role 角色表
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_role`;
    CREATE TABLE `sys_role` (
    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
    `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色名',
    `value` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色值',
    `tips` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色描述',
    `create_by` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建者',
    `update_by` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '最后更新人',
    `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后更新时间',
    `status` int(11) NULL DEFAULT NULL COMMENT '状态(是否可用)',
    `del_flag` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '是否删除',
    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE INDEX `unique_role_name`(`name`) USING BTREE,
    UNIQUE INDEX `unique_role_value`(`value`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
    SET FOREIGN_KEY_CHECKS = 1;
    -- ----------------------------
    -- Records of sys_role
    -- ----------------------------
    INSERT INTO `sys_role`(`id`, `name`, `value`, `tips`, `create_by`, `update_by`, `create_time`, `update_time`, `status`, `del_flag`) VALUES (6, '管理员', 'admin', NULL, 'admin', 'admin', '2019-06-13 19:02:52', '2017-06-26 12:46:09', 1, '0');
    INSERT INTO `sys_role`(`id`, `name`, `value`, `tips`, `create_by`, `update_by`, `create_time`, `update_time`, `status`, `del_flag`) VALUES (8, '超级管理员', 'super', NULL, 'admin', 'admin', '2019-06-13 19:02:52', '2019-06-05 10:55:23', 1, '0');
    INSERT INTO `sys_role`(`id`, `name`, `value`, `tips`, `create_by`, `update_by`, `create_time`, `update_time`, `status`, `del_flag`) VALUES (17, '用户', 'user', NULL, 'admin', 'admin', '2019-06-13 19:02:59', '2017-07-21 09:41:28', 1, '0');

    -- ----------------------------
    -- Table structure for sys_user 用户表
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_user`;
    CREATE TABLE `sys_user` (
    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
    `username` varchar(45) NOT NULL COMMENT '用户名',
    `password` varchar(96) NOT NULL COMMENT '密码',
    `truename` varchar(45) DEFAULT NULL COMMENT '真实名称',
    `birthday` TIMESTAMP COMMENT '生日',
    `sex` int(11) DEFAULT NULL COMMENT '性别(0男1女)',
    `email` varchar(45) DEFAULT NULL COMMENT '邮箱',
    `phone` varchar(45) DEFAULT NULL COMMENT '手机号',
    `status` int(11) DEFAULT NULL COMMENT '账号状态(是否激活)',
    `del_flag` int(10) DEFAULT NULL COMMENT '是否删除',
    `create_time` TIMESTAMP COMMENT '创建时间',
    `update_time` TIMESTAMP COMMENT '最后更新时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `unique_user_username` (`username`)
    ) ENGINE=InnoDB AUTO_INCREMENT=52 DEFAULT CHARSET=utf8;
    -- ----------------------------
    -- Records of sys_user 超级管理员及管理员密码皆为123456
    -- ----------------------------
    INSERT INTO `sys_user`(`id`, `username`, `password`, `truename`, `birthday`, `sex`, `email`, `phone`, `status`, `del_flag`, `create_time`, `update_time`) VALUES (46, 'super', '$2a$10$cKRbR9IJktfmKmf/wShyo.5.J8IxO/7YVn8twuWFtvPgruAF8gtKq', '超级管理员', '2019-06-13 18:44:36', 1, NULL, NULL, 1, 0, '2019-06-13 18:44:36', '2019-06-13 18:44:36');
    INSERT INTO `sys_user`(`id`, `username`, `password`, `truename`, `birthday`, `sex`, `email`, `phone`, `status`, `del_flag`, `create_time`, `update_time`) VALUES (48, 'admin', '$2a$10$cKRbR9IJktfmKmf/wShyo.5.J8IxO/7YVn8twuWFtvPgruAF8gtKq', '管理员', '2019-06-13 18:44:36', 1, NULL, NULL, 1, 0, '2019-06-13 18:44:36', '2019-06-13 18:44:36');

    -- ----------------------------
    -- Table structure for sys_user_role 用户角色表
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_user_role`;
    CREATE TABLE `sys_user_role` (
    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
    `user_id` int(11) NULL DEFAULT NULL COMMENT '用户id',
    `role_id` int(11) NULL DEFAULT NULL COMMENT '角色id',
    `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
    PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
    SET FOREIGN_KEY_CHECKS = 1;
    -- ----------------------------
    -- Records of sys_user_role
    -- ----------------------------

    -- ----------------------------
    -- Table structure for oauth_client_details 鉴权客户端信息
    -- ----------------------------
    DROP TABLE IF EXISTS `oauth_client_details`;
    CREATE TABLE `oauth_client_details` (
    `client_id` varchar(48) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '终端id',
    `resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '资源id',
    `client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '终端密码',
    `scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '终端域',
    `authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '认证授权类型',
    `web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '网络重定向uri',
    `authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '认证人',
    `access_token_validity` int(11) NULL DEFAULT NULL COMMENT 'AccessToken有效期',
    `refresh_token_validity` int(11) NULL DEFAULT NULL COMMENT 'RefreshToken有效期',
    `additional_information` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '附加数据',
    `autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    PRIMARY KEY (`client_id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
    SET FOREIGN_KEY_CHECKS = 1;
    -- ----------------------------
    -- Records of oauth_client_details webApp密码是123456
    -- ----------------------------
    INSERT INTO `oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('app', NULL, 'app', 'app', 'password,refresh_token', NULL, NULL, NULL, NULL, NULL, NULL);
    INSERT INTO `oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('webApp', NULL, '$2a$10$cKRbR9IJktfmKmf/wShyo.5.J8IxO/7YVn8twuWFtvPgruAF8gtKq', 'app', 'authorization_code,password,refresh_token,client_credentials', NULL, NULL, NULL, NULL, NULL, NULL);

system-api

  • 对基本的用户,角色以及菜单信息作为一个项目,在springcloud-system模块下创建 system-api模块,该部分使用mybatiplus的自动生成代码工具可以完成;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <modelVersion>4.0.0</modelVersion>
    <artifactId>system-api</artifactId>
    <description>系统非业务模块的基本类</description>

    <properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
    <dependency>
    <groupId>pers.fulsun</groupId>
    <artifactId>common-data</artifactId>
    </dependency>

    <dependency>
    <groupId>pers.fulsun</groupId>
    <artifactId>common-swagger</artifactId>
    </dependency>
    </dependencies>

system-handle

  • 对用户,角色及菜单的操作作为一个项目,system-handle:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    <modelVersion>4.0.0</modelVersion>
    <artifactId>system-handle</artifactId>
    <description>操作api的模块</description>

    <properties>
    <maven.compiler.target>11</maven.compiler.target>
    <maven.compiler.source>11</maven.compiler.source>
    </properties>

    <dependencies>
    <dependency>
    <groupId>pers.fulsun</groupId>
    <artifactId>system-api</artifactId>
    <version>1.0-SNAPSHOT</version>
    </dependency>
    <!--eureka 客户端-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    <!--实时健康监控-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <!--配置客户端-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-client</artifactId>
    </dependency>

    <!--作为web项目存在-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    </dependencies>
    <!-- 添加spring-boot的maven插件,不能少,打jar包时得用 -->
    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>

创建鉴权模块项目

  • OAuth2的整合主要分为三步:
    • 认证服务器配置(OAuth2Authorization Server Config)
    • 资源服务器配置(Resource Server Config)
    • 安全配置(Security Config)

common-security

  • 首先我们创建一个基础模块 common-security

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <dependencies>
    <!--UPMS API-->
    <dependency>
    <groupId>pers.fulsun</groupId>
    <artifactId>system-api</artifactId>
    </dependency>

    <!--Security依赖-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
    </dependency>

    <!--OAuth2依赖-->
    <dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    </dependency>

    </dependencies>

配置sercurity认证

  • 加配置 ,使用jdbc存储用户信息及终端信息,redis数据库存储token的模式

  • UserDetailsService是OAuth2用于获取用户信息的操作类,此处我们重写该类,从数据库获取用户信息,并将用户信息存入redis缓存:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    package pers.fulsun.demo.springcloud.service;

    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.core.util.ObjectUtil;
    import cn.hutool.json.JSONUtil;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import pers.fulsun.demo.springcloud.entity.SysMenu;
    import pers.fulsun.demo.springcloud.entity.SysRole;
    import pers.fulsun.demo.springcloud.entity.SysUser;
    import pers.fulsun.demo.springcloud.helper.RedisHelper;

    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;

    /**
    * 用户信息服务,实现 Spring Security的UserDetailsService接口方法,用于身份认证
    */
    @Component
    public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private RedisHelper redisHelper;
    @Autowired
    private SysUserService userService;
    @Autowired
    private SysRoleService roleService;
    @Autowired
    private SysPrivilegeService permissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
    boolean enabled = true; // 可用性 :true:可用 false:不可用
    boolean accountNonExpired = true; // 过期性 :true:没过期 false:过期
    boolean credentialsNonExpired = true; // 有效性 :true:凭证有效 false:凭证无效
    boolean accountNonLocked = true; // 锁定性 :true:未锁定 false:已锁定
    //判断缓存中是否存在以该用户名为key的用户信息,如果有,则直接封装返回
    if (StringUtils.isNotBlank(redisHelper.get(username))) {
    SysUser sysUser = JSONUtil.toBean(redisHelper.get(username), SysUser.class);
    return new User(sysUser.getUsername(), sysUser.getPassword(),
    enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, grantedAuthorities);
    }
    //从数据库中查询获取该账户信息
    SysUser sysUser = this.userService.findByUsername(username);

    if (ObjectUtil.isNull(sysUser)) {
    throw new UsernameNotFoundException("用户:" + username + ",不存在!");
    }
    //获取用户角色列表
    List<SysRole> roles = this.roleService.getRoleByUserId(sysUser.getId());
    if (!CollUtil.isEmpty(roles)) {
    for (SysRole role : roles) {
    //角色必须是ROLE_开头,可以在数据库中设置
    GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + role.getValue());
    grantedAuthorities.add(grantedAuthority);
    //获取用户权限列表
    List<SysMenu> permissions = this.permissionService.getPermissionsByRoleId(role.getId());
    if (!CollUtil.isEmpty(permissions)) {
    for (SysMenu menu : permissions) {
    GrantedAuthority authority = new SimpleGrantedAuthority(menu.getCode());
    grantedAuthorities.add(authority);
    }
    }
    }
    }
    //将当前用户存入redis缓存
    // userHelper.addUser(sysUser);
    redisHelper.set("user", JSONUtil.toJsonStr(sysUser));

    //封装返回
    return new User(sysUser.getUsername(), sysUser.getPassword(),
    enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, grantedAuthorities);
    }
    }
  • TokenEnhancer 意为token增强,能够在token原有基础上附加信息,使得我们能够通过token拿到更多的信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package pers.fulsun.demo.springcloud.component;

    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
    import org.springframework.security.oauth2.common.OAuth2AccessToken;
    import org.springframework.security.oauth2.provider.OAuth2Authentication;
    import org.springframework.security.oauth2.provider.token.TokenEnhancer;

    import java.util.HashMap;
    import java.util.Map;

    /**
    * 令牌增强器,通过token可以获取到更多信息[暂时用不到]
    */
    public class CustomTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
    User user = (User) authentication.getPrincipal();
    final Map<String, Object> additionalInfo = new HashMap<>();
    // 注意添加的额外信息,最好不要和已有的json对象中的key重名,容易出现错误
    additionalInfo.put("username", user.getUsername());
    ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
    return accessToken;
    }
    }

认证授权服务器

  • 创建 springcloud-oauth 项目模块作为认证授权服务器:

    • 引入common-security模块依赖,Pom文件如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      <description>认证服务器</description>
      <dependencies>
      <!--Security基础模块依赖-->
      <dependency>
      <groupId>com.springcloud</groupId>
      <artifactId>common-security</artifactId>
      </dependency>
      <!--eureka 客户端-->
      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
      </dependency>
      <!--配置客户端-->
      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-config-client</artifactId>
      </dependency>
      </dependencies>

      <!-- 添加spring-boot的maven插件,不能少,打jar包时得用 -->
      <build>
      <plugins>
      <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      </plugins>
      </build>

认证授权配置

  • 真正的认证授权服务器配置:继承AuthorizationServerConfigurerAdapter ,注解使用@EnableAuthorizationServer开启认证授权服务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    package pers.fulsun.demo.springcloud.config;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Configurable;
    import org.springframework.context.annotation.Bean;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
    import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
    import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
    import org.springframework.security.oauth2.provider.ClientDetailsService;
    import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
    import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
    import org.springframework.security.oauth2.provider.token.TokenStore;
    import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
    import pers.fulsun.demo.springcloud.component.CustomTokenEnhancer;
    import pers.fulsun.demo.springcloud.constances.SecurityConst;
    import pers.fulsun.demo.springcloud.service.UserDetailServiceImpl;

    import javax.sql.DataSource;

    /**
    * 授权服务器配置
    *
    * @author fulsun
    * @date 2021-08-26
    */
    @Configurable
    @EnableAuthorizationServer
    public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager; // 认证管理器
    @Autowired
    private DataSource dataSource; //数据库连接
    @Autowired
    private UserDetailServiceImpl userDetailsService; //用户信息处理器
    @Autowired
    private RedisConnectionFactory redisConnectionFactory; // redis连接工厂

    /**
    * 声明TokenStore实现 说明令牌存储对象
    * 有InMemoryTokenStore(内存),JdbcTokenStore(数据库),RedisTokenStore(redis缓存),JwtTokenStore(jwt)四个实现
    * redis因其过期时间特性,尤为适合用于存储令牌
    *
    * @return
    */
    @Bean(name = "tokenStore")
    TokenStore tokenStore() {
    RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
    return redisTokenStore;
    }

    /**
    * 设置客户端细节信息处理器
    * 配置客户端详情服务(ClientDetailsService)
    * 客户端详情信息在这里进行初始化
    * 通过数据库来存储调取详情信息
    *
    * @param clients
    * @throws Exception
    */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.withClientDetails(clientDetails());
    }

    /**
    * 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
    *
    * @param endpoints 终端对象
    * @throws Exception
    */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    endpoints.tokenStore(tokenStore()) //设置令牌存储对象
    .userDetailsService(userDetailsService) //设置用户信息处理器
    .authenticationManager(authenticationManager) //设置认证处理器
    .tokenEnhancer(new CustomTokenEnhancer()) //token增强
    .reuseRefreshTokens(true); //该字段设置设置refresh token是否重复使用,即通过刷新token,是否保留原有refresh而不会重新获取,true:reuse;false:no reuse.
    // 为解决获取token并发问题
    DefaultTokenServices tokenServices = new DefaultTokenServices();
    tokenServices.setTokenStore(tokenStore()); //设置令牌存储对象
    tokenServices.setTokenEnhancer(new CustomTokenEnhancer()); //设置令牌增强
    tokenServices.setAuthenticationManager(authenticationManager); // 设置令牌处理器
    tokenServices.setSupportRefreshToken(true); //支持刷新token
    tokenServices.setClientDetailsService(clientDetails()); //设置终端管理
    tokenServices.setAccessTokenValiditySeconds(SecurityConst.ACCESS_TOKEN_VALIDITY_SECONDS); // token有效期自定义设置,默认12小时
    tokenServices.setRefreshTokenValiditySeconds(SecurityConst.REFRESH_TOKEN_VALIDITY_SECONDS);//默认30天,这里修改
    endpoints.tokenServices(tokenServices);
    }

    /**
    * 配置令牌端点(Token Endpoint)的安全约束.
    *
    * @param security security
    * @throws Exception
    */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    security.tokenKeyAccess("permitAll()");
    security.checkTokenAccess("isAuthenticated()");
    security.allowFormAuthenticationForClients(); //允许表单认证提交
    }

    /**
    * 声明 ClientDetails实现
    * 有JdbcClientDetailsService【客户端信息存在于数据库】及InMemoryClientDetailsService[客户端信息存在于内存]两个实现
    * 此处可自定义实现redis,暂定使用jdbc方式
    *
    * @return
    */
    @Bean
    public ClientDetailsService clientDetails() {
    JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
    return jdbcClientDetailsService;
    }
    }

安全服务

  • 配置安全服务:继承WebSecurityConfigurerAdapter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    package pers.fulsun.demo.springcloud.config;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
    import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
    import pers.fulsun.demo.springcloud.service.UserDetailServiceImpl;

    /**
    * @author fulsun
    * @description 安全配置
    * @EnableWebSecurity 启用web安全配置
    * @EnableGlobalMethodSecurity 启用全局方法安全注解,就可以在方法上使用注解来对请求进行过滤
    * @date 2021-08-26
    */

    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
    * 注入用户信息服务
    *
    * @return 用户信息服务对象
    */
    @Autowired
    private UserDetailServiceImpl userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
    }

    /**
    * 认证管理
    *
    * @return 认证管理对象
    * @throws Exception 认证异常信息
    */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
    }

    /**
    * 全局用户信息及密码
    *
    * @param auth 认证管理
    * @throws Exception 用户认证异常信息
    */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //设置PreAuthenticatedAuthenticationProvider,否则刷新token时将导致No AuthenticationProvider found for org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
    PreAuthenticatedAuthenticationProvider preAuthenticatedAuthenticationProvider = new PreAuthenticatedAuthenticationProvider();
    //设置userDetailService,否则刷新token时将报NullPointException异常
    preAuthenticatedAuthenticationProvider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<>(userDetailsService));
    auth.authenticationProvider(preAuthenticatedAuthenticationProvider)
    .userDetailsService(userDetailsService) //设置用户信息操作Service
    .passwordEncoder(passwordEncoder()); //设置用户密码解析器
    }

    /**
    * http安全配置
    *
    * @param http http安全对象
    * @throws Exception http安全异常信息
    */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http
    .authorizeRequests()
    .anyRequest().fullyAuthenticated()
    .antMatchers("/oauth/token").permitAll() //不需要令牌,直接访问资源
    .and()
    .csrf().disable();
    http
    // 头部缓存
    .headers()
    .cacheControl()
    .and()
    // 防止网站被人嵌套
    .frameOptions()
    .sameOrigin()
    .and()
    .csrf()
    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
    .and()
    // 跨域支持
    .cors();
    http
    .requestMatchers()
    //接受的请求
    .antMatchers("/login", "/logout", "/oauth/authorize", "/oauth/confirm_access")
    .and()
    .authorizeRequests()// 端点排除
    .anyRequest().authenticated()
    .and()
    .formLogin()
    .loginPage("/login")
    .failureUrl("/login?error")
    .permitAll()
    .and()
    .logout()
    .logoutUrl("/logout")
    .invalidateHttpSession(true).clearAuthentication(true);
    }

    /**
    * 不拦截资源请求
    *
    * @param web
    * @throws Exception
    */
    @Override
    public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/css/**", "/js/**", "/plugins/**", "/favicon.ico");
    }
    }

配置资源服务器

  • 由于资源服务器是所有可能被访问的服务,故而所有可能被访问的服务都需要配置,所以我们在common-security模块进行配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    package pers.fulsun.demo.springboot.config;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
    import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

    /**
    * 使用 @EnableResourceServer注解启用资源服务,继承ResourceServerConfigurerAdapter
    *
    * @author fulsun
    * @description 资源服务配置
    * @date 2021-08-26
    */
    @Configuration
    @EnableResourceServer
    public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Autowired
    private RedisConnectionFactory redisConnectionFactory; // redis连接工厂

    /**
    * 与http安全配置
    *
    * @param http
    * @throws Exception
    */
    @Override
    public void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
    .authorizeRequests()
    .antMatchers("/swagger-ui.html", "/swagger-resources/**",
    "/v2/api-docs/**", "/validatorUrl", "/valid")
    .permitAll() //匹配不需要资源认证路径
    .anyRequest().authenticated()
    .and()
    .httpBasic();
    }

    /**
    * 资源服务配置
    *
    * @param resources
    * @throws Exception
    */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    super.configure(resources);
    //此处如果不进行设置,资源服务器的鉴权认证将会默认从InMemoryTokenStore取数据验证
    resources.tokenStore(new RedisTokenStore(redisConnectionFactory));
    }
    }

授权服务器请求网关

  • 在sprincloud-gateway工程配置文件添加oauth模块的请求网关

    1
    2
    3
    4
    5
    routes:
    - id: springcloud-oauth
    uri: lb://springcloud-oauth
    predicates:
    - Path=/oauth/**

测试

  • 获取token

  • 通过网关使用webApp终端请求认证授权:http://localhost:9999/oauth/token

  • 刷新token方式

  • 使用token访问资源服务器资源

    • 使用上述获取的token,我们可以访问system-handler模块资源,此时的system-handler模块相当于资源服务器,首先在system-handler引入common-security模块:

      1
      2
      3
      4
      5
      <!--oauth2的基础模块-->
      <dependency>
      <groupId>pers.fulsun</groupId>
      <artifactId>common-security</artifactId>
      </dependency>
    • 在该模块的启动类SpringCloudSystemApplication加上:@EnableResourceServer注解,开启资源服务

      1
      2
      3
      4
      5
      6
      7
      8
      @EnableEurekaClient
      @SpringBootApplication
      @EnableResourceServer
      public class SpringCloudSystemApplication {
      public static void main(String[] args) {
      SpringApplication.run(SpringCloudSystemApplication.class, args);
      }
      }
    • 同时在SysUserController中编写接口

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      @Slf4j
      @RestController
      @RequestMapping("/user")
      public class SysUserController {
      @RequestMapping("/hello")
      public String hello() {
      return "world";
      }

      }
    • common-security 中放行 /user/hello

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @Override
      public void configure(HttpSecurity http) throws Exception {
      http.csrf().disable()
      .authorizeRequests()
      //匹配不需要资源认证路径
      .antMatchers("/user/hello")
      .permitAll()
      .anyRequest().authenticated()
      .and()
      .httpBasic();
      }
    • 此时不携带token进行访问,结果报错如下:

    • 携带token访问资源

问题总结

  • 启动认证服务器报错:

    1
    Handling error: NestedServletException, Handler dispatch failed; nested exception is java.lang.NoSuchMethodError: org.springframework.data.redis.connection.RedisConnection.set([B[B)V
    • 是OAuth2版本与redis不兼容问题,升级OAuth2至2.3.3版本可解决
  • 刷新token报:

    1
    No AuthenticationProvider found for org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
    • 无法找到相应的refresh_token所用Provider容器,在SecurityConfig中配置添加,并统一设置UserDetailService,否则报NullPointException

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      /**
      * 全局用户信息及密码
      * @param auth 认证管理
      * @throws Exception 用户认证异常信息
      */
      @Override
      protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      //设置PreAuthenticatedAuthenticationProvider,否则刷新token时将导致"No AuthenticationProvider found for org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
      PreAuthenticatedAuthenticationProvider preAuthenticatedAuthenticationProvider = new PreAuthenticatedAuthenticationProvider();
      //设置userDetailService,否则刷新token时将报NullPointException异常
      preAuthenticatedAuthenticationProvider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<>(userDetailsService));
      auth.authenticationProvider(preAuthenticatedAuthenticationProvider)
      .userDetailsService(userDetailsService) //设置用户信息操作Service
      .passwordEncoder(passwordEncoder()); //设置用户密码解析器
      }
  • 如何保证token与缓存的周期一致?

    • 即保证access_token失效的同时,缓存的所有信息也一并失效。OAuth2的token使用redis缓存,并且设置了有效期,我们可以通过设置存入信息的时效性进行同步

    • 如:token创建之时同步缓存用户信息并设置时长SecurityConst.ACCESS_TOKEN_VALIDITY_SECONDS

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      **
      * 添加用户
      * @param sysUser
      * @return
      */
      public boolean addUser(SysUser sysUser){
      if(log.isDebugEnabled()){
      log.debug("新增用户信息:{}",sysUser);
      }
      this.redisHelper.hset(SecurityConst.USER_DETAIL, sysUser.getUsername(),JSONUtil.toJsonStr(sysUser),SecurityConst.ACCESS_TOKEN_VALIDITY_SECONDS);
      return true;
      }
  • 如何在Gateway网关做统一身份认证?

    • 思路:通过全局过滤器,提取请求中的token值进行校验,由于oauth将token存放于redis,所以我们可以直接从redis中查看是否存在该token,即可实现身份认证

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      /**
      * @description 全局请求过滤器,核验身份授权
      * @date: 2019-04-25 18:07
      */
      @Slf4j
      @Component
      public class GlobalTokenFilter implements GlobalFilter, Ordered {
      // url匹配器
      private AntPathMatcher pathMatcher = new AntPathMatcher();
      @Autowired
      private StringRedisTemplate redisTemplate;
      @Override
      public int getOrder() {
      // TODO Auto-generated method stub
      return -500;
      }
      @Override
      public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
      //取token
      String accessToken = extractToken(exchange.getRequest());
      //取路径
      String path = exchange.getRequest().getPath().value();
      //路径放行
      if(pathMatcher.match("/oauth/**",path)){
      return chain.filter(exchange);
      }
      //拦截非/oauth/的路径
      if(!pathMatcher.match("/oauth/**",path)){
      //如果token为空,直接返回未认证
      if (accessToken == null) {
      return unauthorizedResult(exchange,"尚未登录");
      }else{
      try {
      //从缓存取出token核验,如果存在相应token,则放行
      String params = redisTemplate.opsForValue().get("auth:" +accessToken) ;
      //如果不存在该token值,则直接返回未认证
      if(StrUtil.isBlank(params)){
      throw new Exception("无鉴权信息,请先登陆");
      }
      } catch (Exception e) {
      return unauthorizedResult(exchange, e.getMessage());
      }
      }
      }
      return chain.filter(exchange);
      }
      /**
      * 从request请求中提取token
      * @param request
      * @return
      */
      protected String extractToken(ServerHttpRequest request) {
      List<String> strings = request.getHeaders().get("Authorization");
      String authToken = null;
      if (strings != null) {
      authToken = strings.get(0).substring("Bearer".length()).trim();
      }
      if (StrUtil.isBlank(authToken)) {
      strings = request.getQueryParams().get("access_token");
      if (strings != null) {
      authToken = strings.get(0);
      }
      }
      return authToken;
      }
      public Mono<Void> unauthorizedResult(ServerWebExchange exchange,String message){
      ServerHttpResponse response = exchange.getResponse();
      R rsp = new R();
      rsp.setCode(HttpStatus.UNAUTHORIZED.value());
      rsp.setMsg(message);
      rsp.setData(HttpStatus.UNAUTHORIZED.getReasonPhrase());
      byte[] bits = JSONUtil.toJsonStr(rsp).getBytes(StandardCharsets.UTF_8);
      DataBuffer buffer = response.bufferFactory().wrap(bits);
      /**
      * 设置响应状态为401
      */
      response.setStatusCode(HttpStatus.UNAUTHORIZED);
      //指定编码,否则在浏览器中会中文乱码
      response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
      return exchange.getResponse().writeWith(Flux.just(buffer));
      }
      }