SpringBoot2

springboot在版本3以上最低支持jdk17,所以jdk8只能用springboot2.x.x

注意: 导入他人的项目,如果依赖成功加载,却仍显示找不到,可以在 文件—清除缓存,并重启。

概念

SpringBoot 对 spring 的简化,也是其核心功能和优点:

  • 起步依赖
  • 自动配置
  • 辅助功能(内置服务器….)

具体体现在:

  • parent
  • starter
  • 引导类
  • 内嵌tomcat

parent

由于依赖的版本之间有一定的固定搭配,所以 SpringBoot 制作了多个技术版本搭配的列表,就叫做 parent。所以 ,使用parent可以帮助开发者进行版本的统一管理。

在pom.xml中继承了一个坐标:

1
2
3
4
5
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
</parent>

打开后,可以看到主要有两组信息:

  • 第一组是 各式各样的依赖版本号属性;

  • 第二组是 各式各样的的依赖坐标信息,其中依赖坐标定义是应用了第一组信息中定义的依赖版本属性值; 且是在<dependencyManagement> 标签中,所以这些定义的依赖还未导入,SpringBoot会在starter中导入一些固定的依赖组合。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!-- 第一组信息 版本属性值 -->
    <properties>
    <activemq.version>5.16.3</activemq.version>
    <aspectj.version>1.9.7</aspectj.version>
    .....
    .....
    </properties>
    <!-- 第二组信息 依赖坐标 -->
    <dependencyManagement>
    <dependencies>
    <dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>${hibernate.version}</version>
    </dependency>
    ......
    ......
    </dependencies>
    </dependencyManagement>

starter

依赖坐标的使用往往有一些固定的组合方式,SpringBoot 定义了starter: starter是指使用某种技术时 依赖的固定搭配格式,也是一种最佳解决方案。 使用starter可以帮助开发者减少依赖配置。

​ 所以,实际开发中如果需要用什么技术,先去找有没有这个技术对应的starter:

  • 如果有对应的starter,直接写starter,而且无需指定版本,版本由parent提供;
  • 如果没有对应的starter,再手写坐标;

SpringBoot 对于 starter 的名称都是如下格式:

1
命名规则:spring-boot-starter-技术名称

所有的starter中都会依赖一个starter,叫做spring-boot-starter,它是所有的SpringBoot的starter的基础依赖,里面定义了SpringBoot相关的基础配置。

小结:

  1. 开发SpringBoot程序需要导入坐标时通常导入对应的starter
  2. 每个不同的starter根据功能不同,通常包含多个依赖坐标
  3. 使用starter可以实现快速配置的效果,达到简化配置的目的

引导类

程序运行的入口是 SpringBoot工程创建时自带的有main方法的那个类,运行这个类就可以启动SpringBoot工程的运行。

1
2
3
4
5
6
@SpringBootApplication
public class xxxApplication {
public static void main(String[] args) {
SpringApplication.run(xxxApplication.class, args);
}
}

Spring程序运行的基础是需要创建自己的Spring容器对象(IoC容器)并将所有的对象交给Spring的容器管理,也就是一个一个的Bean。而SpringBoot本身是为了加速Spring程序的开发的,所以创建了这个 引导类,它是所有功能的入口。

内嵌tomcat

在导入的依赖中:

  • spring-boot-starter-web
    • spring-boot-starter-tomcat
      • tomcat-embed-core(tomcat内嵌核心)

就是 tomcat-embed-core 把tomcat功能引入到程序中。

内嵌Tomcat运行原理:Tomcat服务器是一款使用java语言开发的软件,那么tomcat的运行肯定也是以对象的形式运行的,即tomcat可以交给Spring容器管理。

更换内嵌的tomcat:

  • tomcat(默认):apache出品,粉丝多,应用面广,负载了若干较重的组件
  • jetty:更轻量级,负载性能远不及tomcat
  • undertow:负载性能勉强跑赢tomcat

只有加入对应的依赖坐标,并排除tomcat坐标即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
</dependencies>

配置文件

SpringBoot将配置信息集中在一个文件 application.properties 中写,不管是服务器的配置,还是数据库的配置,都是写在一起的。

配置的书写有格式和规范,具体的可以参考官方文档(的附录Application Properties):https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties

导入的spring-boot-starter-web依赖提供了 配置书写 的提示。

支持的配置文件格式:(一般使用 yml格式)

  • properties格式
  • yml格式
  • yaml格式

yaml只是后缀名和yml不一样,其实是同一种格式,虽然 .yaml 才是YAML官方认定的文件拓展名,但实际上,未认定的 .yml 用的人用多得多。

如果三个文件共存的话,不同的配置都会生效,而相同、冲突的配置类型的 优先级从高到低:.properties > .yml > .yaml

YAML

YAML(YAML Ain’t Markup Language),一种数据序列化格式。

  • 具有容易阅读、容易与脚本语言交互、以数据为核心,重数据轻格式的特点。

  • 具有严格的语法格式要求:

    1. 大小写敏感
    2. 属性层级关系使用多行描述,每行结尾使用冒号”:” 结束
    3. 使用缩进表示层级关系,同层级左侧对齐,只允许使用空格(不允许使用Tab键)
    4. 属性值前面添加空格(属性名与属性值之间使用冒号+空格作为分隔)
    5. #号 表示注释
    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
    # 常见的数据书写格式

    boolean: TRUE #TRUE,true,True,FALSE,false,False均可
    float: 3.14 #6.8523015e+5 #支持科学计数法
    int: 123 #0b1010_0111_0100_1010_1110 #支持二进制、八进制、十六进制
    null: ~ #使用~表示null
    string: HelloWorld #字符串可以直接书写
    string2: "Hello World" #可以使用双引号包裹特殊字符
    date: 2018-02-17 #日期必须使用yyyy-MM-dd格式
    datetime: 2018-02-17T15:02:31+08:00 #时间和日期之间使用T连接,最后使用+代表时区

    # 此外,还可以表示数组,用减号作为数据开始符号
    subject:
    - Java
    - 前端
    - 大数据
    enterprise:
    name: itcast
    age: 16
    subject:
    - Java
    - 前端
    - 大数据
    likes: [java,前端,大数据] #数组书写缩略格式
    #对象数组格式一
    users:
    - name: Tom
    age: 4
    - name: Jerry
    age: 5
    #对象数组格式二
    users:
    -
    name: Tom
    age: 4
    -
    name: Jerry
    age: 5
    users2: [ { name:Tom , age:4 } , { name:Jerry , age:5 } ] #对象数组缩略格式

注:

yaml文件对于数字的定义支持多种进制格式,如果是使用字符串,要用引号明确标注。

如: password: 0127 ,这里开头是0,又没有引号,所以会被识别为八进制,转为十进制就是87。

读取数据

读取单个数据:

yaml中保存的单个数据,可以使用Spring中的注解直接读取,使用**@Value** 可以读取单个数据,属性名引用方式:${一级属性名.二级属性名……}

读取全部数据:

SpringBoot提供了一个对象叫 Environment,使用自动装配注解可以将所有的yaml数据封装到这个对象中,然后使用 具体方法getProperties(String),参数填写属性名即可。

1
2
3
4
@Autowired
private Environment env;

env.getProperty("xxx")

读取对象数据:

由于单一数据读取太麻烦,Environment又封装的太厉害。所以一般都是将一组数据封装成一个对象。

步骤:

  1. 定义一个对象,并将该对象纳入Spring管控的范围,也就是定义成一个bean;
  2. 然后使用注解@ConfigurationProperties指定该对象加载哪一组yaml中配置的信息。(需要告知加载的数据前缀)
1
2
3
4
5
6
7
8
@Component
@ConfigurationProperties(prefix = "book")
public class Book {
private Integer id;
private String type;
private String name;
private String description;
}
1
2
3
4
5
book:
id: 12
type: abcd
name: zzc
description: hhhhh

数据的引用:

对于重复出现的数据,可以使用 ${} 进行引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
center:
dataDir: D:/usr/local/fire/data
tmpDir: D:/usr/local/fire/tmp
logDir: D:/usr/local/fire/log
msgDir: D:/usr/local/fire/msgDir

# 使用引用减少重复书写
baseDir: D:/usr/local/fire
center:
dataDir: ${baseDir}/data
tmpDir: ${baseDir}/tmp
logDir: ${baseDir}/log
msgDir: ${baseDir}/msgDir

注:如果需要解析转义字符,需要用双引号包裹起来作为字符 才能解析。

更多数据配置

@ConfigurationProperties

@ConfigurationProperties除了给自定义的bean加载属性值,也可以为第三方bean加载属性,不过格式会特殊些。

  1. 在yml中定义要绑定的属性,注意datasource此时全小写
1
2
datasource:
driverClassName: com.mysql.jdbc.Driver
  1. 使用@Bean注解定义第三方bean,使用@ConfigurationProperties注解为第三方bean进行属性绑定,注意前缀是全小写的datasource

    1
    2
    3
    4
    5
    6
    @Bean
    @ConfigurationProperties(prefix = "datasource")
    public DruidDataSource datasource(){
    DruidDataSource ds = new DruidDataSource();
    return ds;
    }

问题:@ConfigurationProperties注解可以为bean进行属性绑定, 所以它可以写在 类和方法上,找起来比较麻烦。

所以有一个注解:@EnableConfigurationProperties,可以标注使用@ConfigurationProperties注解绑定属性的bean是哪些。使用如下:

  1. 在配置类上开启@EnableConfigurationProperties注解,并标注要使用@ConfigurationProperties注解绑定属性的类

    1
    2
    3
    4
    @SpringBootApplication
    @EnableConfigurationProperties(ServerConfig.class)
    public class Springboot13ConfigurationApplication {
    }
  2. 在对应的类上直接使用@ConfigurationProperties进行属性绑定

    1
    2
    3
    4
    5
    6
    7
    @Data
    @ConfigurationProperties(prefix = "servers")
    public class ServerConfig {
    private String ipAddress;
    private int port;
    private long timeout;
    }

    注:这里不需要@Component 来标注为bean, 因为使用@EnableConfigurationProperties注解时,spring会默认将其标注的类定义为bean,因此无需再次声明@Component注解了。 (一起用会标注重复的bean,会报错)

  3. 在yml中定义要绑定的属性:

    1
    2
    3
    4
    servers:
    ip-address: 192.168.0.1
    port: 2345
    timeout: 3000

如果使用@ConfigurationProperties注解时,出现一个标红的坐标提示信息,可以添加一个坐标以解决:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

宽松绑定

进行数据绑定时, yml文件和@ConfigurationProperties(prefix = “xx”)的 前缀必须是小写 的,否则会出错。

而前缀所管辖的数据则非常宽松,也就是,配置文件中的命名格式与变量名的命名格式可以进行格式上的最大化兼容。只要字母对的上,不论大小写不一致还是加了 “_” 或 “-“ 都能正确加载。

springboot推荐使用烤肉串模式/中划线模式,如 ip-address ,data-source。

注:宽松绑定仅针对springboot中@ConfigurationProperties注解进行属性绑定时有效,对@Value是无效的

常用计量单位绑定

对于yml文件中的需要规定单位的配置值,如时间,存储大小。springboot利用了JDK8中提供的用来表示计量单位的数据类型 来绑定计量单位。

  • Duration:表示时间间隔,可以通过 @DurationUnit 注解描述时间单位,例如上例中描述的单位为小时(ChronoUnit.HOURS)

    常用单位:

    DAYS,SECONDS,MINUTES,HALF_DAYS,CENTURIES,

    DECADES,ERAS,FOREVER,HOURS,MICROS,MILLENNIA,

    MILLIS,MONTHS,NANOS,WEEKS,YEARS

  • DataSize:表示存储空间,可以通过 @DataSizeUnit 注解描述存储空间单位,例如上例中描述的单位为MB(DataUnit.MEGABYTES)

    常用单位:MEGABYTES,BYTES,GIGABYTES,KILOBYTES,TERABYTES

1
2
3
4
5
6
7
8
9
10
@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
@DurationUnit(ChronoUnit.HOURS)
private Duration serverTimeOut;

@DataSizeUnit(DataUnit.MEGABYTES)
private DataSize dataSize;
}

校验

对于配置中的数据和代码中的定义的数据,可能存在数据类型不匹配的问题,这样会无法正常绑定。所以需要校验功能。

在JAVAEE的JSR303规范中给出了具体的数据校验标准,开发者可以根据自己的需要选择对应的校验框架。
此处使用Hibernate提供的校验框架来作为实现进行数据校验。

  1. 开启校验框架

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!--1.导入JSR303规范-->
    <dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    </dependency>
    <!--使用hibernate框架提供的校验器做实现-->
    <dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    </dependency>
  2. 在需要开启校验功能的类上使用注解@Validated开启校验功能

    1
    2
    3
    4
    5
    6
    @Component
    @Data
    @ConfigurationProperties(prefix = "servers")
    @Validated //开启对当前bean的属性注入校验
    public class ServerConfig {
    }
  3. 对具体的字段设置校验规则

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Component
    @Data
    @ConfigurationProperties(prefix = "servers")
    @Validated //开启对当前bean的属性注入校验
    public class ServerConfig {
    //设置具体的规则
    @Max(value = 8888,message = "最大值不能超过8888")
    @Min(value = 202,message = "最小值不能低于202")
    private int port;
    }

    通过设置数据格式校验,就可以有效避免非法数据加载。

DTO部分

DTO,即数据传输对象,用于表现层和应用层之间的数据交互。

简单来说Model面向业务,我们是通过业务来定义Model的。而DTO是面向界面UI,是通过UI的需求来定义的。 通过DTO我们实现了表现层与Model之间的解耦。

例子:

  • 表现层一致性处理: Result 结果类

    一般后端传给前端的数据,都应该封装成一个同一的结果类,所以需要定义一个dto,统一格式,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Result {
    private Boolean success; //业务是否成功
    private String errorMsg; //出错时要传递的消息
    private Object data; //业务数据
    private Long total; //

    public static Result ok(){
    return new Result(true, null, null, null);
    }
    public static Result ok(Object data){
    return new Result(true, null, data, null);
    }
    public static Result ok(List<?> data, Long total){
    return new Result(true, null, data, total);
    }
    public static Result fail(String errorMsg){
    return new Result(false, errorMsg, null, null);
    }
    }
  • 实体类与要传给前端时,有些变量不一致时,也要定义一个dto。

    如:实体类User中有密码等隐私字段,而传出去的数据不能包含这些信息,所以可以定义一个UserDTO,只定义需要的变量即可。

    1
    2
    3
    4
    5
    6
    @Data
    public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
    }
    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
    @Data
    @EqualsAndHashCode(callSuper = false)
    @Accessors(chain = true)
    @TableName("tb_user")
    public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
    * 主键
    */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
    * 手机号码
    */
    private String phone;

    /**
    * 密码,加密存储
    */
    private String password;

    /**
    * 昵称,默认是随机字符
    */
    private String nickName;

    /**
    * 用户头像
    */
    private String icon = "";

    /**
    * 创建时间
    */
    private LocalDateTime createTime;

    /**
    * 更新时间
    */
    private LocalDateTime updateTime;
    }

SpringBoot整合

SpringBoot整合其他技术,都是先导入对应的starter或坐标,再去使用对应技术。

整合JUnit

JUnit的依赖默认导入了。

使用注解@SpringBootTest替换了原来的两个注解。如果要手动加载引导类,就写上属性 classes = 引导类.class

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
class Tests {
//注入你要测试的对象
@Autowired
private BookDao bookDao;

@Test
void contextLoads() {
//执行要测试的对象对应的方法
bookDao.save();
}
}

整合Mybatis

原来的Spring整合Mybatis的步骤需要有:

  • 导入坐标
  • Spring核心配置:SpringConfig
  • MyBatis要交给Spring接管的bean:MyBatisConfig
  • 数据源对应的bean
  • 数据库连接信息:jdbc.properties

而使用 SpringBoot 整合 Mybatis,只需要:

  • 创建模块时勾选对应技术:Mybatis和Mysql

    或者手工导入:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <dependencies>
    <!--1.导入对应的starter-->
    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
    </dependency>

    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
    </dependency>
    <!-- 分页插件 -->
    <dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.3.0</version>
    </dependency>
    </dependencies>
  • 配置数据源信息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # 配置相关信息
    spring:
    datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia
    username: root
    password: zzc
    # mybatis
    mybatis:
    typeAliasesPackage: com.zzc.library.po
    mapperLocations: classpath:mapper/*.xml
    configuration:
    map-underscore-to-camel-case: true # 驼峰映射
    # 分页插件
    pagehelper:
    helper-dialect: mysql
    reasonable: true
    support-methods-arguments: true

到此,就配置完毕了。之后只需书写 实体类和映射接口(Dao)即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//实体类
public class Book {
private Integer id;
private String type;
private String name;
private String description;
}
//映射接口
@Mapper
public interface BookDao {
@Select("select * from tbl_book where id = #{id}")
public Book getById(Integer id);
}
//测试类
@SpringBootTest
class Springboot05MybatisApplicationTests {
@Autowired
private BookDao bookDao;
@Test
void contextLoads() {
System.out.println(bookDao.getById(1));
}
}

整合MybatisPlus

  • 导入对应的starter:

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.3</version>
    </dependency>

    第三方提供的starter,命名为:**-spring-boot-starter 或者 **-boot-starter

  • 配置数据源信息

    1
    2
    3
    4
    5
    6
    7
    #2.配置相关信息
    spring:
    datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ssm_db
    username: root
    password: root

同样两步配置完毕,之后写MybatisPlus的程序:

1
2
3
4
5
6
//映射接口 
@Mapper
public interface BookDao extends BaseMapper<Book> {
}
//测试类
.....

整合Druid

  • 导入对应坐标

    1
    2
    3
    4
    5
    6
    7
    <dependencies>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.16</version>
    </dependency>
    </dependencies>
  • 修改数据源:

    1
    2
    3
    4
    5
    6
    7
    spring:
    datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource

    在数据源配置中的type属性,用于指定数据源类型(默认指定的数据源时HiKari)

    如果数据源需要进行个性化的配置,就需要导入对应数据源的starter,如导入Druid的:

    1
    2
    3
    4
    5
    6
    7
    <dependencies>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.6</version>
    </dependency>
    </dependencies>

    修改配置:

    1
    2
    3
    4
    5
    6
    7
    spring:
    datasource:
    druid:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
    username: root
    password: root

运维事项

工程打包

  1. 使用maven的package进行打包,会打包至target目录下, 后缀为 .jar
  2. 使用 “java -jar 打包的文件名”命令运行工程即可。

注:jar支持命令启动需要依赖maven插件,需要确定有导入以下插件。

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>

临时属性

  1. 使用jar命令启动SpringBoot工程时可以使用临时属性替换配置文件中的属性
  2. 临时属性添加方式:java –jar 工程名.jar –-属性名=值
  3. 多个临时属性之间使用空格分隔
  4. 临时属性必须是当前boot工程支持的属性,否则设置无效
1
java -jar 项目打包文件.jar --server.port=8080 --spring.datasource.druid.password=123

在开发环境jdea中测试临时属性:在运行/调试配置中的“程序参数”进行输入。

在“程序参数”中输入的数据,其实会保存到启动程序的参数args中:

1
2
3
4
5
6
@SpringBootApplication
public class XxxApplication {
public static void main(String[] args) {
SpringApplication.run(XxxApplication.class, args); //args参数可以不传,相当于断掉外部临时参数的入口,使得临时参数无法生效
}
}

配置文件

配置文件4级分类: 满足不同阶段对配置的设置需求

  • SpringBoot中4级配置文件

    1级: file :config/application.yml 【级别最高】 (file是指打包文件.jar所在的目录)

    2级: file :application.yml

    3级:classpath:config/application.yml (classpath是源根,即工程的resources目录)

    4级:classpath:application.yml 【级别最低】

  • 作用:

    • 1级与2级留做系统打包后设置通用属性,1级常用于运维经理进行线上整体项目部署方案调控。2级用于运维人员配置涉密的线上环境。

    • 3级与4级用于系统开发阶段设置通用属性,3级常用于项目经理进行整体项目属性调控,4级是程序员开发时用的。

自定义配置文件:

在运行/调试配置中的“程序参数”中输入自定义的配置文件:

1
--spring.config.name=文件名
1
--spring.config.location=classpath:/文件名.yml

多配置文件常用于将配置进行分类,进行独立管理,或将将可选配置单独制作便于上线更新维护:

1
--spring.config.location=classpath:/文件名.yml,classpath:/文件名.properties
  • 单服务器项目:使用自定义配置文件需求较低
  • 多服务器项目:使用自定义配置文件需求较高,将所有配置放置在一个目录中,统一管理
  • 基于SpringCloud技术,所有的服务器将不再设置配置文件,而是通过配置中心进行设定,动态加载配置信息

多环境开发配置

多环境开发:

1.单文件:

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
# 应用哪个环境, 以及公共的配置
spring:
profiles:
active: pro

# 使用 --- 区分不同的环境, spring.profiles配置环境名
---
# 生产环境
spring:
profiles: pro
server:
port: 80

---
# 开发环境
spring:
profiles: dev
server:
port: 81

---
# 测试环境
spring:
profiles: test
server:
port: 82

2.多文件:

把多个环境的配置放在一个文件里,安全性不好,可以采用多文件的配置方式:

  1. 主启动文件 application.yml
1
2
3
4
# 应用哪个环境, 以及公共的配置
spring:
profiles:
active: pro

环境分类文件的命名为: application-环境名.yml

  1. 环境分类文件 application-pro.yml

    1
    2
    server:
    port: 80
  2. 环境分类文件 application-dev.yml

    1
    2
    server:
    port: 81
  3. 环境分类文件 application-test.yml

    1
    2
    server:
    port: 82

多环境分组管理:

根据功能对配置文件中的信息进行拆分,并制作成独立的配置文件,命名规则如下

  • application-devDB.yml
  • application-devRedis.yml
  • application-devMVC.yml

主启动文件application.yml中使用include属性进行指定:

1
2
3
4
spring:
profiles:
active: dev
include: devDB,devRedis,devMVC

从Spring2.4版开始使用group属性替代include属性,降低了配置书写量 —— 使用group属性定义多种主环境与子环境的包含关系

1
2
3
4
5
6
7
spring:
profiles:
active: dev
group:
"dev": devDB,devRedis,devMVC
"pro": proDB,proRedis,proMVC
"test": testDB,testRedis,testMVC

Maven与SpringBoot多环境兼容:

当Maven与SpringBoot同时对多环境进行控制时,以Mavn为主, SpringBoot使用@..@占位符读取Maven对应的配置属性值:

  1. Maven中设置多环境属性(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
    <profiles> 
    <profile>
    <id>dev_env</id>
    <properties>
    <profile.active>dev</profile.active>
    </properties>
    <activation>
    <activeByDefault>true</activeByDefault>
    </activation>
    </profile>
    <profile>
    <id>pro_env</id>
    <properties>
    <profile.active>pro</profile.active>
    </properties>
    </profile>
    <profile>
    <id>test_env</id>
    <properties>
    <profile.active>test</profile.active>
    </properties>
    </profile>
    </profiles
  2. Spring中引用Maven属性:

    1
    2
    3
    spring:
    profiles:
    active: @profile.active@
  3. 执行Maven打包,可在生成的boot打包.jar文件中查看。

日志

日志作用:

  • 编程期调试代码
  • 运营期记录信息:
    • 记录日常运营重要信息(峰值流量、平均响应时长……)
    • 记录应用报错信息(错误堆栈)
    • 记录运维过程数据(扩容、宕机、报警……)

使用:

1.导入依赖

1
2
3
4
5
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

2.yml配置文件中可以设置日志级别:debug,info,warn,error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 开启debug模式,输出调试信息,常用于检查系统运行状况
debug: true
# 设置日志级别,root表示根节点,即整体应用日志级别
logging:
# 设置日志组
group:
# 自定义组名,设置当前组中所包含的包
ebank: com.zzc.controller,com.zzc.dao
level:
root: warn
# 为对应组设置日志级别
ebank: debug
# 为对包设置日志级别
com.zzc.controller: debug

3.在对应类上添加注解:

1
2
3
4
5
6
7
8
9
10
11
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
public void test(){
log.debug("debug ...");
log.info("info ...");
log.warn("warn ...");
log.error("error ...");
}
}

或者:

1
2
3
4
5
private static final Logger log = LoggerFactory.getLogger(UserController.class);
log.debug("debug ...");
log.info("info ...");
log.warn("warn ...");
log.error("error ...");

日志输出格式控制:

1
2
3
logging:
pattern:
console: "%d %clr(%p) --- [%16t] %clr(%-40.40c){cyan} : %m %n"

默认的是: 时间,级别,PID,所属线程,所属类/接口名,日志信息

设置记录日志的文件:

1
2
3
4
5
6
7
8
9
10
11
logging:
# 单一日志文件,日志都记在一个文件,会导致文件过大
file:
name: server.log
# 滚动日志文件,超过指定大小就再新建一个日志文件
logback:
rollingpolicy:
# 指定的文件大小
max-file-size: 3KB
# 文件名
file-name-pattern: server.%d{yyyy-MM-dd}.%i.log

开发使用

热部署

热部署,就是修改程序后,服务器只修改更新后的程序,不用重启服务器。

springboot项目设置热部署:

部署原理:

一个springboot项目在运行时实际上是分两个过程进行的,根据加载的东西不同,划分成base类加载器与restart类加载器。

  • base类加载器:用来加载jar包中的类,jar包中的类和配置文件由于不会发生变化,因此不管加载多少次,加载的内容不会发生变化
  • restart类加载器:用来加载开发者自己开发的类、配置文件、页面等信息,这一类文件受开发者影响

​ 当springboot项目启动时,base类加载器执行,加载jar包中的信息后,restart类加载器执行,加载开发者制作的内容。当执行构建项目后,由于jar中的信息不会变化,因此base类加载器无需再次执行,所以仅仅运行restart类加载即可,也就是将开发者自己制作的内容重新加载就行了,这就完成了一次热部署的过程。

​ 也可以说热部署的过程实际上是重新加载restart类加载器中的信息。

  • 手动启动热部署:

    1. 导入对应坐标:

      1
      2
      3
      4
      5
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <optional>true</optional>
      </dependency>
    2. 手动构建项目: bulid——Build Project,或 CTRL+F9

  • 自动启动热部署:

    1. 打开 File —— settings —— Build,Execution,Deployment —— Compiler —— 勾选【Build project automatically】,意思是自动构建项目
    2. 使用快捷键 Ctrl+Alt+Shift+/ ,打开维护面板,选择第1项【Registry…】,搜索compiler,勾选。这样就完成了。

    idea设置当idea工具失去焦点5秒后进行热部署。就是你从idea工具中切换到其他工具时进行热部署,比如改完程序需要到浏览器上去调试,这个时候idea就自动进行热部署操作。

设置热部署监控的文件范围(有些文档文件的修改不需要热部署):

—— 开发者工具中有一组配置,当满足了配置中的条件后,才会启动热部署,配置中默认不参与热部署的目录信息如下:

  • /META-INF/maven
  • /META-INF/resources
  • /resources
  • /static
  • /public
  • /templates

​ 以上目录中的文件如果发生变化,是不参与热部署的。如果想修改配置,可以通过application.yml文件进行设定哪些文件不参与热部署操作:

1
2
3
4
5
spring:
devtools:
restart:
# 设置不参与热部署的文件或文件夹
exclude: static/**,public/**,config/application.yml

关闭热部署:

线上环境运行时是不可能使用热部署功能的,所以需要强制关闭此功能,通过配置可以关闭此功能(关闭了监视代码变动的功能):

1
2
3
4
spring:
devtools:
restart:
enabled: false

或者(优先级比配置文件高,不容易被其他配置文件覆盖):

1
2
3
4
5
6
7
@SpringBootApplication
public class XxxApplication {
public static void main(String[] args) {
System.setProperty("spring.devtools.restart.enabled","false");
SpringApplication.run(XxxApplication.class);
}
}

测试

加载测试用属性

测试过程需要模拟一些线上情况,或者模拟一些特殊情况。如果当前环境按照线上环境已经设定好了,例如是下面的配置:

1
2
3
env:
maxMemory: 32GB
minMemory: 16GB

但是如果想测试对应的兼容性,需要测试如下配置:

1
2
3
env:
maxMemory: 16GB
minMemory: 8GB

但测试时不能随意修改源码,所以需要在测试环境中创建一组临时属性,去覆盖源码中设定的属性。

  • 临时属性

    在测试用例中,springboot可以通过注解@SpringBootTest添加属性来模拟临时属性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //properties属性可以为当前测试用例添加临时的属性配置
    @SpringBootTest(properties = {"test.prop=testValue1"})
    public class PropertiesAndArgsTest {

    @Value("${test.prop}")
    private String msg;

    @Test
    void testProperties(){
    System.out.println(msg);
    }
    }
  • 临时参数

    springboot可以通过命令行参数输入配置信息,测试用例通过@SpringBootTest(args={}) 也可以:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //args属性可以为当前测试用例添加临时的命令行参数
    @SpringBootTest(args={"--test.prop=testValue2"})
    public class PropertiesAndArgsTest {

    @Value("${test.prop}")
    private String msg;

    @Test
    void testProperties(){
    System.out.println(msg);
    }
    }

加载测试用bean

测试时,可能需要独立的bean,专门应用于测试环境:

  1. 在测试包test中创建专用的测试环境配置类

    1
    2
    3
    4
    5
    6
    7
    @Configuration
    public class MsgConfig {
    @Bean
    public String msg(){
    return "bean msg";
    }
    }
  2. 在启动测试环境处,导入测试环境专用的配置类,使用@Import注解即可实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @SpringBootTest
    @Import({MsgConfig.class})
    public class ConfigurationTest {

    @Autowired
    private String msg;

    @Test
    void testConfiguration(){
    System.out.println(msg);
    }
    }

Web环境模拟测试

在测试中对表现层功能进行测试需要一个基础和一个功能。

  • 一个基础是:运行测试程序时,必须启动web环境,不然没法测试web功能。
  • 一个功能是:必须在测试程序中具备发送web请求的能力,不然无法实现web功能的测试。

测试类中启动web环境:

​ 在测试类上方的@SpringBootTest注解,有一个属性叫做webEnvironment。通过该属性就可以设置在测试用例中启动web环境:

1
2
3
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WebTest {
}

springboot提供了4中设置值:

  • MOCK:根据当前设置确认是否启动web环境,例如使用了Servlet的API就启动web环境,属于适配性的配置
  • DEFINED_PORT:使用自定义的端口作为web服务器端口
  • RANDOM_PORT:使用随机端口作为web服务器端口
  • NONE:不启动web环境

通过上述配置,启动测试程序时就可以正常启用web环境,建议测试时使用RANDOM_PORT,避免代码中因为写死设定引发线上功能打包测试时由于端口冲突导致意外现象的出现。

测试类中发送请求:

  1. 在测试类中通过注解@AutoConfigureMockMvc 开启web虚拟调用功能:

    1
    2
    3
    4
    5
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    //开启虚拟MVC调用
    @AutoConfigureMockMvc
    public class WebTest {
    }
  2. 定义发起虚拟调用的对象MockMVC,通过自动装配的形式初始化对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    //开启虚拟MVC调用
    @AutoConfigureMockMvc
    public class WebTest {
    // @Autowired
    // private MockMvc mvc;

    @Test
    void testWeb(@Autowired MockMvc mvc) {
    }
    }
  3. 创建一个虚拟请求对象,封装请求的路径,并使用MockMVC对象发送对应请求:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    //开启虚拟MVC调用
    @AutoConfigureMockMvc
    public class WebTest {

    @Test
    void testWeb(@Autowired MockMvc mvc) throws Exception {
    //http://localhost:8080/books
    //创建虚拟请求,当前访问/books
    MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
    //执行对应的请求
    mvc.perform(builder);
    }
    }
  4. 发完请求会得到一个响应对象,测试该响应对象的各种信息是否符合要求:

    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
    @Test
    void testStatus(@Autowired MockMvc mvc) throws Exception {
    MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
    ResultActions action = mvc.perform(builder);

    //设定预期值 与真实值进行比较,成功测试通过,失败测试失败

    //对比状态码:
    //定义本次调用的预期值
    StatusResultMatchers status = MockMvcResultMatchers.status();
    //预计本次调用时成功的:状态200
    ResultMatcher ok = status.isOk();
    //添加预计值到本次调用过程中进行匹配
    action.andExpect(ok);

    //对比响应头信息
    HeaderResultMatchers header = MockMvcResultMatchers.header();
    ResultMatcher contentType = header.string("Content-Type", "application/json");
    action.andExpect(contentType);

    //对比响应体内容(非json格式)
    ContentResultMatchers content = MockMvcResultMatchers.content();
    ResultMatcher result = content.string("springboot2");
    action.andExpect(result);

    //对比响应体内容(json格式)
    ContentResultMatchers content = MockMvcResultMatchers.content();
    ResultMatcher result = content.json("{\"id\":1,\"name\":\"springboot2\",\"type\":\"springboot\"}");
    action.andExpect(result);
    }

数据层测试回滚

打包的阶段的test生命周期属于必须被运行的生命周期,如果跳过会给系统带来极高的安全隐患,所以测试用例必须执行。

不过为了避免测试过程对数据库产生影响,所以需要让测试程序不提交事务:

  • 在测试用例中添加注解@Transactional 即可实现当前测试用例的事务不提交。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
@Transactional
@Rollback(true)
public class DaoTest {
@Autowired
private BookService bookService;

@Test
void testSave(){
Book book = new Book();
book.setName("springboot3");
book.setType("springboot3");
book.setDescription("springboot3");

bookService.save(book);
}
}

如果想提交事务,可以修改为:@Rollback(false),即可正常提交事务。

随机测试数据

对于测试用例的数据固定书写肯定是不合理的,springboot提供了在配置中使用随机值的机制,确保每次运行程序加载的数据都是随机的。具体如下:

1
2
3
4
5
6
7
8
testcase:
book:
id: ${random.int}
id2: ${random.int(10)}
type: ${random.int!5,10!}
name: ${random.value}
uuid: ${random.uuid}
publishTime: ${random.long}

​ 当前配置就可以在每次运行程序时创建一组随机数据,避免每次运行时数据都是固定值的尴尬现象发生,有助于测试功能的进行。数据的加载按照之前加载数据的形式,使用@ConfigurationProperties注解即可

1
2
3
4
5
6
7
8
9
10
11
@Component
@Data
@ConfigurationProperties(prefix = "testcase.book")
public class BookCase {
private int id;
private int id2;
private int type;
private String name;
private String uuid;
private long publishTime;
}

​ 对于随机值的产生,还有一些小的限定规则,比如产生的数值性数据可以设置范围等:

1
2
3
4
5
testcase:
book:
id: ${random.int} #随机数
id2: ${random.int(10)} # 10以内的随机数
type: ${random.int!5,10!} #5到10的随机数

初始化阶段

在 Spring Boot 应用程序中,ApplicationRunner,CommandLineRunner 和 ApplicationListener 都可用于在应用程序启动时执行一些特定操作。

以下是它们之间的主要区别:

  1. ApplicationRunner: ApplicationRunner 是一个接口,用于在 Spring Boot 应用程序启动时运行特定代码。当实现 ApplicationRunner 接口时,需要重写 run 方法。该方法接收一个 ApplicationArguments 类型的参数,可以用于处理传递给应用程序的命令行参数。

    1
    2
    3
    4
    5
    6
    public class MyAppRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) {
    System.out.println("ApplicationRunner running...");
    }
    }
  2. CommandLineRunner: CommandLineRunner 也是一个接口,与 ApplicationRunner 类似,用于在 Spring Boot 应用程序启动时运行特定代码。实现 CommandLineRunner 接口时,也需要重写 run 方法。不同的是,该方法接收一个字符串数组(String[])作为参数,用于处理传递给应用程序的命令行参数。

    1
    2
    3
    4
    5
    6
    public class MyCommandLineRunner implements CommandLineRunner {
    @Override
    public void run(String... args) {
    System.out.println("CommandLineRunner running...");
    }
    }
  3. ApplicationListener: ApplicationListener 是一个泛型接口,用于在 Spring Boot 应用程序中监听特定类型的事件。实现 ApplicationListener 接口时,需要指定监听的事件类型,并重写 onApplicationEvent 方法。在此方法中处理事件。与 ApplicationRunner 和 CommandLineRunner 不同,ApplicationListener 可用于监听各种事件,而不仅仅是应用程序启动事件。

    例如,要在应用程序启动时执行特定操作,可以实现 ApplicationListener<ApplicationReadyEvent>:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class MyAppListener implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
    // 只有当前ApplicationContext是根上下文时,此时程序处于根上下文初始化阶段
    if (event.getApplicationContext().getParent() == null) {
    ......
    }
    }
    }

总结:

  • ApplicationRunner 和 CommandLineRunner 都用于在 Spring Boot 应用程序启动时执行特定代码,但它们处理命令行参数的方式不同。ApplicationRunner 使用 ApplicationArguments 对象,而 CommandLineRunner 使用字符串数组。
  • ApplicationListener 用于监听应用程序中的特定类型事件,不仅限于启动事件。可以通过实现 ApplicationListener 并指定相应事件类型来处理不同的事件。

数据层解决方案

自定义序列化

  1. 构建要进行序列化的实体类User:

    会用到的常见json注解:

    • @JsonIgnoreProperties:
      此注解是类注解,作用是在json序列化时将Java bean中的某些属性忽略掉,序列化和反序列化都受影响。
    • @JsonIgnore:
      此注解用于属性或者方法上(最好是属性上),作用和上面的@JsonIgnoreProperties一样。
    • @JsonFormat:
      此注解用于属性或者方法上(最好是属性上),可以方便的把Date类型直接转化为我们想要的模式,比如@JsonFormat(pattern = “yyyy-MM-dd HH-mm-ss”)
    • @JsonSerialize:
      此注解用于属性或者getter方法上,用于在序列化时嵌入我们自定义的序列化器,比如序列化一个double时在其后面限制两位小数点。
    • @JsonDeserializ:
      此注解用于属性或者setter方法上,用于在反序列化时嵌入我们自定义的反序列化器,比如反序列化一个Date类型的时间字符串。
    • @JsonCreator与@JsonProperty:
      该注解的作用就是指定反序列化时替代无参构造函数,构造方法的参数前面需要加上@JsonProperty注解。
    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
    @Data
    @ToString
    @JsonIgnoreProperties(value = {"word"})
    public class User {

    /**
    * 注意:在进行JSON序列化和反序列化时,要么提供一个无参的构造方法,要么在其他构造方法上添加@JsonCreator注解.
    */
    private String name;
    private int age;
    private boolean sex;
    private Date birthday;
    private String word;
    private double salary;

    // @JsonCreator指定反序列化时用于替代无参构造函数, 参数前要加上@JsonProperty
    @JsonCreator
    public User(@JsonProperty("name") String name, @JsonProperty("age") int age, @JsonProperty("sex") boolean sex, @JsonProperty("birthday") Date birthday,
    @JsonProperty("word") String word, @JsonProperty("salary") double salary) {
    super();
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.birthday = birthday;
    this.word = word;
    this.salary = salary;
    }

    /**
    * 反序列化一个固定格式的Date
    */
    @JsonDeserialize(using = CustomeJackson.MyDeserializer.class)
    public void setBirthday(Date birthday) {
    this.birthday = birthday;
    }

    /**
    * 序列化指定格式的double格式
    */
    @JsonSerialize(using = CustomeJackson.MySerializer.class)
    public double getSalary() {
    return salary;
    }

    }
  2. 编写自己的JsonSerializer和JsonDeserializer类,来进行自定义的序列化和反序列操作。并使用**@JsonComponent注解**将其注册为spring beans(原来是用是通过Module方式注册到Jackson中)。

    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
    @JsonComponent
    public class CustomeJackson {

    /**
    * 自定义序列化器,格式化数值
    */
    public static class MySerializer extends JsonSerializer<Double> {

    private DecimalFormat df = new DecimalFormat("##.00");

    /**
    * 序列化操作,继承JsonSerializer,重写Serialize函数
    */
    @Override
    public void serialize(
    Double value,
    JsonGenerator jsonGenerator,
    SerializerProvider serializerProvider) throws IOException {
    jsonGenerator.writeString(df.format(value));
    }
    }

    /**
    * 自定义反序列化器,格式化时间
    */
    public static class MyDeserializer extends JsonDeserializer<Date> {

    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

    @Override
    public Date deserialize(
    JsonParser jsonParser,
    DeserializationContext deserializationContext) throws IOException {
    Date date = null;
    try {
    date = sdf.parse(jsonParser.getText());
    } catch (ParseException e) {
    e.printStackTrace();
    }
    return date;
    }
    }

    }
  3. 直接编写controller即可,会自动进行序列化操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @RestController
    public class UserController {

    /**
    * 将对象转为json字符串-->序列化
    */
    @GetMapping("/user/{salary}")
    public User home(@PathVariable("salary") Long salary) {
    return new User("一一哥", 30, true, new Date(), "程序员", salary);
    }

    /**
    * 将一个json转化为对象-->反序列化
    */
    @RequestMapping(value = "user")
    public String getValue(@RequestBody User user) {
    log.warn("user=" + user.toString());
    return user.toString();
    }

    }

httpClient

点击接口 xxxMapping处的图标的“在HTTP客户端中打开”可以跳转到 httpClient,输入测试数据并运行即可。

推荐将测试数据的文件单独保存到一处:

  • 新建一个文件保存端口数据:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    "dev": {
    "access_token": "",
    "gateway_host": "localhost:63010",
    "content_host": "localhost:63040",
    "system_host": "localhost:63110",
    "media_host": "localhost:63050",
    "search_host": "localhost:63080",
    "auth_host": "localhost:63070",
    "checkcode_host": "localhost:63075",
    "learning_host": "localhost:63020"
    }
    }
  • 新建另一个测试w文件,如xc-content-api.http,输入测试数据:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ###
    POST http://{{content_host}}/content/course/list?pageNo=1&pageSize=2
    Content-Type: application/json

    {
    "auditStatus": "",
    "courseName": "测试",
    "publishStatus": ""
    }

MybatisPlus

概念

国人开发的Mybatis的便捷版。

使用步骤

  1. 导入相关的starter:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.3</version>
    </dependency>
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
    </dependency>
  2. 配置数据源:

    1
    2
    3
    4
    5
    6
    spring:
    datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=UTC
    username: root
    password: zzc
    • 如果数据源需要进行个性化的配置,就需要导入对应数据源的starter,如导入Druid的:

      1
      2
      3
      4
      5
      6
      7
      <dependencies>
      <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid-spring-boot-starter</artifactId>
      <version>1.2.6</version>
      </dependency>
      </dependencies>

      修改配置:

      1
      2
      3
      4
      5
      6
      7
      spring:
      datasource:
      druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
      username: root
      password: root
  3. 创建数据层的 Dao/Mapper 接口

    接口继承BaseMapper接口 —— BaseMapper接口中提供了许多常用方法,我们只需要从容器中获取Mapper就可以了,不用自己去编写sql语句。

    扫描:可以直接在接口上加@Mapper,或者在启动类上设置扫描@MapperScan(“xxx”)

    1
    2
    3
    @Mapper
    public interface BookDao extends BaseMapper<Book> {
    }
  4. 测试

常用设置

表的映射规则

默认情况下,MP操作的表名就是实体类的类名,但如果表名与类名不一致就需要设置映射规则。

单独设置: 在实体类上加上 @TableName

1
2
3
4
5
6
7
8
@TableName("tbl_book")
@Data
public class Book {
private Integer id;
private String type;
private String name;
private String description;
}

全局设置: 一般一个项目的表名的前缀有统一风格,可以通过 .yml 配置文件配置

1
2
3
4
mybatis-plus:
global-config:
db-config:
table-prefix: tbl_ #设置表名通用前缀

主键生成策略

默认情况下,使用MP插入数据时,主键生成策略是基于雪花算法的自增id。

如果需要使用别的策略,可以:

单独设置: 在代表主键的字段上加上 @TableId,使用其type属性指定主键生成策略。

1
2
3
4
5
6
7
@Data
public class Book {
//设置主键自增长
@TableId(type = IdType.AUTO)
private Integer id;
...
}

全局设置:

1
2
3
4
mybatis-plus:
global-config:
db-config:
id-type: auto #设置主键id字段的生成策略 为参照数据库设定的策略,默认是使用 基于雪花算法的自增id

字段映射

默认MP会根据实体类的属性名去映射表的列名。

如果列名和实体类的属性不一致,可以用 @TableField 设置映射关系。

1
2
@TableField("address")
private String addressStr;

驼峰映射

默认情况下MP会开启字段名列名的驼峰映射,即将驼峰命名的属性翻译成下划线命名,如:userName会翻译成 user_name

sql执行日志

配置sql日志输出,即打印对应的sql执行语句:

1
2
3
4
mybatis-plus:
configuration:
# 日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

数据层使用

基本方法

包名为dao或mapper, 数据层接口需要继承BaseMapper,BaseMapper提供常见的sql方法。

  • select() —— 查询
  • selectBatchIds(list) 批量查询
  • insert() —— 插入数据
  • delete() —— 删除数据
    • deleteBatchIds(list) 批量删除
  • update() —— 更新数据

条件构造器Wrapper

wrapper是一个接口,用于更方便的构造条件。

继承体系:

  • wrapper
    • AbstractWrapper
      • QueryWrapper
      • UpdateWrapper

AbstractWrapper提供了许多构造where条件的方法;

QueryWrapper额外提供了针对Select语法的select方法;

UpdateWrapper额外提供了用于针对set语法的set方法;

提供的方法中,可以加上一个Boolean参数,表示是否要加上当前的条件;因为有些时候传入的变量可能是null,而查询时会直接当作字符串处理,所以可以加上 变量 != null、Strings.isNotEmpty(变量) 等等, 来判断。

1
2
3
4
5
6
@Test
public void test(){
QueryWrapper wrapper = new QueryWrapper();
wrapper.like(Strings.isNotEmpty(name), User::getName, name);
userMapper.selectList(wrapper);
}

AbstractWrapper

完整的AbstractWrapper方法参照官方文档:条件构造器 | MyBatis-Plus (baomidou.com)

常用AbstractWrapper方法:

eq:equals,等于

gt:greater than ,大于 >

ge:greater than or equals,大于等于≥

lt:less than,小于<

le:less than or equals,小于等于≤

between:相当于SQL中的BETWEEN

like:模糊匹配。like(“name”,”黄”),相当于SQL的name like ‘%黄%’

likeRight:模糊匹配右半边。likeRight(“name”,”黄”),相当于SQL的name like ‘黄%’

likeLeft:模糊匹配左半边。likeLeft(“name”,”黄”),相当于SQL的name like ‘%黄’

notLike:notLike(“name”,”黄”),相当于SQL的name not like ‘%黄%’

isNull

isNotNull

and:SQL连接符AND

or:SQL连接符OR

in: in(“age”,{1,2,3})相当于 age in(1,2,3)

groupBy: groupBy(“id”,”name”)相当于 group by id,name

orderByAsc :orderByAsc(“id”,”name”)相当于 order by id ASC,name ASC

orderByDesc :orderByDesc (“id”,”name”)相当于 order by id DESC,name DESC

例1:

SQL语句如下:

1
2
3
4
5
6
SELECT 
id,user_name,PASSWORD,NAME,age,address
FROM
`USER`
WHERE
age > 18 AND address = '狐山'

用Wrapper写法如下:

1
2
3
4
5
6
7
8
@Test
public void testWrapper01(){
QueryWrapper wrapper = new QueryWrapper();
wrapper.gt("age",18);
wrapper.eq("address","狐山");
List<User> users = userMapper.selectList(wrapper);
System.out.println(users);
}

例2:

SQL语句如下:

1
2
3
4
5
6
7
8
SELECT 
id,user_name,`PASSWORD`,NAME,age,address
FROM
`USER`
WHERE
id IN(1,2,3) AND
age BETWEEN 12 AND 29 AND
address LIKE '%山%'

用Wrapper写法如下:

1
2
3
4
5
6
7
8
9
@Test
public void testWrapper02(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.in("id",1,2,3);
wrapper.between("age",12,29);
wrapper.like("address","山");
List<User> users = userMapper.selectList(wrapper);
System.out.println(users);
}

例3:

SQL语句如下:

1
2
3
4
5
6
7
8
9
SELECT 
id,user_name,`PASSWORD`,NAME,age,address
FROM
USER
WHERE
id IN(1,2,3) AND
age > 10
ORDER BY
age DESC

用Wrapper写法如下:

1
2
3
4
5
6
7
8
9
@Test
public void testWrapper03(){
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.in("id",1,2,3);
queryWrapper.gt("age",10);
queryWrapper.orderByDesc("age");
List<User> users = userMapper.selectList(queryWrapper);
System.out.println(users);
}

QueryWrapper

QueryWrapper的 select() 可以设置 要/不要查询的列名:

  • select(String s…. )—— 形参为要查询的列名

  • select(Class<T> entityClass, Predicate<TableFieldInfo> predicate) —— 第一个参数为实体类的class字节码对象,第二个参数是Predicate类型,用于过滤要查询的字段(主键除外)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // sql语句: SELECT id,user_name FROM USER    查user_name,而id作为主键默认会查
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.select(User.class, new Predicate<TableFieldInfo>() {
    @Override
    public boolean test(TableFieldInfo tableFieldInfo) {
    //返回true为要查询,false为不查询
    return "user_name".equals(tableFieldInfo.getColumn());
    }
    });
    List<User> users = userMapper.selectList(queryWrapper);
  • select(Predicate<TableFieldInfo> predicate)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // sql语句: SELECT id,user_name,PASSWORD,NAME,age FROM USER 即不查address字段,其他都查
    QueryWrapper<User> queryWrapper = new QueryWrapper<>(new User());
    queryWrapper.select(new Predicate<TableFieldInfo>() {
    @Override
    public boolean test(TableFieldInfo tableFieldInfo) {
    return !"address".equals(tableFieldInfo.getColumn());
    }
    });
    List<User> users = userMapper.selectList(queryWrapper);

UpdateWrapper

UpdateWrapper的set方法来设置要更新的列及其值,不用传入一个类,同时这种方式也可以使用Wrapper去指定更复杂的更新条件。

1
2
3
4
5
6
7
//UPDATE USER 
// SET age = 99
// where id > 1
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.gt("id",1);
updateWrapper.set("age",99);
userMapper.update(null,updateWrapper);

Lambda条件构造器

MP提供了一个Lambda条件构造器可以让我们直接以实体类的方法引用的形式来指定列名。这样就可以避免在条件构造中 写错列名 了。

1
2
3
4
5
QueryWrapper<User> queryWrapper = new QueryWrapper();
//queryWrapper.gt("age",18);
//queryWrapper.eq("address","狐山");
queryWrapper.gt(User::getAge,18);
queryWrapper.eq(User::getAddress,"狐山");

自定义SQL

虽然MP为我们提供了很多常用的方法,并且也提供了条件构造器。但是如果真的遇到了复制的SQL时,我们还是需要自己去定义方法,自己去写对应的SQL,这样SQL也更有利于后期维护。

​ 因为MP是对mybatis做了增强,所以还是支持之前Mybatis的方式去自定义方法。

​ 同时也支持在使用Mybatis的自定义方法时使用MP的条件构造器帮助我们进行条件构造。

Mybatis方式

1、定义方法

​ 在Mapper接口中定义方法

1
2
3
4
5
public interface UserMapper extends BaseMapper<User> {

User findMyUser(Long id);
}

2、创建xml

​ 先配置xml文件的存放目录

1
2
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml

创建对应的xml映射文件

3、在xml映射文件中编写SQL

​ 创建对应的标签,编写对应的SQL语句

1
2
3
4
5
6
7
8
<?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="com.sangeng.mapper.UserMapper">

<select id="findMyUser" resultType="com.sangeng.domian.User">
select * from user where id = #{id}
</select>
</mapper>

Mybatis方式结合条件构造器

​ 我们在使用上述方式自定义方法时。如果也希望我们的自定义方法能像MP自带方法一样使用条件构造器来进行条件构造的话只需要使用如下方式即可。

①方法定义中添加Warpper类型的参数

添加Warpper类型的参数,并且要注意给其指定参数名。

1
2
3
4
public interface UserMapper extends BaseMapper<User> {

User findMyUserByWrapper(@Param(Constants.WRAPPER) Wrapper<User> wrapper);
}

②在SQL语句中获取Warpper拼接的SQL片段进行拼接。

1
2
3
<select id="findMyUserByWrapper" resultType="com.sangeng.domian.User">
select * from user ${ew.customSqlSegment}
</select>

注意:不能使用#{}应该用${}

分页查询

  1. 配置分页查询拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class PageConfig {
/**
* 3.4.0之前的版本
* @return
*/
/* @Bean
public PaginationInterceptor paginationInterceptor(){
return new PaginationInterceptor();
}*/

/**
* 3.4.0之后版本
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
  1. 接口:IPage, 实现类:Page

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //分页功能
    @Test
    void testGetPage(){
    IPage page = new Page(2,5); //第2页,每页5条
    bookDao.selectPage(page, null); //第二个参数为wrapper
    page.getCurrent(); //当前页码值
    page.getSize(); //每页显示数
    page.getTotal(); //数据总量
    page.getPages(); //总页数
    page.getRecords(); //详细数据
    }

业务层Service

包名一般为service,也叫service层。

Service接口定义与数据层接口定义的差别:

  • 业务层的接口名称一般是业务名,如登录业务就叫 login(username, passwd)
  • 数据层的接口名称,一般要写明用什么查什么,如登录业务中要调用的查询用户信息功能,叫 selectByUserNameAndPassword(username, passwd)

MP提供了service层的实现,只需要编写一个接口,继承IService,并创建一个接口实现类继承ServiceImpl,即可使用。

相比于Mapper接口,Service层主要是支持了更多批量操作的方法。

如:

  • 之前的写法:

    1
    2
    3
    public interface UserService {
    List<User> list();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Service
    public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public List<User> list() {
    return userMapper.selectList(null);
    }
    }
  • 现在的写法:

    1
    2
    3
    public interface UserService extends IService<User> { 

    }
    1
    2
    3
    4
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {

    }

    需要其他的自定义方法的话,直接加就行。

更多功能

代码生成器

MP提供了一个代码生成器,可以让我们一键生成实体类,Mapper接口,Service,Controller等全套代码 。使用方式如下

  1. 添加依赖
1
2
3
4
5
6
7
8
9
10
11
<!--mybatisplus代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!--模板引擎-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
  1. 生成

​ 修改相应配置后执行以下代码即可

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
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import org.junit.jupiter.api.Test;

public class GeneratorTest {
@Test
public void generate() {
AutoGenerator generator = new AutoGenerator();

// 全局配置
GlobalConfig config = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
// 设置输出到的目录
config.setOutputDir(projectPath + "/src/main/java");
config.setAuthor("zzc");
// 生成结束后是否打开文件夹
config.setOpen(false);

// 全局配置添加到 generator 上
generator.setGlobalConfig(config);

// 数据源配置
DataSourceConfig dataSourceConfig = new DataSourceConfig();
dataSourceConfig.setUrl("jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&serverTimezone=UTC");
dataSourceConfig.setDriverName("com.mysql.cj.jdbc.Driver");
dataSourceConfig.setUsername("root");
dataSourceConfig.setPassword("zzc");

// 数据源配置添加到 generator
generator.setDataSource(dataSourceConfig);

// 包配置, 生成的代码放在哪个包下
PackageConfig packageConfig = new PackageConfig();
packageConfig.setParent("com.zzc");

// 包配置添加到 generator
generator.setPackageInfo(packageConfig);

// 策略配置
StrategyConfig strategyConfig = new StrategyConfig();
// 下划线驼峰命名转换
strategyConfig.setNaming(NamingStrategy.underline_to_camel);
strategyConfig.setColumnNaming(NamingStrategy.underline_to_camel);
// 开启lombok
strategyConfig.setEntityLombokModel(true);
// 开启RestController
strategyConfig.setRestControllerStyle(true);
generator.setStrategy(strategyConfig);
generator.setTemplateEngine(new FreemarkerTemplateEngine());

// 开始生成
generator.execute();
}
}

自动填充

在进行插入或更新操作时,对字段自动填充。

使用:

  1. 在需要填充的字段上添加注解 TableField, 其属性 fill 来设定什么时候进行自动填充。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * 更新时间
    */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    /**
    * 创建时间
    */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
  2. 继承自定义填充器MetaObjectHandler,实现两个方法(插入时填充,更新时填充)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Component
    public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
    this.setFieldValByName("createTime", LocalDateTime.now(), metaObject);
    this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
    this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
    }
    }

逻辑删除

逻辑删除:数据没有被删除,只是不会被展示出来;

使用:

  • 先在数据库 配置好逻辑删除的实体字段名,并规定表示删除和未删除状态的字段值,如规定 delFlag = 1为已删除,delFlag = 0 为未删除;

  • 再配置yaml文件:

    1
    2
    3
    4
    5
    6
    mybatis-plus:
    global-config:
    db-config:
    logic-delete-field: delFlag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
    logic-delete-value: 1 # 逻辑已删除值(默认为 1)
    logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

乐观锁

并发操作时,我们需要保证对数据的操作不发生冲突。乐观锁就是其中一种方式。

乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。.因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。

使用乐观锁时一般在表中增加一个version列。用来记录我们对每次记录操作的版本。每次对某条记录进行过操作是,对应的版本也需要+1。

在执行更新时: set version = 老版本+1 where version = 老版本

使用:

  1. 添加对应MP拦截器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Configuration
    public class MPConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
    //创建MP拦截器栈
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    //分页拦截器
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
    //乐观锁拦截器
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    return interceptor;
    }
    }
  2. 在实体类的字段上加上@Version注解

    1
    2
    @Version
    private Integer version;
  3. 测试

SpringSecurity

Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。

​ 一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。

​ 一般Web应用都需要进行认证和授权。

​ 认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

​ 授权:经过认证后判断当前用户是否有权限进行某个操作

​ 而认证和授权也是SpringSecurity作为安全框架的核心功能。

认证

一般,登录流程是:

  1. 前端携带用户名密码访问登录接口;
  2. 服务器去和数据库校验用户名和密码,校验通过会根据用户名id生成一个token;
  3. 服务器把token响应给前端;

后续访问:

  1. 前端在登录后的后续请求,需要在请求头中携带token;
  2. 服务器获取token进行解析,得到userId;
  3. 根据userId获取用户信息,判断是否有权限访问;

而SpringSecurity其实就是一个过滤器链,内部包含了提供各种功能的过滤器。依靠这些过滤器来进行认证和授权。

SpringSecurity比较重要的过滤器有(按经过顺序排序):

  • UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。
  • ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
  • FilterSecurityInterceptor:负责权限校验的过滤器。

SpringSecurity的一个简单认证流程如下:

springsecurity简单认证流程.drawio

其中的接口含义:

  • Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
  • AuthenticationManager接口:定义了认证Authentication的方法
  • UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
  • UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

使用思路:

  • 登录

    1. 自定义登录接口:

      上述流程图中,第一个过滤器 UsernamePasswordAuthenticationFilter 会自动跳转到Security的登录页面,所以要改为自己的控制层接口,并调用ProviderManager的方法进行认证,如果认证通过生成jwt(并把用户信息存入redis中)。

      • ProviderManager接口的实现类是AuthenticationManager;
    2. 自定义UserDetailsService (替换掉默认流程中的InmemoryUserDetailManager,因为它是到内存中查询的),在这个实现类中去查询数据库

  • 校验:

    1. 定义Jwt认证过滤器:获取token, 解析token获取其中的userid,根据userid从redis中获取用户信息,存入SecurityContextHolder

使用

前置准备

  1. 依赖:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!--springSecurity-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!--jwt依赖-->
    <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
    </dependency>

    引入依赖后访问接口会跳到SpringSecurity的默认登陆页面,默认用户名是user,密码会输出在控制台。必须登陆之后才能对接口进行访问。

  2. springSecurity配置类:

    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
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    // 一般让SpringSecurity使用BCryptPasswordEncoder来进行密码校验, 而不用默认的
    @Bean
    public PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http
    //关闭csrf
    .csrf().disable()
    // 不通过session过去SecurityContext
    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    .and()
    .authorizeRequests()
    // 对于登录接口, 允许匿名访问
    .antMatchers("/user/login").anonymous()
    // 除上面外的所有请求全部需要鉴权认证
    .anyRequest().authenticated();

    //添加自定义过滤器
    http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

    //配置自定义异常处理器
    http.exceptionHandling()
    //配置认证失败处理器
    .authenticationEntryPoint(authenticationEntryPoint)
    .accessDeniedHandler(accessDeniedHandler);

    //允许跨域
    http.cors();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
    }
    }
  3. JWT工具类:

    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
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.JwtBuilder;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;

    import javax.crypto.SecretKey;
    import javax.crypto.spec.SecretKeySpec;
    import java.util.Base64;
    import java.util.Date;
    import java.util.UUID;

    /**
    * JWT工具类
    */
    public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
    //设置秘钥明文
    public static final String JWT_KEY = "sangeng";

    public static String getUUID(){
    String token = UUID.randomUUID().toString().replaceAll("-", "");
    return token;
    }

    /**
    * 生成jwt
    * @param subject token中要存放的数据(json格式)
    * @return
    */
    public static String createJWT(String subject) {
    JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
    return builder.compact();
    }

    /**
    * 生成jwt
    * @param subject token中要存放的数据(json格式)
    * @param ttlMillis token超时时间
    * @return
    */
    public static String createJWT(String subject, Long ttlMillis) {
    JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
    return builder.compact();
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
    SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
    SecretKey secretKey = generalKey();
    long nowMillis = System.currentTimeMillis();
    Date now = new Date(nowMillis);
    if(ttlMillis==null){
    ttlMillis=JwtUtil.JWT_TTL;
    }
    long expMillis = nowMillis + ttlMillis;
    Date expDate = new Date(expMillis);
    return Jwts.builder()
    .setId(uuid) //唯一的ID
    .setSubject(subject) // 主题 可以是JSON数据
    .setIssuer("sg") // 签发者
    .setIssuedAt(now) // 签发时间
    .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
    .setExpiration(expDate);
    }

    /**
    * 创建token
    * @param id
    * @param subject
    * @param ttlMillis
    * @return
    */
    public static String createJWT(String id, String subject, Long ttlMillis) {
    JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
    return builder.compact();
    }

    public static void main(String[] args) throws Exception {
    String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";
    Claims claims = parseJWT(token);
    System.out.println(claims);
    }

    /**
    * 生成加密后的秘钥 secretKey
    * @return
    */
    public static SecretKey generalKey() {
    byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
    SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
    return key;
    }

    /**
    * 解析
    *
    * @param jwt
    * @return
    * @throws Exception
    */
    public static Claims parseJWT(String jwt) throws Exception {
    SecretKey secretKey = generalKey();
    return Jwts.parser()
    .setSigningKey(secretKey)
    .parseClaimsJws(jwt)
    .getBody();
    }

    }

实现

登录

  1. 创建一个类,实现UserDetailsService要返回的类型 UserDetails:

    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
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class LoginUser implements UserDetails {

    private User user;

    private List<String> permissions;

    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities;

    public LoginUser(User user, List<String> permissions) {
    this.user = user;
    this.permissions = permissions;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    if (authorities != null) {
    return authorities;
    }
    //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
    authorities = permissions.stream()
    .map(SimpleGrantedAuthority::new)
    .collect(Collectors.toList());
    return authorities;
    }

    @Override
    public String getPassword() {
    return user.getPassword();
    }

    @Override
    public String getUsername() {
    return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
    return true;
    }

    @Override
    public boolean isAccountNonLocked() {
    return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
    return true;
    }

    @Override
    public boolean isEnabled() {
    return true;
    }
    }
  2. 创建一个类实现UserDetailsService接口,重写其中的方法。从数据库中查询数据,而不是从内存查。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    //根据用户名查询用户信息
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(User::getUserName,username);
    User user = userMapper.selectOne(wrapper);
    //如果查询不到数据就通过抛出异常来给出提示
    if(Objects.isNull(user)){
    throw new RuntimeException("用户名或密码错误");
    }
    //TODO 根据用户查询权限信息 添加到LoginUser中
    List<String> list = menuMapper.selectPermsByUserId(user.getId());
    //封装成UserDetails对象返回
    return new LoginUser(user, list);
    }
    }

    其中selectPermsByUserId(user.getId())查询语句为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    SELECT 
    DISTINCT m.`perms`
    FROM
    sys_user_role ur
    LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
    LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
    LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
    WHERE
    user_id = 2
    AND r.`status` = 0
    AND m.`status` = 0
  3. 编写登录接口:

    ​ 需要先让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。(详细看上面的配置类SecurityConfig)

    ​ 在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。

    ​ 认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @RestController
    public class LoginController {

    @Autowired
    private LoginServcie loginServcie;

    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody User user){
    return loginServcie.login(user);
    }
    }
    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
    @Service
    public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    @Override
    public ResponseResult login(User user) {
    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
    Authentication authenticate = authenticationManager.authenticate(token);
    if (Objects.isNull(authenticate)) {
    throw new RuntimeException("用户名或密码错误");
    }
    // 使用userid生成token
    LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
    String userId = loginUser.getUser().getId().toString();
    String jwt = JwtUtil.createJWT(userId);
    // authenticate存入redis
    redisCache.setCacheObject("login:" + userId, loginUser);
    // 把token响应给前端
    HashMap<String, String> map = new HashMap<>();
    map.put("token", jwt);
    return new ResponseResult(200, "登录成功", map);
    }

    @Override
    public ResponseResult logout() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    LoginUser loginUser = (LoginUser) authentication.getPrincipal();
    Long userid = loginUser.getUser().getId();
    redisCache.deleteObject("login:" + userid);
    return new ResponseResult(200, "退出成功");
    }
    }

认证

需要自定义一个过滤器:

  1. 这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。
  2. 使用userid去redis中获取对应的LoginUser对象。
  3. 然后封装Authentication对象存入SecurityContextHolder

需要把过滤器添加到Security的过滤器链中:http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

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
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
// 放行
filterChain.doFilter(request, response);
return;
}
// 解析token
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
// 从redis中获取用户信息
String redisKey = "login:" + userId;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if (Objects.isNull(loginUser)) {
throw new RuntimeException("用户未登录");
}
// 存入securityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}

授权

​ 在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。

​ 所以在项目中只需要把当前登录用户的权限信息也存入Authentication。

​ 然后设置资源所需要的权限即可。

使用:

  1. 启动类开启配置:

    1
    @EnableGlobalMethodSecurity(prePostEnabled = true)
  2. 配置资源的权限校验:@PreAuthorize

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @RestController
    public class HelloController {

    @RequestMapping("/hello")
    @PreAuthorize("hasAuthority('test')")
    public String hello(){
    return "hello";
    }
    }

    常用的权限校验属性:

    • hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。

      1
      @PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')")
    • hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。

      1
      @PreAuthorize("hasRole('system:dept:list')")
    • hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。

      1
      @PreAuthorize("hasAnyRole('admin','system:dept:list')")

其他配置

密码加密更改

默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。一般不会采用这种方式。一般推荐替换为BCryptPasswordEncoder。

1
2
3
4
5
6
7
8
9
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

}

自定义失败处理

​ 在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

  • 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
  • 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。

所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。

1
2
3
4
5
6
7
8
9
10
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "权限不足");
String json = JSON.toJSONString(result);
WebUtils.renderString(response,json);

}
}
1
2
3
4
5
6
7
8
9
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录");
String json = JSON.toJSONString(result);
WebUtils.renderString(response,json);
}
}

在Security的配置类的configure方法中使用HttpSecurity对象的方法进行配置:

1
2
3
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);

跨域

处了对MVC配置外,因为资源都会收到SpringSecurity的保护,所以想要跨域访问还要让SpringSecurity运行跨域访问。

在配置类中的configure方法中使用HttpSecurity对象的方法进行配置:

1
2
3
4
5
6
@Override
protected void configure(HttpSecurity http) throws Exception {
....
//允许跨域
http.cors();
}

自定义权限校验

定义自己的权限校验方法:在@PreAuthorize注解中使用我们的方法。

1
2
3
4
5
6
7
8
9
10
11
12
@Component("ex")
public class SGExpressionRoot {

public boolean hasAuthority(String authority){
//获取当前用户的权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
List<String> permissions = loginUser.getPermissions();
//判断用户权限集合中是否存在authority
return permissions.contains(authority);
}
}

​ 在SPEL表达式中使用 @ex相当于获取容器中bean的名字为ex的对象。然后再调用这个对象的hasAuthority方法

1
2
3
4
5
@RequestMapping("/hello")
@PreAuthorize("@ex.hasAuthority('system:dept:list')")
public String hello(){
return "hello";
}

认证成功/失败处理器

在UsernamePasswordAuthenticationFilter进行登录认证的时候

  • 如果登录成功了会调用AuthenticationSuccessHandler的方法进行认证成功后的处理。 AuthenticationSuccessHandler就是登录成功处理器。
  • 如果认证失败了会调用AuthenticationFailureHandler的方法进行认证失败后的处理。

可以自己去自定义成功处理器进行成功后的相应处理。

1
2
3
4
5
6
7
8
9
@Component
public class SGSuccessHandler implements AuthenticationSuccessHandler {

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("认证成功了");
}
}

1
2
3
4
5
6
7
@Component
public class SGFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("认证失败了");
}
}

在Security配置类中配置:

1
2
3
4
5
6
7
8
9
10
    protected void configure(HttpSecurity http) throws Exception {
....
http.formLogin()
// 配置认证成功处理器
.successHandler(successHandler)
// 配置认证失败处理器
.failureHandler(failureHandler);

http.authorizeRequests().anyRequest().authenticated();
}

JWT

JSON Web Token(JSON Web令牌)

它定义了一种紧凑的、自包含的方式,用于在各方之间以JSON对象安全地传输信息。此信息可以验证和信任,因为它是数字签名的。

  • JWT 通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。

作用:

  • 授权:一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。它的开销很小并且可以在不同的域中使用。如:单点登录。
  • 信息交换:在各方之间安全地传输信息。JWT可进行签名(如使用公钥/私钥对),因此可确保发件人。由于签名是使用标头和有效负载计算的,因此还可验证内容是否被篡改。

优点:

  • 简洁(Compact):可以通过URL,POST参数或者在HTTP header发送,数据量小,传输速度也很快;
  • 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库;
  • Token是以JSON加密的形式保存在客户端,所以JWT是跨语言的,原则上任何web形式都支持。
  • 不需要在服务端保存会话信息,特别适用于分布式微服务。

组成结构: 是一个String字符串,由3部分组成,中间用点隔开

  • 标头(Header):有令牌的类型和所使用的签名算法,如HMAC、SHA256、RSA;使用Base64编码组成;
  • 有效载荷(Payload):有效负载,包含声明;声明是有关实体(通常是用户)和其他数据的声明,不放用户敏感的信息,如密码。同样使用Base64编码
  • 签名(Signature):Signature需要使用编码后的header和payload
    加上我们提供的一个密钥,使用header中指定的签名算法(HS256)进行签名。签名的作用是保证JWT没有被篡改过

token格式:head.payload.singurater 如:xxxxx.yyyy.zzzz

jwt与token的区别:

  • 相同: 都是访问资源的令牌, 都可以记录用户信息,都是只有验证成功后才能访问资源。
  • 不同: 服务端验证客户端发来的token信息要进行数据的查询操作;JWT验证客户端发来的token信息就不用, 在服务端使用密钥校验就可以,不用数据库的查询。

Token验证流程:

  1. 把用户的用户名和密码发到后端
  2. 后端进行校验,校验成功会生成token, 把token发送给客户端
  3. 客户端自己保存token, 再次请求就要在Http协议的请求头中带着token去访问服务端,和在服务端保存的token信息进行比对校验。

JWT验证流程:

  1. 在头部信息中声明加密算法和常量, 然后把header使用json转化为字符串
  2. 在载荷中声明用户信息,同时还有一些其他的内容;再次使用json 把载荷部分进行转化,转化为字符串
  3. 使用在header中声明的加密算法和每个项目随机生成的secret来进行加密, 把第一步分字符串和第二部分的字符串进行加密, 生成新的字符串。词字符串是独一无二的。
  4. 解密的时候,只要客户端带着JWT来发起请求,服务端就直接使用secret进行解密。

封装工具类例子:

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
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

/**
* JWT工具类
*/
public class JwtUtil {

//有效期为
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "sangeng";

public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}

/**
* 生成jwt
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}

/**
* 生成jwt
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
return builder.compact();
}

private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis=JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("sg") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}

/**
* 创建token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
return builder.compact();
}

public static void main(String[] args) throws Exception {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";
Claims claims = parseJWT(token);
System.out.println(claims);
}

/**
* 生成加密后的秘钥 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}

/**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}

}

题外-RBAC权限模型

RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。

包含:

  1. 用户表:每个用户都有唯一的UID识别,并被授予不同的角色
  2. 角色表:不同角色具有不同的权限
  3. 权限表
  4. 用户-角色关联表(一个用户可以分配多个角色)
  5. 角色-权限关联表(一个角色包含duo’ge)

常用工具/框架

lombok

Maven依赖:

1
2
3
4
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

实习类

  • @Getter, @Setter

    生成默认的get、set方法,可以加在类上,或只加在某字段上;

    注:@Getter可用于枚举而@Setter不能, 与@Accessors注解一起使用会产生影响

  • @Accessors

    用于修改getter和setter方法的内容

    功能:

    • fluent: 如果为true,让getter、setter方法的方法名没有前缀。且默认让chain为true。
    • chain:如果为true,则setter方法返回this,而不是void。
    • prefix:匹配字段前缀,如果前缀存在,则去除前缀并创建新的字段名,否则跳过该字段,并生成警告。(前缀后面不能是小写字母,不然不会不会匹配)
  • 构造方法:

    • @AllArgsConstructor:全参数构造方法
    • @RequiredArgsConstructor:带参数构造方法,参数只能是带有@NonNull和final修饰的未经初始化字段
    • @NoArgsConstructor:无参构造方法

    可以配置属性: access = AccessLevel.PRIVATE 设置构造方法为私有

  • @ToString,**@EqualAndHashCode**: 重写toString,equals,hashCode方法

  • Data

    集成了多个注解:

    • @Getter
    • @Setter
    • @NoArgsConstructor
    • @ToString
    • @EqualsAndHashCode

    属性: staticConstructor

  • @Value: 是不可变的@Data,所有字段由private和final修饰,不会产生setter方法,类本身也由final修饰。

  • @Builder、**@Singular**: 自动生成构造者模式代码,Singular是针对集合属性的特殊处理。

    1
    2
    3
    4
    5
    @Builder
    public class User {
    private String name;
    private int age;
    }
    1
    2
    3
    4
    User user = User.builder()
    .name("Tom")
    .age(20)
    .build();
  • @Slf4j:帮助生成日志记录代码,使用 log.xxx输出日志

  • @NonNull

    • 用在方法的参数上: 在方法/构造函数体的开头插入一个空检查,即在赋值该字段时对参数进行判空检查!

      1
      2
      3
      4
      5
      6
      7
      public class Question {

      private String text;

      public Question(@NonNull final String text) {
      this.text = text;
      }
    • 用在字段上:任何为该字段赋值的生成方法也将生成这些空检查。

      1
      2
      3
      4
      public class Question {
      @NonNull
      private String text;
      }

代码块

  • @Cleanup: 自动管理资源,会自动调用close()关闭资源。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Cleanup InputStream in = new FileInputStream(args[0]);
    @Cleanup OutputStream out = new FileOutputStream(args[1]);
    byte[] b = new byte[10000];
    while (true) {
    int r = in.read(b);
    if (r == -1) break;
    out.write(b, 0, r);
    }
    // 如果要清理的对象没有close()方法,可以指定一个无参方法来进行自动清理
    @Cleanup("dispose") CoolBar bar = new CoolBar(parent, 0);

common-lang3

StringUtils

所有操作都是null安全的。

  • IsEmpty/IsBlank - 检查字符串是否包含文本

    • ```java
      // 字符串为 “”和null时, 返回true
      boolean isEmpty(String str)
      1
      2
      3
      4

      * ```java
      // 字符串为 " ","",null时, 返回true
      boolean isBlank(String str)
  • clean/Trim/Strip - 删除前导和尾随的控制字符(char <= 32)

    • // 如果是null则返回""
      String clean(String str)
      
      1
      2
      3
      4
      5
      6

      * ```java
      // 允许返回null
      String trim(String str)
      // 如果只有null和空格, 则返回null
      String trimToNull(String str)
    • ```java
      // 前后删除指定字符stripChars中的任意一个, null视为空格
      // 允许返回null
      // 如:StringUtils.strip(“ abcyx”, “xyz”) = “ abc”
      String strip(String str, String stripChars)
      // stripChars为null, 此时效果和trim一样
      String strip(String str)
      String stripToNull(String str)
      // 可以批量操作数组中的字符串
      String[] stripAll(String[] strs, String stripChars)

      1
      2
      3
      4
      5
      6
      7
      8

      * Equals - 比较两个字符串,null安全

      ```java
      // 允许比较null
      boolean equals(String str1, String str2)
      // 忽略大小写
      boolean equalsIgnoreCase(String str1, String str2)
  • startsWith - 检查字符串是否以前缀开头

  • endsWith - 检查字符串是否以后缀结尾

  • IndexOf/LastIndexOf/Contains - 检索下标位置

    • ```java
      // 使用Stirng.indexOf方法, 字符串为null或找不到时返回-1
      int indexOf(String str, char/String searchChar, [int startPos])
      // 返回str中找到的第ordinal个searchStr的下标, 否则返回-1
      ordinalIndexOf(String str, String searchStr, int ordinal)
      // 同样有忽略大小写的版本
      int indexOfIgnoreCase(String str, String searchStr)
      1
      2
      3
      4
      5
      6

      * ```java
      // str中是否包含searchChar
      boolean contains(String str, char searchChar)
      // 忽略大小写
      boolean containsIgnoreCase(String str, String searchStr)
  • IndexOfAny/LastIndexOfAny/IndexOfAnyBut/LastIndexOfAnyBut - 检索字符串中是否有给定字符集中的任何一个

    • ```java
      // searchChars也可以以String形式给出, 会转换为char[]
      int indexOfAny(String str, char[] searchChars)
      int indexOfAny(String str, String searchChars)
      1
      2
      3
      4
      5

      * ```java
      // 查找不在字符集中的任何字符
      int indexOfAnyBut(String str, char[] searchChars)
      int indexOfAnyBut(String str, String searchChars)
  • ContainsOnly/ContainsNone/ContainsAny - 字符串是否仅包含/无/任何这些字符

    1
    2
    3
    4
    5
    6
    7
    8
    /** 
    StringUtils.containsOnly(null, *) = false
    StringUtils.containsOnly(*, null) = false
    StringUtils.containsOnly("", *) = true
    StringUtils.containsOnly("ab", '') = false
    StringUtils.containsOnly("abab", 'abc') = true
    */
    boolean containsOnly(String str, char[] valid)
  • Substring/Left/Right/Mid - 子字符串提取

  • SubstringBefore/SubstringAfter/SubstringBetween - 相对于其他字符串的子字符串提取

    • ```java
      // 获取第一次出现分隔符之前的子字符串。不返回分隔符。
      String substringBefore(String str, String separator)
      1
      2
      3
      4
      5

      * ```java
      // 获取嵌套在两个字符串之间的字符串。仅返回第一个匹配项。
      // StringUtils.substringBetween("yabczyabcz", "y", "z") = "abc"
      String substringBetween(String str, String open, String close)
  • Split/Join - 将字符串拆分为子字符串数组,反之亦然

    • ```java
      // 将提供的文本拆分为具有最大长度的数组,并指定分隔符。
      // 分隔符不包含在返回的字符串数组中。相邻的分隔符被视为一个分隔符。
      // 输入 null 字符串返回 null。 null 分隔符字符在空格上拆分
      // 如果找到max个分隔的子字符串,则最后一个返回的字符串包括第一个 max - 1 返回的字符串之后的所有字符(包括分隔符)。
      String[] split(String str, String separatorChars, int max)
      1
      2
      3
      4

      * ```java
      // 拼接数组的元素, 可以选择是否有分隔符
      String join(Object[] array, char separator)
  • Remove/Delete - 删除字符串的一部分

    • ```java
      // 从源字符串中删除子字符串的所有匹配项
      String remove(String str, String remove)
      // 仅当位于字符串开头时, 删除此子字符串
      String removeStart(String str, String remove)
      1
      2
      3
      4
      5
      6

      * ```java
      // 删除空格, 空格定义为{' ', '\t', '\r', '\n', '\b'}
      String deleteSpaces(String str)
      // 删除空格, 空格是' '
      String deleteWhitespace(String str)
  • Replace/Overlay - 搜索字符串并用另一个字符串替换一个字符串

    • // 将text中[start, end)的字符串替换成overlay
      String overlayString(String text, String overlay, int start, int end)
      
      1
      2
      3
      4
      5
      6
      7
      8

      * Chomp/Chop - 从字符串末尾删除子字符串,否则将其保留。

      * ```java
      // 末尾删除一个换行符, 换行符是 “\n”、“”\r 或 “\r\n”。
      String chomp(String str)
      // 末尾删除separator
      String chomp(String str, String separator)
    • // 删除最后一个字符, 如果结尾是"\r\n",都删除
      String chop(String str)
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

      * LeftPad/RightPad/Center/Repeat - 填充字符串

      * UpperCase/LowerCase/SwapCase/Capitalize/Uncapitalize - 更改字符串的大小写

      * ```java
      // 将第一个字母更改为标题大小写
      String capitalize(String str)
      // 取消大写字符串,将第一个字母更改为标题大小写
      String uncapitalize(String str)
    • ```java
      // 大写字符转换为小写,标题大小写字符转换为小写,小写字符转换为大写
      // StringUtils.swapCase(“The dog has a BONE”) = “tHE DOG HAS A bone”
      String swapCase(String str)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18

      * CountMatches - 计算一个字符串在另一个字符串中的出现次数

      * IsAlpha/IsNumeric/IsWhitespace/IsAsciiPrintable - 检查字符串中的字符

      * ```java
      // 检查字符串是否仅包含 字母
      boolean isAlpha(String str)
      // 检查字符串是否仅包含 字母和空格 (' ')
      boolean isAlphaSpace(String str)
      // 检查字符串是否仅包含 字母或数字
      boolean isAlphanumeric(String str)
      // 检查字符串是否仅包含 字母、数字或空格 (' ')
      boolean isAlphanumericSpace(String str)
      // 检查字符串是否仅包含 ASCII 可打印字符
      boolean isAsciiPrintable(String str)
      // 检查字符串是否仅包含小写字符
      boolean isAllLowerCase(String str)
  • DefaultString - 防止空输入字符串

    1
    2
    // 返回原字符串,如果字符串为 null,则返回空字符串 ("")
    String defaultString(String str)
  • Reverse/ReverseDelimited - 反转字符串

    • ```java
      // 反转由特定字符分隔的字符串, 分隔符之间的字符串不会反转
      // 如分隔符为’.’, 则java.lang.String 变为 String.lang.java
      String reverseDelimited(String str, char separatorChar)
      1
      2
      3
      4
      5
      6
      7

      * Abbreviate - 使用省略号缩写字符串

      ```java
      // 如果长度小于maxWidth字符,则str返回它。否则将其缩写为 substring(str, 0, max-3) + "..."
      // 注: maxWidth >= 4
      String abbreviate(String str, int maxWidth)
  • Difference - 比较字符串并报告其差异(返回第二个字符串的其余部分,从它与第一个字符串不同的位置开始)

  • getCommonPrefix - 比较数组中的所有字符串,并返回所有字符串通用的初始字符序列

  • getLevensteinDistance - 将一个字符串更改为另一个字符串所需的更改次数

ArrayUtils

这是对数组、基础类型数组(如int[])和包装类数组(Integer[])的操作。
此类优雅地处理 null 输入。数组输入不会引发 null 异常。但是,包含元素的 null Object 数组可能会引发异常。每种方法都记录了其行为。

  • add/addAll:添加元素方法

  • contains:包含方法

  • isSameLength:判断长度相等

  • nullToEmpty:null转换成空

  • subarray:接取数组

  • indexOf:获取索引

  • toPrimitive/toObject/toMap/toArray

  • reverse: 反转方法

  • toString:打印数组方法

  • remove/removeElement/removeAll:删除元素方法

  • isEmpty:判空方法

  • getLength:获取长度长度

  • clone:克隆方法

BooleanUtils

对布尔基元和布尔对象的操作。
此类尝试优雅地处理 null 输入。不会为输入引发 null 异常。

  • toString:转化为字符串
  • toBoolean/toBooleanObject:基本类和封装类转换
  • toInteger:转换为整数
  • compare:比较
  • and:与
  • or:或
  • negate:非
  • xor:异或
  • isTrue:判断真假

ClassUtils

  • hierarchy:获取父类层级
  • getAllSuperclasses:获取父类
  • getAllInterfaces:获取实现接口
  • getSimpleName/getShortCanonicalName/getShortClassName:获取类名
  • getAbbreviatedName:获取类的缩略名
  • getPackageName/getPackageCanonicalName:获取包名
  • isAssignable:判断是否可以转型
  • isInnerClass:判断是否内部类
  • isPrimitiveWrapper/isPrimitiveOrWrapper:判断是否为基础类或包装类
  • toClass:对象转Class对象
  • convertClassesToClassNames:类名和Class类互转

RandomUtils

  • // 生成 [0, Integer.MAX_VALUE) 之间的随机 int 值
    int nextInt()	
    
    1
    2
    3
    4

    * ```java
    // 生成 [startInclusive,endExclusive) 之间的随机整数,起始值不能小于终止值。
    int nextInt(int startInclusive, int endExclusive)
  • // 生成 [0, Long.MAX_VALUE) 之间的 long 值
    long nextLong()
    
    1
    2
    3
    4

    * ```java
    // 生成 [startInclusive,endExclusive) 之间的随机 long 值
    long nextLong(long startInclusive, long endExclusive)
  • // 生成 [0, Double.MAX_VALUE) 直接的随机 double 值
    double nextDouble()	
    
    1
    2
    3
    4

    * ```java
    // 生成 [startInclusive,endExclusive) 之间的随机 double 值
    double nextDouble(double startInclusive, double endInclusive)
  • ```java
    // 随机生成一个布尔值,true 或者 false
    boolean nextBoolean()

    1
    2
    3
    4

    * ```java
    // 生成指定个数的字节数组,如 nextBytes(10) 生成的字节数组有 10 个 byte 元素
    byte[] nextBytes(int count)

RandomStringUtils

  • // 创建长度为指定个数(count)的随机字符串,将从所有字符集中选择字符,不含字母和数字
    random(int count)
    
    1
    2
    3
    4
    5

    * ```java
    // 生成指定个数(count)的随机字符串,字符将从参数指示的字母或数字字符集中选择
    // letters和numbers的布尔取值是指随机生成是否含有字母或数字
    random(int count, boolean letters, boolean numbers)
  • ```java
    // 创建长度为指定个数的随机字符串,从指定的字符集(chars)中选择字符.
    random(int count, char[] chars)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    ### ObjectUtils

    * allNotNull(Object... values):检查给定数组中的任何元素值是否都不是 null
    * anyNotNull(Object... values):检查给定数组中的元素是否有不是 null 的值。

    * T defaultIfNull(T object, final T defaultValue): 如果传递的对象是 null,则返回默认值

    * firstNonNull(T... values):返回数组中不是 null 的第一个值
    * isEmpty(Object object):检查对象是否为空

    ### NumberUtils

    ```java
    //从数组中选出最大值
    NumberUtils.max(new int[] { 1, 2, 3, 4 });//---4
    //判断字符串是否全是整数
    NumberUtils.isDigits("153.4");//--false
    //判断字符串是否是有效数字
    NumberUtils.isNumber("0321.1");//---false
    // 字符串转BigDecimal, BigInteger,Double,Float,Integer,Long
    NumberUtils.createXxxx("13324352")
    NumberUtils.toXxxx("123", default) // 转换,转换失败返回默认值default

DateUtils

1
2
3
4
5
6
//日期加n天
DateUtils.addDays(new Date(), n);
//判断是否同一天
DateUtils.isSameDay(date1, date2);
//字符串时间转换为Date
DateUtils.parseDate(str, parsePatterns);

DateFormatUtils

加密解密类

1
2
3
4
5
6
7
8
// MD5加密
String encodeStr=DigestUtils.md5Hex(text + key);
// 密钥进行验证
String md5Text = md5(text, key);
if(md5Text.equalsIgnoreCase(md5)){
System.out.println("MD5验证通过");
return true;
}

SystemUtils

swagger

swagger可以利用添加注解,来快速生成接口文档,以及进行接口测试。

  1. 引入依赖:

    1
    2
    3
    4
    5
    6
    <!-- Spring Boot 集成 swagger -->
    <dependency>
    <groupId>com.spring4all</groupId>
    <artifactId>swagger-spring-boot-starter</artifactId>
    <version>1.9.0.RELEASE</version>
    </dependency>
  2. 配置文件:

    1
    2
    3
    4
    5
    6
    swagger:
    title: "xxx内容管理系统"
    description: "内容管理系统对课程相关信息进行管理"
    base-package: com.zzc.content # 要进行文档生成的包的地址
    enabled: true
    version: 1.0.0
  3. 在要进行生成的类上,加上@EnableSwagger2Doc

    在类上添加@Api,在方法上添加@ApiOperation,可以生成中文标题,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Api(value = "管理接口", tags = "课程信息管理接口")
    @EnableSwagger2Doc
    @RestController
    public class CourseBaseInfoController {

    @ApiOperation("课程查询接口")
    @PostMapping("/course/list")
    public PageResult<CourseBase> list(PageParams pageParams, @RequestBody QueryCourseParamsDto queryCourseParamsDto) {
    return null;
    }
    }
  4. 访问地址:http://a.b.c.d:xxxx/..../swagger-ui.html,如:

    1
    http://localhost:63040/content/swagger-ui.html

一些常用的swagger注解:

1
2
3
4
5
6
7
8
9
10
11
@Api:修饰整个类,描述Controller的作用
@ApiOperation:描述一个类的一个方法,或者说一个接口
@ApiParam:单个参数描述
@ApiModel:用对象来接收参数
@ApiModelProperty:用对象接收参数时,描述对象的一个字段
@ApiResponse:HTTP响应其中1个描述
@ApiResponses:HTTP响应整体描述
@ApiIgnore:使用该注解忽略这个API
@ApiError :发生错误返回的信息
@ApiImplicitParam:一个请求参数
@ApiImplicitParams:多个请求参数

fastjson

log4j2

guava

MinIO

视频编码

FFmpeg

远程调用RestTemplate

RestTemplate这个类是 Spring 框架提供的一个工具类,用于远程调用HTTP接口(REST风格)。

创建RestTemplate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
RestTemplate restTemplate = new RestTemplate(factory);
return restTemplate;
}

@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(5000);
factory.setConnectTimeout(15000);
// 设置代理
//factory.setProxy(null);
return factory;
}

// 或者
@Bean
RestTemplate restTemplate(){
return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
}

常用方法:

Get请求

  1. 没有动态参数的请求:

    1
    2
    3
    4
    5
    6
    // 被请求的接口
    @GetMapping("/test/get")
    @ResponseBody
    public BookDto get() {
    return new BookDto(1, "Spring系列");
    }

    可以使用 getForObject(url, class) 或 getForEntity(url, class)

    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
    RestTemplate restTemplate = new RestTemplate();
    String url = "http://localhost:8080/chat16/test/get";

    public void test() {
    // getForObject方法,获取响应体,将其转换为第二个参数指定的类型
    BookDto bookDto = restTemplate.getForObject(url, BookDto.class);
    System.out.println(bookDto);
    }
    //输出: BookDto{id=1, name='Spring系列'}

    public void test() {
    //getForEntity方法,返回值为ResponseEntity类型
    // ResponseEntity中包含了响应结果中的所有信息,比如头、状态、body
    ResponseEntity<BookDto> responseEntity = restTemplate.getForEntity(url, BookDto.class);
    //状态码
    System.out.println(responseEntity.getStatusCode());
    //获取头
    System.out.println("头:" + responseEntity.getHeaders());
    //获取body
    BookDto bookDto = responseEntity.getBody();
    System.out.println(bookDto);
    }
    //输出:
    // 200 OK
    // 头:[Content-Type:"application/json;charset=UTF-8", Transfer-Encoding:"chunked", Date:"Sat, 02 Oct 2021 07:05:15 GMT", Keep-Alive:"timeout=20", Connection:"keep-alive"]
    // BookDto{id=1, name='Spring系列'}
  2. url中含有动态参数:

    1
    2
    3
    4
    5
    @GetMapping("/test/get/{id}/{name}")
    @ResponseBody
    public BookDto get(@PathVariable("id") Integer id, @PathVariable("name") String name) {
    return new BookDto(id, name);
    }

    还是 getForObject 或 getForEntity,参数加上map即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    RestTemplate restTemplate = new RestTemplate();
    //url中有动态参数
    String url = "http://localhost:8080/chat16/test/get/{id}/{name}";
    Map<String, String> uriVariables = new HashMap<>();
    uriVariables.put("id", "1");
    uriVariables.put("name", "Spring系列");

    public void test() {
    //getForObject方法
    BookDto bookDto = restTemplate.getForObject(url, BookDto.class, uriVariables);
    System.out.println(bookDto);
    }

    @Test
    public void test() {
    //getForEntity方法
    ResponseEntity<BookDto> responseEntity = restTemplate.getForEntity(url, BookDto.class, uriVariables);
    BookDto bookDto = responseEntity.getBody();
    System.out.println(bookDto);
    }
  3. 接口返回值为泛型的请求:

    1
    2
    3
    4
    5
    6
    7
    8
    @GetMapping("/test/getList")
    @ResponseBody
    public List<BookDto> getList() {
    return Arrays.asList(
    new BookDto(1, "Spring系列"),
    new BookDto(2, "SpringMVC系列")
    );
    }

    需要使用 exchange 方法,并通过其中的 ParameterizedTypeReference类型的参数来指定泛型类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    RestTemplate restTemplate = new RestTemplate();
    //返回值为泛型
    String url = "http://localhost:8080/chat16/test/getList";

    public void test() {
    //若返回结果是泛型类型的,需要使用到exchange方法,
    //这个方法中有个参数是ParameterizedTypeReference类型,通过这个参数类指定泛型类型
    ResponseEntity<List<BookDto>> responseEntity = restTemplate.exchange(
    url,
    HttpMethod.GET,
    null,
    new ParameterizedTypeReference<List<BookDto>>() {}
    );
    List<BookDto> bookDtoList = responseEntity.getBody();
    System.out.println(bookDtoList);
    }
  4. 下载小文件,直接用byte[]接收,如果文件过大,用字节数据接收会OOM

    1
    ResponseEntity<byte[]> responseEntity = restTemplate.getForEntity(url, byte[].class)

    下载大文件,需要使用execute方法,方法中有个ResponseExtractor 类型的参数,可以在其中的回调方法拿到响应流,边读边处理,就不会导致内存溢出。

    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
    RestTemplate restTemplate = new RestTemplate();
    String url = "http://localhost:8080/chat16/test/downFile";
    /**
    * 文件比较大的时候,比如好几个G,就不能返回字节数组了,会把内存撑爆,导致OOM
    * 需要这么做:
    * 使用execute方法,这个方法中有个ResponseExtractor类型的参数,
    * restTemplate拿到结果之后,会回调{@link ResponseExtractor#extractData}这个方法,
    * 在这个方法中可以拿到响应流,然后进行处理,这个过程就是变读边处理,不会导致内存溢出
    */
    String result = restTemplate.execute(
    url,
    HttpMethod.GET,
    null,
    new ResponseExtractor<String>() {
    @Override
    public String extractData(ClientHttpResponse response) throws IOException {
    System.out.println("状态:"+response.getStatusCode());
    System.out.println("头:"+response.getHeaders());
    //获取响应体流
    InputStream body = response.getBody();
    //处理响应体流
    String content = IOUtils.toString(body, "UTF-8");
    return content;
    }
    }, new HashMap<>());

    System.out.println(result);
  5. 需要传递请求头:

    1
    2
    3
    4
    5
    6
    7
    @GetMapping("/test/getAll/{path1}/{path2}")
    @ResponseBody
    public Map<String, Object> getAll(@PathVariable("path1") String path1,
    @PathVariable("path2") String path2,
    HttpServletRequest request) {
    ....
    }

    用到HttpEntity类型的参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public void test() {
    RestTemplate restTemplate = new RestTemplate();
    String url = "http://localhost:8080/chat16/test/header";

    //1、请求头放在HttpHeaders对象中
    MultiValueMap<String, String> headers = new HttpHeaders();
    headers.add("header-1", "V1");
    headers.add("header-2", "Spring");
    headers.add("header-2", "SpringBoot");
    //2、RequestEntity:请求实体,请求的所有信息都可以放在RequestEntity中,比如body部分、头、请求方式、url等信息
    RequestEntity requestEntity = new RequestEntity(
    null, //body部分数据
    headers, //头
    HttpMethod.GET,//请求方法
    URI.create(url) //地址
    );
    ResponseEntity<Map<String, List<String>>> responseEntity = restTemplate.exchange(
    requestEntity, // 请求实体
    new ParameterizedTypeReference<Map<String, List<String>>>() {}
    );
    Map<String, List<String>> result = responseEntity.getBody();
    System.out.println(result);
    }

POST请求

根据请求头中的Content-Type来指定请求的类型,一般有3种:

Content-Type 说明
application/x-www-form-urlencoded 页面中普通的 form 表单提交时就是这种类型,表单中的元素会按照名称和值拼接好,然后之间用&连接,格式如:p1=v1&p2=v2&p3=v3然后通过 urlencoded 编码之后丢在 body 中发送
multipart/form-data 页面中表单上传文件的时候,用到的就是这种格式
application/json 将发送的数据转换为 json 格式,丢在 http 请求的 body 中发送,后端接口通常用@RequestBody 配合对象来接收。
  1. 普通表单请求: application/x-www-form-urlencoded 类型的请求

    1
    2
    3
    4
    5
    @PostMapping("/test/form1")
    @ResponseBody
    public BookDto form1(BookDto bookDto) {
    return bookDto;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public void test() {
    RestTemplate restTemplate = new RestTemplate();
    String url = "http://localhost:8080/chat16/test/form1";

    //1、表单信息,需要放在MultiValueMap中,MultiValueMap相当于Map<String,List<String>>
    MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
    //调用add方法填充表单数据(表单名称:值)
    body.add("id","1");
    body.add("name","SpringMVC系列");
    //2、发送请求(url,请求体,返回值需要转换的类型)
    BookDto result = restTemplate.postForObject(url, body, BookDto.class);
    System.out.println(result);
    }
  2. 上传本地文件:multipart/form-data 类型的请求

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @PostMapping(value = "/test/form2")
    @ResponseBody
    public Map<String, String> form2(@RequestParam("file1") MultipartFile file1) {
    Map<String, String> fileMetadata = new LinkedHashMap<>();
    fileMetadata.put("文件名", file1.getOriginalFilename());
    fileMetadata.put("文件类型", file1.getContentType());
    fileMetadata.put("文件大小(byte)", String.valueOf(file1.getSize()));
    return fileMetadata;
    }

    上传的文件需要包装为org.springframework.core.io.Resource,常用的有 3 中[FileSystemResource、InputStreamResource、ByteArrayResource]

    • 其中InputStreamResource、ByteArrayResource即通过流和字节数组的方式传输文件,使用时需要重写getFilename()和getFilename()方法,否则会上传失败
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Test
    public void test() {
    RestTemplate restTemplate = new RestTemplate();
    String url = "http://localhost:8080/chat16/test/form2";

    //1、表单信息,需要放在MultiValueMap中,MultiValueMap相当于Map<String,List<String>>
    MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
    //调用add方法放入表单元素(表单名称:值)
    //2、文件对应的类型,需要是org.springframework.core.io.Resource类型的,常见的有[FileSystemResource、InputStreamResource、ByteArrayResource]
    body.add("file1", new FileSystemResource(".\\src\\main\\java\\com\\javacode2018\\springmvc\\chat16\\dto\\UserDto.java"));
    //3、头
    HttpHeaders headers = new HttpHeaders();
    headers.add("header1", "v1");
    headers.add("header2", "v2");
    //4、请求实体
    RequestEntity<MultiValueMap<String, Object>> requestEntity = new RequestEntity<>(body, headers, HttpMethod.POST, URI.create(url));
    //5、发送请求(请求实体,返回值需要转换的类型)
    ResponseEntity<Map<String, String>> responseEntity = restTemplate.exchange(
    requestEntity,
    new ParameterizedTypeReference<Map<String, String>>() {}
    );
    Map<String, String> result = responseEntity.getBody();
    System.out.println(result);
    }
  3. 发送 json 格式数据:传递 java 对象,即application/json类型的请求

    1
    2
    3
    4
    5
    @PostMapping("/test/form4")
    @ResponseBody
    public BookDto form4(@RequestBody BookDto bookDto) {
    return bookDto;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    public void test() {
    RestTemplate restTemplate = new RestTemplate();
    String url = "http://localhost:8080/chat16/test/form4";
    BookDto body = new BookDto(1, "SpringM系列");

    BookDto result = restTemplate.postForObject(url, body, BookDto.class);
    System.out.println(result);
    }

    如果body是java对象,RestTemplate 默认自动配上 Content-Type=application/json, 如果 body 的值是 json 格式字符串的时候,调用的时候需要在头中明确指定 Content-Type=application/json:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public void test17() {
    RestTemplate restTemplate = new RestTemplate();
    String url = "http://localhost:8080/chat16/test/form5";
    //1、:请求体为一个json格式的字符串
    String body = "[{\"id\":1,\"name\":\"SpringMVC系列\"},{\"id\":2,\"name\":\"MySQL系列\"}]";
    /**
    * 2、若请求体为json字符串的时候,需要在头中设置Content-Type=application/json;
    * 若body是普通的java类的时候,无需指定这个,RestTemplate默认自动配上Content-Type=application/json
    */
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    //3、请求实体(body,头、请求方式,uri)
    RequestEntity requestEntity = new RequestEntity(body, headers, HttpMethod.POST, URI.create(url));

    //4、发送请求(请求实体,返回值需要转换的类型)
    ResponseEntity<List<BookDto>> responseEntity = restTemplate.exchange(
    requestEntity,
    new ParameterizedTypeReference<List<BookDto>>() {
    });
    //5、获取结果
    List<BookDto> result = responseEntity.getBody();
    System.out.println(result);
    }

DELETE、PUT、OPTION请求

1
2
3
public void delete(String url, Object... uriVariables);
public void delete(String url, Map<String, ?> uriVariables);
public void delete(URI url);

put和post类似,把类型改为PUT即可

OPTIONS 请求用来探测接口支持哪些 http 方法:

1
2
3
public Set<HttpMethod> optionsForAllow(String url, Object... uriVariables);
public Set<HttpMethod> optionsForAllow(String url, Map<String, ?> uriVariables);
public Set<HttpMethod> optionsForAllow(URI url);

集成HttpClient

RestTemplate 内部默认用的是 jdk 自带的 HttpURLConnection 发送请求的,性能上面并不是太突出。 可以将其替换为 httpclient 或者 okhttp。

以下为集成HttpClient:

  1. 引入 maven 配置

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.7</version>
    </dependency>

    创建 RestTemplate 时指定 HttpClient 配置,代码如下

    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
    public HttpClient httpClient() {
    HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
    try {
    //设置信任ssl访问
    SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, (arg0, arg1) -> true).build();
    httpClientBuilder.setSSLContext(sslContext);
    HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
    SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
    Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
    // 注册http和https请求
    .register("http", PlainConnectionSocketFactory.getSocketFactory())
    .register("https", sslConnectionSocketFactory).build();

    //使用Httpclient连接池的方式配置(推荐),同时支持netty,okHttp以及其他http框架
    PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
    // 最大连接数
    poolingHttpClientConnectionManager.setMaxTotal(1000);
    // 同路由并发数
    poolingHttpClientConnectionManager.setDefaultMaxPerRoute(100);
    //配置连接池
    httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
    // 重试次数
    httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(0, true));
    //设置默认请求头
    List<Header> headers = new ArrayList<>();
    httpClientBuilder.setDefaultHeaders(headers);
    return httpClientBuilder.build();
    } catch (Exception e) {
    throw new RuntimeException(e);
    }
    }

    public ClientHttpRequestFactory clientHttpRequestFactory() {
    HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient());
    // 连接超时(毫秒),这里设置10秒
    clientHttpRequestFactory.setConnectTimeout(10 * 1000);
    // 数据读取超时时间(毫秒),这里设置60秒
    clientHttpRequestFactory.setReadTimeout(60 * 1000);
    // 从连接池获取请求连接的超时时间(毫秒),不宜过长,必须设置,比如连接不够用时,时间过长将是灾难性的
    clientHttpRequestFactory.setConnectionRequestTimeout(10 * 1000);
    return clientHttpRequestFactory;
    }

    public RestTemplate restTemplate(){
    //创建RestTemplate的时候,指定ClientHttpRequestFactory
    return new RestTemplate(this.clientHttpRequestFactory());
    }

    @Test
    public void test18() {
    RestTemplate restTemplate = this.restTemplate();
    String url = "http://localhost:8080/chat16/test/get";
    //getForObject方法,获取响应体,将其转换为第二个参数指定的类型
    BookDto bookDto = restTemplate.getForObject(url, BookDto.class);
    System.out.println(bookDto);
    }

集成 okhttp

引入 maven 配置

1
2
3
4
5
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.3.1</version>
</dependency>

复制

创建 RestTemplate

1
new RestTemplate(new OkHttp3ClientHttpRequestFactory());

模板引擎

模板引擎就是:模板+数据=输出,以Jsp为例,Jsp页面就是模板,页面中嵌入的jsp标签就是数据,两者相结合输出html网页。

  1. 浏览器请求web服务器
  2. 服务器渲染页面,渲染的过程就是向 jsp页面(模板)内填充数据(模型)。
  3. 服务器将渲染生成的页面返回给浏览器。

常用的java模板引擎有:Jsp、Freemarker、Thymeleaf 、Velocity 等

Freemarker

使用:

  1. 添加依赖:

    1
    2
    3
    4
    5
    <!-- Spring Boot 对结果视图 Freemarker 集成 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
    </dependency>
  2. 配置文件:(页面路径为 resources下的templates)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    spring:
    freemarker:
    enabled: true
    cache: false #关闭模板缓存,方便测试
    settings:
    template_update_delay: 0
    suffix: .ftl #页面模板后缀名
    charset: UTF-8
    template-loader-path: classpath:/templates/ #页面模板位置(默认为 classpath:/templates/)
    resources:
    add-mappings: false #关闭项目中的静态资源映射(static、resources文件夹下的资源)
  3. 编写模板文件:如test.ftl模板文件,后缀为 .ftl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="utf-8">
    <title>Hello World!</title>
    </head>
    <body>
    Hello ${name}!
    </body>
    </html>
  4. 编写controller方法,准备模型数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Controller
    public class FreemarkerController {

    @GetMapping("/testfreemarker")
    public ModelAndView test(){
    ModelAndView modelAndView = new ModelAndView();
    //设置模型数据
    modelAndView.addObject("name","小明");
    //设置模板名称-会根据视图名称加.ftl找到模板
    modelAndView.setViewName("test");
    return modelAndView;
    }
    }

认证

用户授权: 用户认证通过后可以访问系统的资源,但只允许访问自己拥有权限的资源,系统会判断用户是否拥有对应的访问权限。

  • 统一认证:平台的不同身份的用户使用统一的认证入口。
  • 单点登录(SSO,Single Sign On):只需认证一次便可以在多个拥有访问权限的系统中访问; 即在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
  • 第三方认证:

Spring Security

介绍

Spring Security是一个用于身份验证和访问控制的框架。

原理: Spring Security对Web资源的保护是靠Filter实现的,当初始化时,Spring Security会创建一个名为 SpringSecurityFilterChain 的Servlet过滤器,类型为 org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类。

  • FilterChainProxy是一个代理,真正起作用的是SecurityFilterChain所包含的各个Filter,不过这些过滤器并不直接处理认证和授权,而是由 认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)调用这些过滤器进行处理。

  • Spring Security功能的实现主要是由一系列过滤器链相互配合完成。以下为几个主要的过滤器:

    • SecurityContextPersistenceFilter :这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
    • UsernamePasswordAuthenticationFilter :用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变;
    • FilterSecurityInterceptor :是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了;
    • ExceptionTranslationFilter :能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
  • Spring Security的执行流程:

    image-20230723142350194

    1. 用户提交用户名、密码被SecurityFilterChain中的UsernamePasswordAuthenticationFilter过滤器获取到,封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
    2. 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
    3. 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。
    4. SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
    5. 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个List<AuthenticationProvider>列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终AuthenticationProvider将UserDetails填充至Authentication。

使用

  1. 添加依赖:

    1
    2
    3
    4
    5
    6
    7
    8
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
  2. 加入配置类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
    // 密码为明文方式
    return NoOpPasswordEncoder.getInstance();
    // return new BCryptPasswordEncoder();
    }

    // 配置安全拦截机制
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http
    .authorizeRequests()
    .antMatchers("/r/**").authenticated()//访问/r开始的请求需要认证通过
    .anyRequest().permitAll()// 其它请求全部放行
    .and()
    .formLogin().successForwardUrl("/login-success");//登录成功跳转到/login-success
    }

    }

    认证服务service,主要用于返回用户信息UserDetails

    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
    // 实现UserDetailsService,在loadUserByUsername方法中返回用户信息UserDetails
    @Service
    public class UserServiceImpl implements UserDetailsService {

    @Autowired
    XcUserMapper xcUserMapper;

    /**
    * @description 根据账号查询用户信息
    * @param s 账号
    * @return org.springframework.security.core.userdetails.UserDetails
    */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

    XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, s));
    if(user == null){
    //返回空表示用户不存在
    return null;
    }

    //取出数据库存储的正确密码
    String password = user.getPassword();
    //TODO 校验密码
    ....

    //用户权限,如果不加报Cannot pass a null GrantedAuthority collection
    String[] authorities = {"p1"};
    //为了安全在令牌中不放密码
    user.setPassword(null);

    //将user对象转json
    String userString = JSON.toJSONString(user);
    //创建UserDetails对象
    UserDetails userDetails = User.withUsername(userString).password(password).authorities(authorities).build();

    return userDetails;
    }

    }

    UserDetails接口只返回了username、密码等信息,如果需要拓展用户信息

    由于JWT令牌中用户身份信息来源于UserDetails,UserDetails中仅定义了username为用户的身份信息,这里有两个思路:

    1. 扩展UserDetails,使之包括更多的自定义属性,
    2. 扩展username的内容 ,比如存入json数据内容作为username的内容。

    相比较而言,方案二比较简单还不用破坏UserDetails的结构,上述代码就是拓展了username,即username属性放入的是user信息的json字符串。

  3. 在对应资源上添加访问所需权限

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @RestController
    public class LoginController {
    ....
    @RequestMapping("/r/r1")
    @PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问
    public String r1(){
    return "访问r1资源";
    }

    @RequestMapping("/r/r2")
    @PreAuthorize("hasAuthority('p2')")//拥有p2权限方可访问
    public String r2(){
    return "访问r2资源";
    }
    ...
    }

JWT

客户端访问网站前会先申请到令牌,接下来客户端携带令牌去访问资源,资源服务器会请求认证服务 校验令牌的合法性。 如果每次访问都需要一次远程校验,那么执行效率会降低。这就可以用到JWT格式的令牌了。

JWT令牌中已经包括了用户相关的信息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权。

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

  • JWT是无状态认证:

    • 传统的基于session的方式是有状态认证,用户登录成功将用户的身份信息存储在服务端,这样加大了服务端的存储压力,并且这种方式不适合在分布式系统中应用。

      当用户访问应用服务,每个应用服务都会去服务器查看session信息,如果session中没有该用户则说明用户没有登录,此时就会重新认证,而解决这个问题的方法是Session复制。

    JWT令牌可以将用户身份信息存储在令牌中,无需存储session,用户认证通过后认证服务颁发令牌给用户,用户将令牌存储在客户端,去访问应用服务时携带令牌去访问,服务端从jwt解析出用户信息。这个过程就是无状态认证。

优点:

  1. jwt基于json,非常方便解析。
  2. 可以在令牌中自定义丰富的内容,易扩展
  3. 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
  4. 资源服务使用JWT可不依赖认证服务即可完成授权。

缺点:

  1. JWT令牌较长,占存储空间比较大。

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

  • Header

    头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA),内容是一个json对象。(使用Base64Url编码,得到的字符串即第一部分)

  • Payload

    负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的信息字段,比如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。(使用Base64Url编码,得到的字符串即第二部分)

    此部分不能存放敏感信息,因为这部分可以直接解码还原得到原始内容。

  • Signature

    签名,用于防止jwt内容被篡改。这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明的签名算法进行签名。如:

    1
    2
    3
    4
    5
    // 其中 secret: 签名所使用的密钥。
    HMACSHA256(
    base64UrlEncode(header) + "." + base64UrlEncode(payload),
    secret
    )
    • 为什么 JWT可以防止篡改?

      第三部分使用签名算法对第一部分和第二部分的内容进行签名,常用的签名算法是 HS256,常见的还有md5,sha 等,签名算法需要使用密钥进行签名,密钥不对外公开,并且签名是不可逆的,如果第三方更改了内容那么服务器验证签名就会失败,要想保证验证签名正确必须保证内容、密钥与签名前一致。

OAuth2

OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用OAUTH认证服务,任何服务提供商都可以实现自身的OAUTH认证服务。现在业界有OAUTH的多种语言实现,很多大公司也提供了OAUTH认证服务。(如微信扫码认证,这是一种第三方认证的方式,这种认证方式是基于OAuth2协议实现)

  • Spring Security支持OAuth2认证,OAuth2提供 授权码模式、密码模式、简化模式、客户端模式等四种授权模式(微信扫码登录是基于授权码模式),这四种模式中授权码模式和密码模式应用较多。

授权码模式使用例子:

  1. 依赖:

    1
    2
    3
    4
    5
    6
    7
    8
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
  2. 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
    @Configuration
    public class TokenConfig {

    @Autowired
    TokenStore tokenStore;

    // 密钥
    private String SIGNING_KEY = "mq123";

    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;

    // TokenStore是auth2的持久性接口, JwtTokenStore并不实际存储令牌, 而是可以将访问令牌和身份验证相互转换
    @Bean
    public TokenStore tokenStore() {
    return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setSigningKey(SIGNING_KEY);
    return converter;
    }

    //令牌管理服务
    @Bean(name = "authorizationServerTokenServicesCustom")
    public AuthorizationServerTokenServices tokenService() {
    DefaultTokenServices service = new DefaultTokenServices();
    service.setSupportRefreshToken(true); //支持刷新令牌
    service.setTokenStore(tokenStore); //令牌存储策略
    service.setAccessTokenValiditySeconds(7200); //令牌默认有效期2小时
    service.setRefreshTokenValiditySeconds(259200); //刷新令牌默认有效期3天

    TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
    tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
    service.setTokenEnhancer(tokenEnhancerChain);

    return service;
    }

    }

    auth2授权配置:

    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
    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Resource(name = "authorizationServerTokenServicesCustom")
    private AuthorizationServerTokenServices authorizationServerTokenServices;

    @Autowired
    private AuthenticationManager authenticationManager;

    //客户端详情服务
    @Override
    public void configure(ClientDetailsServiceConfigurer clients)
    throws Exception {
    clients
    .inMemory() // 使用in-memory存储
    .withClient("XcWebApp") // client_id
    .secret(new BCryptPasswordEncoder().encode("XcWebApp")) //客户端密钥
    .resourceIds("xuecheng-plus") //资源列表
    // 该client允许的授权类型有authorization_code,password,refresh_token,implicit,client_credentials
    .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
    .scopes("all") //允许的授权范围
    .autoApprove(false) //false表示跳转到授权页面
    .redirectUris("http://www.51xuecheng.cn/") //客户端接收授权码的重定向地址
    ;
    }

    //令牌端点的访问配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    endpoints
    .authenticationManager(authenticationManager) //认证管理器(在security配置类中)
    .tokenServices(authorizationServerTokenServices) //令牌管理服务(在令牌配置类中)
    .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }

    //令牌端点的安全配置
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
    security
    .tokenKeyAccess("permitAll()") //oauth/token_key是公开
    .checkTokenAccess("permitAll()") //oauth/check_token公开
    .allowFormAuthenticationForClients() //表单认证(申请令牌)
    ;
    }

    }
    • AuthorizationServerConfigurerAdapter要求配置以下几个类:
      1. ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),为了不让随便一个客户端都可以随便接入到它的认证服务,服务提供商会给批准接入的客户端一个身份,用于接入时的凭据,有客户端标识和客户端秘钥,在这里配置批准接入的客户端的详细信息。
      2. AuthorizationServerEndpointsConfigurer:用来配置令牌(token)的访问端点和令牌服务(token services)。
      3. AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束.
  3. 测试:

    1
    2
    3
    4
    ### 授权码模式
    ### 第一步申请授权码(浏览器请求)/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.51xuecheng.cn
    ### 第二步申请令牌
    POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=authorization_code&code=CTvCrB&redirect_uri=http://www.51xuecheng.cn

    得到的令牌大致为:

    1
    2
    3
    4
    5
    6
    7
    {
    "access_token": "368b1ee7-a9ee-4e9a-aae6-0fcab243aad2",
    "token_type": "bearer",
    "refresh_token": "3d56e139-0ee6-4ace-8cbe-1311dfaa991f",
    "expires_in": 7199,
    "scope": "all"
    }

密码模式:

密码模式相对授权码模式简单,授权码模式需要借助浏览器供用户亲自授权,密码模式不用借助浏览器。 但是也直接将用户敏感信息泄漏给了client,因此这种模式只能用于client是我们自己开发的情况下。

1
2
### 密码模式
POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=zhangsan&password=123

同一认证入口

一般网站都会支持多种认证方式:账号密码认证、手机验证码认证、扫码登录等,基于当前研究的Spring Security认证实现大致为:

  1. 支持账号和密码认证:采用OAuth2协议的密码模式即可实现。
  2. 支持手机号加验证码认证:用户认证提交的是手机号和验证码,并不是账号和密码。
  3. 微信扫码认证: 基于OAuth2协议与微信交互,网站向微信服务器申请到一个令牌,然后携带令牌去微信查询用户信息,查询成功则用户在学成在线项目认证通过。

由于不同的认证方式提交的数据不一样,比如:手机加验证码方式会提交手机号和验证码,账号密码方式会提交账号、密码、验证码。 对此我们可以在UserDetailsService实现类的loadUserByUsername()方法上作文章,将用户原来提交的账号数据改为提交json数据,json数据可以扩展不同认证方式所提交的各种参数。

实现:

  1. 创建一个DTO类表示认证的参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
    * @description 认证用户请求参数
    */
    @Data
    public class AuthParamsDto {

    private String username; //用户名
    private String password; //域 用于扩展
    private String cellphone;//手机号
    private String checkcode;//验证码
    private String checkcodekey;//验证码key
    private String authType; // 认证的类型 password:用户名密码模式类型 sms:短信模式类型
    private Map<String, Object> payload = new HashMap<>();//附加数据,作为扩展,不同认证类型可拥有不同的附加数据。如认证类型为短信时包含smsKey : sms:3d21042d054548b08477142bbca95cfa; 所有情况下都包含clientId

    }
  2. 定义统一的认证接口,不同的认证方式处理要实现该接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * @description 统一的认证接口
    */
    public interface AuthService {

    /**
    * @param authParamsDto 认证参数
    * @return model.po.XcUser 用户信息
    * @description 认证方法
    */
    XcUserExt execute(AuthParamsDto authParamsDto);

    }
  3. 修改loadUserByUsername,返回用户信息前进行统一认证处理

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
@Service
public class UserServiceImpl implements UserDetailsService {

@Autowired
XcUserMapper xcUserMapper;

@Autowired
XcMenuMapper xcMenuMapper;

@Autowired
ApplicationContext applicationContext;

//传入的请求认证的参数就是AuthParamsDto
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//将传入的json转成AuthParamsDto对象
AuthParamsDto authParamsDto = null;
try {
authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
} catch (Exception e) {
throw new RuntimeException("请求认证参数不符合要求");
}

//认证类型,目前有password,wx, 根据不同的认证类型得到不同的认证接口
String authType = authParamsDto.getAuthType();
//根据认证类型从spring容器取出指定的bean
String beanName = authType + "_authservice";
AuthService authService = applicationContext.getBean(beanName, AuthService.class);

//调用统一execute方法完成认证
XcUserExt xcUserExt = authService.execute(authParamsDto);

//封装xcUserExt用户信息为UserDetails并返回
UserDetails userPrincipal = getUserPrincipal(xcUserExt);
return userPrincipal;
}

/**
* @param xcUser 用户id,主键
* @return model.po.XcUser 用户信息
* @description 查询用户信息
*/
public UserDetails getUserPrincipal(XcUserExt xcUser) {
String password = xcUser.getPassword();
//权限
String[] authorities = {"test"};
//根据用户id查询用户的权限
List<XcMenu> xcMenus = xcMenuMapper.selectPermissionByUserId(xcUser.getId());
if (xcMenus.size() > 0) {
List<String> permissions = new ArrayList<>();
xcMenus.forEach(m -> {
//拿到了用户拥有的权限标识符
permissions.add(m.getCode());
});
//将permissions转成数组
authorities = permissions.toArray(new String[0]);
}

xcUser.setPassword(null);
//将用户信息转json
String userJson = JSON.toJSONString(xcUser);
UserDetails userDetails = User.withUsername(userJson).password(password).authorities(authorities).build();
return userDetails;
}

}
  1. 原来的DaoAuthenticationProvider会进行密码校验,所以需要重新定义DaoAuthenticationProviderCustom类,重写类的additionalAuthenticationChecks方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 重写了DaoAuthenticationProvider的校验的密码的方法,因为要统一认证入口,有一些认证方式不需要校验密码
    @Component
    public class DaoAuthenticationProviderCustom extends DaoAuthenticationProvider {

    // 被实现了的userDetailsService,进行统一认证处理
    @Autowired
    public void setUserDetailsService(UserDetailsService userDetailsService) {
    super.setUserDetailsService(userDetailsService);
    }

    // 取消默认的密码对比
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {

    }
    }

    在配置类WebSecurityConfig类指定daoAuthenticationProviderCustom

    1
    2
    3
    4
    5
    6
    7
    @Autowired
    DaoAuthenticationProviderCustom daoAuthenticationProviderCustom;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(daoAuthenticationProviderCustom);
    }
  2. 具体的校验处理逻辑,如以账号密码为例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    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
    /**
    * @description 账号名密码方式
    */
    @Service("password_authservice")
    public class PasswordAuthServiceImpl implements AuthService {

    @Autowired
    XcUserMapper xcUserMapper;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    CheckCodeClient checkCodeClient;

    @Override
    public XcUserExt execute(AuthParamsDto authParamsDto) {
    //账号
    String username = authParamsDto.getUsername();

    //输入的验证码
    String checkcode = authParamsDto.getCheckcode();
    //验证码对应的key
    String checkcodekey = authParamsDto.getCheckcodekey();

    if (StringUtils.isEmpty(checkcode) || StringUtils.isEmpty(checkcodekey)) {
    throw new RuntimeException("请输入的验证码");
    }

    // 远程调用验证码服务接口去校验验证码
    Boolean verify = checkCodeClient.verify(checkcodekey, checkcode);
    if (verify == null || !verify) {
    throw new RuntimeException("验证码输入错误");
    }

    //账号是否存在
    //根据username账号查询数据库
    XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username));

    //查询到用户不存在,要返回null即可,spring security框架抛出异常用户不存在
    if (xcUser == null) {
    throw new RuntimeException("账号不存在");
    }

    //验证密码是否正确
    //如果查到了用户拿到正确的密码
    String passwordDb = xcUser.getPassword();
    //拿到用户输入的密码
    String passwordForm = authParamsDto.getPassword();
    //校验密码
    boolean matches = passwordEncoder.matches(passwordForm, passwordDb);
    if (!matches) {
    throw new RuntimeException("账号或密码错误");
    }
    XcUserExt xcUserExt = new XcUserExt();
    BeanUtils.copyProperties(xcUser, xcUserExt);

    return xcUserExt;
    }
    }

常见问题

跨域

如果请求的协议、主机或端口不同,会被CORS policy阻止,因为没有Access-Control-Allow-Origin 头信息。CORS全称是 cross origin resource share 表示跨域资源共享。

出现这个问题的原因是基于浏览器的同源策略,去判断是否跨域请求,同源策略是浏览器的一种安全机制,从一个地址请求另一个地址,如果协议、主机、端口三者全部一致则不属于跨域,否则有一个不一致就是跨域请求。

比如:

从http://localhost:8601 到 http://localhost:8602 由于端口不同,是跨域。

从http://192.168.101.10:8601 到 http://192.168.101.11:8601 由于主机不同,是跨域。

从http://192.168.101.10:8601 到 https://192.168.101.10:8601 由于协议不同,是跨域。

  • 注:服务器之间不存在跨域请求。

浏览器判断是跨域请求会在请求头上添加origin,表示这个请求来源哪里。比如:

1
2
GET / HTTP/1.1   
Origin: http://localhost: 8601

服务器收到请求判断这个Origin是否允许跨域,如果允许则在响应头中说明允许该来源的跨域请求,如下:

1
Access-Control-Allow-Origin: http://localhost:8601  

如果允许任何域名来源的跨域请求,则响应如下:

1
Access-Control-Allow-Origin:*  

解决跨域的方法:

  1. JSONP

    通过script标签的src属性进行跨域请求,如果服务端要响应内容则首先读取请求参数callback的值,callback是一个回调函数的名称,服务端读取callback的值后将响应内容通过调用callback函数的方式告诉请求方。如下图:

  2. 添加响应头

    服务端在响应头添加 Access-Control-Allow-Origin:*, 设置一个过滤器即可:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Configuration
    public class GlobalCorsConfig {

    @Bean
    public CorsFilter corsFilter(){

    CorsConfiguration config = new CorsConfiguration();
    config.addAllowedOrigin("*"); // 允许跨域调用的白名单
    config.addAllowedHeader("*"); // 放行全部头信息
    config.addAllowedMethod("*"); // 允许所有请求方法跨域调用
    config.setAllowCredentials(true); // 允许跨域发送cookie

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config); // 允许所有请求都走过滤器

    return new CorsFilter(source);
    }
    }
  3. 通过nginx代理跨域

    由于服务端之间没有跨域,浏览器通过nginx去访问跨域地址。

JSR303校验

前端请求的参数的校验,在Controller和Service中都需要校验:

  • Contoller中校验请求参数的合法性,包括:必填项校验,数据格式校验,比如:是否是符合一定的日期格式,等。
  • Service中要校验的是业务规则相关的内容,比如:课程已经审核通过所以提交失败。

其中Contoller中的校验可以写成通用代码,而 JSR-303 是在JavaEE6规范中就定义了的参数校验的规范,它定义了Bean Validation,即对bean属性进行校验。

SpringBoot提供了JSR-303的支持,即spring-boot-starter-validation,它的底层使用Hibernate Validator,Hibernate Validator是Bean Validation 的参考实现。

基本校验规则:

  • 空检查
    @Null 验证对象是否为null
    @NotNull 验证对象是否不为null, 无法查检长度为0的字符串
    @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
    @NotEmpty 检查约束元素是否为NULL或者是EMPTY.

  • Booelan检查
    @AssertTrue 验证 Boolean 对象是否为 true
    @AssertFalse 验证 Boolean 对象是否为 false

  • 长度检查
    @Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
    @Length(min=, max=) Validates that the annotated string is between min and max included.

  • 日期检查
    @Past 验证 Date 和 Calendar 对象是否在当前时间之前,验证成立的话被注释的元素一定是一个过去的日期
    @Future 验证 Date 和 Calendar 对象是否在当前时间之后 ,验证成立的话被注释的元素一定是一个将来的日期
    @Pattern 验证 String 对象是否符合正则表达式的规则,被注释的元素符合制定的正则表达式,regexp:正则表达式 flags: 指定 Pattern.Flag 的数组,表示正则表达式的相关选项。

  • 数值检查
    建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为”“,Integer为null
    @Min 验证 Number 和 String 对象是否大等于指定的值
    @Max 验证 Number 和 String 对象是否小等于指定的值
    @DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
    @DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
    @Digits 验证 Number 和 String 的构成是否合法
    @Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
    @Range(min=, max=) 被指定的元素必须在合适的范围内
    @Range(min=10000,max=50000,message=”range.bean.wage”)
    @Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证)
    @CreditCardNumber信用卡验证
    @Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。
    @ScriptAssert(lang= ,script=, alias=)
    @URL(protocol=,host=, port=,regexp=, flags=)

使用:

  1. 添加依赖:

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
  2. 直接使用注解定义校验规则,并在方法对应参数上添加**@Validated**以开启校验,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @Data
    @ApiModel(value="AddCourseDto", description="新增课程基本信息")
    public class AddCourseDto {

    @NotEmpty(message = "课程名称不能为空")
    @ApiModelProperty(value = "课程名称", required = true)
    private String name;

    @NotEmpty(message = "适用人群不能为空")
    @Size(message = "适用人群内容过少",min = 10)
    @ApiModelProperty(value = "适用人群", required = true)
    private String users;

    @NotEmpty(message = "课程分类不能为空")
    @ApiModelProperty(value = "大分类", required = true)
    private String mt;

    @NotEmpty(message = "课程分类不能为空")
    @ApiModelProperty(value = "小分类", required = true)
    private String st;

    ...
    }
    1
    2
    3
    4
    5
    6
    7
    @ApiOperation("新增课程基础信息")
    @PostMapping("/course")
    public CourseBaseInfoDto createCourseBase(@RequestBody @Validated AddCourseDto addCourseDto){
    //机构id,由于认证系统没有上线暂时硬编码
    Long companyId = 1L;
    return courseBaseInfoService.createCourseBase(companyId,addCourseDto);
    }

分组校验:

有时候在同一个属性上设置一个校验规则不能满足要求,比如:订单编号由系统生成,在添加订单时要求订单编号为空,在更新订单时要求订单编写不能为空。

于是可以用分组校验,如:添加订单定义@NULL规则属于insert分组,更新订单定义@NotEmpty规则属于update分组。

使用:

  1. 定义不同的接口类型(空接口)表示不同的分组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /**
    * 校验分组
    */
    public class ValidationGroups {

    public interface Inster{};
    public interface Update{};
    public interface Delete{};
    }
  2. 在定义校验规则时,指定分组:groups = {xxx.class}

    1
    2
    3
    4
    @NotEmpty(groups = {ValidationGroups.Inster.class},message = "添加课程名称不能为空")
    @NotEmpty(groups = {ValidationGroups.Update.class},message = "修改课程名称不能为空")
    @ApiModelProperty(value = "课程名称", required = true)
    private String name;
  3. 在Controller方法中启动校验规则指定要使用的分组名:

    1
    2
    3
    4
    5
    6
    @PostMapping("/course")
    public CourseBaseInfoDto createCourseBase(@RequestBody @Validated({ValidationGroups.Inster.class}) AddCourseDto addCourseDto){
    //机构id,由于认证系统没有上线暂时硬编码
    Long companyId = 1L;
    return courseBaseInfoService.createCourseBase(companyId,addCourseDto);
    }

自定义校验规则:

….

RBAC授权

业界通常基于RBAC实现授权。

RBAC分为两种方式:

  • 基于角色的访问控制(Role-Based Access Control)

    • 这种是按角色进行授权,比如:主体的角色为总经理可以查询企业运营报表,查询员工工资信息等,如:

      1
      2
      3
      if(主体.hasRole("总经理角色id")){
      查询工资
      }

      如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断用户的角色是否是总经理或部门经理”,修改代码如下:

      1
      2
      3
      if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")){
      查询工资
      }
  • 基于资源的访问控制(Resource-Based Access Control)

    • 这种是按资源(或权限)进行授权,比如:用户必须具有查询工资权限才可以查询员工工资信息等,如:

      1
      2
      3
      if(主体.hasPermission("查询工资权限标识")){
      查询工资
      }

上述可以看出,基于资源的访问控制的可扩展性会好些

RBAC权限模型,数据库表包含:

  1. 用户表:每个用户都有唯一的UID识别,并被授予不同的角色
  2. 角色表:不同角色具有不同的权限
  3. 权限表
  4. 用户-角色关联表
  5. 角色-权限关联表