spring boot 入门

70 阅读26分钟

Spring-boot

hello World

添加依赖

添加maven依赖

<!-- 添加父工程,父工程主要是做版本控制和依赖管理--> 
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.4.RELEASE</version>
</parent><!-- web项目添加依赖,只需要一个web其他会自动导入 --> 
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

部署

maven中添加插件,然后打成jar包,然后使用java -jar 运行项目即可

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

配置

修改依赖版本

如果需要手动修改可以在pom.xml添加对应依赖的版本号

<properties>
    <mysql.version>5.1.43</mysql.version>
</properties><dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>
程序入口
//注解表示是springboot的主类、入口
@SpringBootApplication
public class MySpringBootApplication {
    public static void main(String[] args) {
        //得到IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MySpringBootApplication.class,args);
        //获得springContext添加的所有组件的名称
        String[] beanDefinitionNames = run.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            System.out.println(beanDefinitionName);
        }
    }
}
springboot自动配置流程
  • 自动配置好Tomcat

    • 引入依赖
    • 配置Tomcat
  • 自动配置好springmvc常用功能如字符编码

    • SpringBoot帮我们配置好了所有web开发的常见场景
  • 自动扫描包

    • 主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来

    • 无需以前的包扫描配置

    • 想要改变扫描路径

      • @SpringBootApplication(scanBasePackages="com.lun")
      • @ComponentScan 指定扫描路径
  • 各种配置拥有默认值

    • 默认配置最终都是映射到某个类上,如:MultipartProperties
    • 配置文件的值最终会绑定每个类上,这个类会在容器中创建对象
总结

SpringBoot默认会在底层配好所有的组件,但是如果用户自己配置了以用户的优先

  • SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration

  • 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。(xxxxProperties里面读取,xxxProperties和配置文件进行了绑定)

  • 生效的配置类就会给容器中装配很多组件

  • 只要容器中有这些组件,相当于这些功能就有了

  • 定制化配置

    • 用户直接自己@Bean替换底层的组件
    • 用户去看这个组件是获取的配置文件什么值就去修改。

xxxxxAutoConfiguration ---> 组件 ---> xxxxProperties里面拿值 ----> application.properties

查看自动配置报告

查看自动配置了哪些

  • application.properties配置文件中debug=true开启自动配置报告。

    • Negative(不生效)
    • Positive(生效)

注解,spring配置类型注解

@Configuration

使用Configuration配置的类默认是单例的

  • proxyBeanMethods修改代理bean的方式

    • Full模式:proxyBeanMethods=true则是单例模式
    • Lite模式:proxyBeanMethods=false则每次返回的都是一个新对象
  • 配置 类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断

  • 配置 类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式(默认)

/**
 * 1、配置类里面使用@Bean标注在方法上给容器注册组件,默认也是单实例的
 * 2、配置类本身也是组件
 * 3、proxyBeanMethods:代理bean的方法
 *      Full(proxyBeanMethods = true)(保证每个@Bean方法被调用多少次返回的组件都是单实例的)(默认)
 *      Lite(proxyBeanMethods = false)(每个@Bean方法被调用多少次返回的组件都是新创建的)
 */
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {
​
    /**
     * Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象
     * @return
     */
    @Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值,就是组件在容器中的实例
    public User user01(){
        User zhangsan = new User("zhangsan", 18);
        //user组件依赖了Pet组件
        zhangsan.setPet(tomcatPet());
        return zhangsan;
    }
​
    @Bean("tom")
    public Pet tomcatPet(){
        return new Pet("tomcat");
    }
}
@ComponentScan

添加扫描包的范围

@ComponentScan("edu.mkit.test")
@import

给容器中自动创建出这个类型的组件、默认组件的名字就是全类名

@Import({User.class, DBHelper.class})//直接添加这个类型的组件
@Configuration //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {
}
@Conditional条件装配

条件装配:满足Conditional指定的条件,则进行组件注入

  • ConditionalOnProperty:控制配置类是否生效,可以将配置与代码进行分离,实现了更好的控制配置
  • ConditionalOnResource
  • ConditionalOnBean
  • ConditionalOnClass
  • ConditionalOnMissingBean
  • ConditionalOnMissingClass
//ConditionalOnBean在容器中存在tom的时候user才会注入,如果tom不存在那么user也不存在
@ConditionalOnBean(name="tom")
@Bean
public User user(){
    User zhangsan = new User("zhangsan", 18);
    zhangsan.setPet(tomcatPet());
    return zhangsan;
}
​
@Bean("tom")
public Pet tomcatPet(){
    return new Pet("tomcat");
}
@importResource

导入Spring配置文件,即可使用原来的xml

@ImportResource("classpath:beans.xml")
public class MyConfig {
...
}
@ConfigurationProperties

读取到properties文件中的内容,并且把它封装到JavaBean中

car.brand=BMW
car.price=100000
@Component
@ConfigurationProperties(prefix = "car")
//prefix是前缀-> new Car().price = car.price

@MatrixVariable矩阵变量

web请求

拦截器

  1. 自定义拦截器

    自定义拦截器,即拦截器的实现类,一般有两种自定义方式:

    定义一个类,实现org.springframework.web.servlet.HandlerInterceptor接口。

    定义一个类,继承已实现了HandlerInterceptor接口的类,例如org.springframework.web.servlet.handler.HandlerInterceptorAdapter抽象类。

    //实现HandlerInterceptor 接口
    //preHandle:在业务处理器处理请求之前被调用。预处理,可以进行编码、安全控制、权限校验等处理;
    boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    		throws Exception;
    //postHandle:在业务处理器处理请求执行完成后,生成视图之前执行。后处理(调用了Service并返回ModelAndView,但未进行页面渲染),有机会修改ModelAndView 
    void postHandle(
    		HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
    		throws Exception;
    
    //在DispatcherServlet完全处理完请求后被调用,可用于清理资源等。返回处理(已经渲染了页面);
    void afterCompletion(
    		HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
    		throws Exception;
    
  2. 把拦截器添加到WebMvcConfiguration中

    实现addInterceptors方法,在方法中添加

    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // addPathPatterns(/**) 拦截所有请求,excludePathPatterns("")放行请求
        registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**").excludePathPatterns("");
    ​
    }
    

Converter转换器

使用场景:

前端请求url为:http://127.0.0.1:8080/address?address=四川/成都/青羊区如何使用对象进行接收

使用转换器

// 实体类
@Data
@ToString
public class Address {
 private String province;
 private String city;
 private String area;
}
​
// controller
@Controller
public class AddressController {
 @ResponseBody
 @GetMapping("/address")
 public Address address(Address address){
     return address;
 }
}
​
​
//转换器
@Configuration
public class Config implements WebMvcConfigurer {
 @Override
 public void addFormatters(FormatterRegistry registry) {
     registry.addConverter(new Converter<String,Address>(){
         @Override
         public Address convert(String source) {
             String[] split = source.split("/");
             Address address = new Address();
             address.setProvince(split[0]);
             address.setCity(split[1]);
             address.setArea(split[2]);
             return address;
         }
     });
 }
​

数据响应与内容协商

内容协商

springboot根据请求头Accept:text/html,application/xhtml+xml,application/xml

来确定返回给客户端的内容是什么类型

请求处理

请求映射

REST风格

springboot默认关闭了rest风格,开启需要添加配置,RequestMapping里的参数是区分大小写的,所以写的时候最好是全小写

spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true   #开启页面表单的Rest功能

Rest原理(表单提交要使用REST的时候)

  • 表单提交会带上_method=PUT

  • 请求过来被HiddenHttpMethodFilter拦截

    • 请求是否正常,并且是POST

      • 获取到_method的值。
      • 兼容以下请求;PUT.DELETE.PATCH
      • 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。
      • 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。
修改默认的_methd
@Configuration(proxyBeanMethods = false)//Lite(多例)模式
public class WebConfig{
    //自定义filter
    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
        //创建自己的HiddenHttpMethodFilter添加到IOC容器中
        HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
        methodFilter.setMethodParam("_m");//把_methd修改为_m
        return methodFilter;
    }    
}

请求映射原理

SpringMVC所有请求的入口 org.springframework.web.servlet.DispatcherServlet -> doDispatch()

所有的请求映射都在HandlerMapping中:

  • SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;

  • SpringBoot自动配置了默认 的 RequestMappingHandlerMapping

  • 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。

    • 如果有就找到这个请求对应的handler
    • 如果没有就是下一个 HandlerMapping
  • 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping。自定义 HandlerMapping

请求处理注解-常用参数注解

注解:

  • @PathVariable 路径变量

    获取请求路径占位符的值

    @RequestMapping("test/{id}}")
    public String test5(@PathVariable("id") Long id ,){
        // id为请求占位符的id
        return "";
    }
    ​
    
  • @RequestHeader 获取请求头

  • @RequestParam 获取请求参数(指问号后的参数,url?a=1&b=2)[GET]

    @GetMapping("/greeting")
    public String greeting(@RequestParam(name="name", required=false, defaultValue="World") String name, Model model) {
        //name参数和value参数基本等价,都是获取前端传入的数据
        //使用 @RequestParam 注解将 HTTP 请求中名为 name 的参数映射到 String 类型的 name 参数上,required=false 表示该参数不是必需的,默认值为 "World".
        //如果没有设置默认值,且required为true那么如果请求中没有包含name这个参数会抛出异常
        model.addAttribute("name", name);
        return "greeting";
    }
    
  • @CookieValue 获取Cookie值

  • @RequestAttribute 获取request域属性

    //在形参中直接使用requestAttribute可以省去在方法体内使用request.getRequestAttribute获取这一步
    public String index(@requestAttribute("msg") String msg){
        
    }
    
  • @RequestBody 获取请求体[POST]

  • @MatrixVariable 矩阵变量,springboot默认关闭

    一般,矩阵变量会应用到这样的场景。在页面开发中,如果Cookie被禁用了,该如何获得Session中的数据?

    如果使用Cookie,通过获取Cookie中的JSessionID,找到Session对象,从而获取该Session中的数据。而Cookie被禁用了,就无法获取JSessionID,也就不能用这种方法获取Session的数据。这时,我们可以使用url重写的方式解决这一问题,比如这样的url:/abc;jsessionid=xxxxx,也就是将Cookie的值使用矩阵变量的方式传递

  • @ModelAttribute

数据校验

校验注解

注解验证的数据类型说明
Null所有类型验证元素值必须为 null
NotNull所有类型验证元素值必须不为 null
NotBlankCharSequence验证元素值不能为 null,并且至少包含一个非空白字符。
NotEmptyCharSequence、Collection、Map、Array验证元素值不能为 null,且不能为空
Size(min = min, max = max)同 NotEmpty验证元素的 size 必须在 min 和 max 之间(包含边界),认为 null 是有效的
AssertFalseboolean、Boolean验证元素值必须为 false,认为 null 是有效的
AssertTrue同 AssertFalse验证元素值必须为 true,认为 null 是有效的
DecimalMax(value=, inclusive=)BigDecimal、BigInteger、CharSequence,byte、 short、int、long 及其包装类型,由于舍入错误,不支持double和float验证元素值必须小于等于指定的 value 值,认为 null 是有效的
DecimalMin同 DecimalMax验证元素值必须大于等于指定的 value 值,认为 null 是有效的
Max同 DecimalMax,不支持CharSequence验证元素值必须小于等于指定的 value 值,认为 null 是有效的
Min同 DecimalMax,不支持CharSequence验证元素值必须大于等于指定的 value 值,认为 null 是有效的
Digits(integer =, fraction =)同 DecimalMax验证元素整数位数的上限 integer 与小数位数的上限 fraction,认为 null 是有效的
PositiveBigDecimal、BigInteger,byte、short、int、long、float、double 及其包装类型验证元素必须为正数,认为 null 是有效的
PositiveOrZero同Positive验证元素必须为正数或 0,认为 null 是有效的
Negative同Positive验证元素必须为负数,认为 null 是有效的
NegativeOrZero同Positive验证元素必须为负数或 0,认为 null 是有效的
FutureDate、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate验证元素值必须是一个将来的时间,认为 null 是有效的
FutureOrPresent同 Future验证元素值必须是当前时间或一个将来的时间,认为 null 是有效的
Past同 Future验证元素值必须是一个过去的时间,认为 null 是有效的
PastOrPresent同 Future验证元素值必须是当前时间或一个过去的时间,认为 null 是有效的
Email(regexp = 正则表达式,flag = 标志的模式)CharSequence验证注解的元素值是Email,可以通过 regexp 和 flag 指定自定义的 email 格式,认为 null 是有效的
Pattern同 Email验证元素值符合正则表达式,认为 null 是有效的
案例

!!注意!!

如果只在model类里写了注解,但是在请求上没有开启功能,注解还是不会生效,必须在Control类上加上@Valid注解才有用

public class BrandEntity implements Serializable {
    
    @TableId
    @Null(message = "新增时必须为空")
    private Long brandId;
    
    @NotBlank(message = "品牌名不能为空")
    private String name;
​
    @URL(message = "logo必须是url地址")
    @NotEmpty(message = "不能为空")
    private String logo;
}
​
@RequestMapping("/info/{brandId}")
public R info(@Valid @PathVariable("brandId") Long brandId){
    BrandEntity brand = brandService.getById(brandId);
​
    return R.ok().put("brand", brand);
}
分组校验

在不同的请求下,需要不同的注解来校验数据,

如自增id字段,在新增接口中,则不需要校验该数据,但是在更新数据时,则该字段不能为空。

所以需要分组校验。

分组时,必须在Control接口指定当前的分组,且还需要提前准备好分组接口(接口什么都不写, 只需要一个空接口)。

如果在Control指定了分组,而校验没有指定分组的话,则该校验会失效

public class BrandEntity implements Serializable {
    
    @TableId
    @Null(message = "新增时必须为空",groups = {AddGroup.class})
    @NotNull(message = "修改时必须有该字段",groups = {UpdateGroup.class})
    private Long brandId;
    
    @NotBlank(message = "品牌名不能为空")
    // 如果在Control指定了分组,如果没有写,则不会识别到该校验
    private String name;
​
    @URL(message = "logo必须是url地址")
    @NotEmpty(message = "不能为空")
    private String logo;
}
​
@RequestMapping("/save")
//@RequiresPermissions("product:brand:save")
public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand){
    brandService.save(brand);
​
    return R.ok();
}
​
// 接口内容可以什么都不写,只是单纯使用名字分组的
public interface AddGroup {
}
​
自定义校验
  1. 自定义一个校验注解
  2. 实现自定义注解的自定义校验器
  3. 添加配置文件,校验失败提示信息

一个校验注解,可以由多个校验器实现

package com.yibai.common.valid;
​
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
​
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
​
​
// 自定义注解
@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
​
    String message() default "{com.yibai.common.valid.ListValue.message}";
​
    Class<?>[] groups() default { };
​
    Class<? extends Payload>[] payload() default { };
​
    int[] vals() default { };
​
}
​
​
package com.yibai.common.valid;
​
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
​
// 自定义校验器
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
​
    private Set<Integer> set = new HashSet<>();
​
    /**
     * 初始化方法
     * @param constraintAnnotation
     */
    @Override
    public void initialize(ListValue constraintAnnotation) {
​
        int[] vals = constraintAnnotation.vals();
​
        for (int val : vals) {
            set.add(val);
        }
​
    }
​
    /**
     * 判断是否效验成功
     * @param value 需要效验的值
     * @param context
     * @return
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
​
        //判断是否有包含的值
        boolean contains = set.contains(value);
​
        return contains;
    }
​
}
​
public class BrandEntity implements Serializable {
    /**
     * 显示状态[0-不显示;1-显示]
     */
    @ListValue(vals = {0,1},message = "显示状态必须是0或1")
    private Integer showStatus;  
}
​
# 文件名 ValidationMessages.properties 
com.yibai.common.valid.ListValue.message=必须是指定的值

简化开发

lombok

开发过程中用注解的方式,简化了 JavaBean 的编写,避免了冗余和样板式代码而出现的插件,让编写的类更加简洁。

添加依赖

 <dependency>
     <groupId>org.projectlombok</groupId>
     <artifactId>lombok</artifactId>
</dependency>
@NoArgsConstructor //无参构造器
@AllArgsConstructor //全参构造器,如果需要其他单独的构造器需要自己重新写
@Data //get&set方法
@ToString //tostring
@EqualsAndHashCode //equals和hashcode
@Slf4j //添加后可以使用(log.info("test...");)直接输出日志信息,而不用sout,

伪热更新(自动重启项目)dev-tools

在IDEA中,项目或者页面修改以后:Ctrl+F9。

添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
</dependency>

yaml

YAML 是 "YAML Ain't Markup Language"(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)。

yaml代码提示插件
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<!-- 下面插件作用是工程打包时,不将spring-boot-configuration-processor打进包内,让其只在编码的时候有用 -->
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <excludes>
                    <exclude>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-configuration-processor</artifactId>
                    </exclude>
                </excludes>
            </configuration>
        </plugin>
    </plugins>
</build>
基本语法
  • key: value;kv之间有空格
  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • '#'表示注释
  • 字符串无需加引号,如果要加,单引号''、双引号""表示字符串内容会被 转义、不转义
  • 大写字母前面可以使用-加小写字母代替如:M-》-m
数据类型

字面量:单个的、不可再分的值。date、boolean、string、number、null

对象

键值对的集合。map、hash、set、object ,冒号后面需要一个空格在跟上具体的数值

#行内写法:  
k: {k1:v1,k2:v2,k3:v3}
#或
k: 
  k1: v1
  k2: v2
  k3: v3
数组

一组按次序排列的值。array、list、queue

#行内写法:  
k: [v1,v2,v3]
#或者
k:
 - v1
 - v2
 - v3
示例

java

public class Person {
    private String userName;
    private Boolean boss;
    private Date birth;
    private Integer age;
    private Pet pet;
    private String[] interests;
    private List<String> animal;
    private Map<String, Object> score;
    private Set<Double> salarys;
    private Map<String, List<Pet>> allPets;
}
public class Pet {
    private String name;
    private Double weight;
}

yaml

person:
  userName: zhangsan
  boss: false
  birth: 2019/12/12 20:12:33
  age: 18
  pet: 
    name: tomcat
    weight: 23.4
  interests: [篮球,游泳]
  animal: 
    - jerry
    - mario
  score:
    english: 
      first: 30
      second: 40
      third: 50
    math: [131,140,148]
    chinese: {first: 128,second: 136}
  salarys: [3999,4999.98,5999.99]
  allPets:
    sick:
      - {name: tom}
      - {name: jerry,weight: 47}
    health: [{name: mario,weight: 47}]

静态资源

静态资源目录

只要静态资源放在类路径下: called /static (or /public or /resources or /META-INF/resources

访问 : 当前项目根路径/ + 静态资源名

原理: 静态映射/**。

请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面。

也可以改变默认的静态资源路径,/static/public,/resources, /META-INF/resources失效

# 修改默认静态资源路径
#2.5.6新版本这个弃用了,有一个新的方式,web:resources:static-locations:[classpath:/newresource/]
resources:
  static-locations: [classpath:/newresource/]

添加静态资源访问前缀

spring:
  mvc:
    static-path-pattern: /res/**

静态资源访问方式:当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找

webjar

可用jar方式添加css,js等资源文件,

www.webjars.org/

例如,添加jquery

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.5.1</version>
</dependency>

访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面地址要按照依赖里面的包路径。

自定义web配置

默认配置

springboot的默认web配置在WebMVCAutoConfiguration类中

Security

Spring Security是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制。

关于安全方面的两个主要区域是“认证”和“授权”(或者访问控制),一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是 Spring Security 重要核心功能。

1、用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录。

2、用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。

过滤器

security本质就是一个过滤器链,通过一个个过滤器来进行安全验证,Security 采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个过滤器

SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的 15 个过滤器进行说明:

(1) WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。

(2) SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder 中的信息清除,例如Session 中维护一个用户的安全信息就是这个过滤器处理的。

(3) HeaderWriterFilter:用于将头信息加入响应中。

(4) CsrfFilter:用于处理跨站请求伪造。

(5)LogoutFilter:用于处理退出登录。

(6)UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的 usernameParameter 和 passwordParameter 两个参数的值进行修改。

(7)DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。

(8)BasicAuthenticationFilter:检测和处理 http basic 认证。

(9)RequestCacheAwareFilter:用来处理请求的缓存。

(10)SecurityContextHolderAwareRequestFilter:主要是包装请求对象 request。

(11)AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在Authentication 对象,如果不存在为其提供一个匿名 Authentication。

(12)SessionManagementFilter:管理 session 的过滤器

(13)ExceptionTranslationFilter:处理 AccessDeniedException 和AuthenticationException 异常。

(14)FilterSecurityInterceptor:可以看做过滤器链的出口。

(15)RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie里找出用户的信息, 如果 Spring Security 能够识别出用户提供的 remember me cookie,用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

重要过滤器

UsernamePasswordAuthenticationFilter 过滤器:该过滤器会拦截前端提交的 POST 方式的登录表单请求,并进行身份认证。

ExceptionTranslationFilter 过滤器:该过滤器不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。

FilterSecurityInterceptor 过滤器:该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并 由 ExceptionTranslationFilter 过滤器进行捕获和处理。

控制器

AuthenticationFailureHandler //认证失败处理器

AuthenticationSuccessHandler //认证成功处理器

LogoutSuccessHandler //退出成功处理器

MkAccessDenyHandler//访问拒绝处理器

SecurityConfig

authorizeRequests //URL访问控制授权

antMatchers //使用ant表达式匹配URL(里面可以写多个请求)

regexMatchers()//使用正则表达式匹配请求

anyRequest //全部请求

denyAll() //禁止所有请求,或上一个方法匹配的请求

permitAll //上一个方法匹配到的请求,或直接通过所有请求

authenticated //匹配的请求需要登录才能访问

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //拦截请求
        http.
                authorizeRequests()
                //允许login底下的请求访问
                .antMatchers("/login/**").permitAll()
                //禁止所有请求访问
                .anyRequest().denyAll();
​
​
    }
}

security 注解

security默认是不开启注解功能的,如果想要使用,需要在springboot的启动类上添加@EnableGlobalMethodSecurity注解,开启security的注解使用

@enableglobalmethodsecurity()

  • prepostenabled:是否启用 spring security pre/post annotations。默认值为 false。
  • securedenabled:是否启用使用 @secured 注释进行授权。默认值为 false。
  • jsr250enabled:是否启用 jsr-***注解进行授权。默认值为 false。
  • proxytargetclass:是否要使用基于子类的(cglib)代理或基于接口的代理。默认为false,意味着使用基于接口的代理
// 先决条件授权,要求在进入方法之前执行授权检查。,可以将登录用户的 roles / permissions 参数 传到方法中
@PreAuthorize()
// 后置授权,允许在退出方法时执行授权检查
@PostAuthorize()
// 要求用户具有指定的角色才能访问方法。需要注意匹配的字符串需要增加前缀 ROLE_。
@Secured
// 权限验证通过后,留下指定用户名的数据。对返回数据做过滤
@PostFilter
// 对传递参数值做过滤。
@PreFilter

spring cache缓存

Spring 定义了CacheManager和Cache接口来提供缓存的抽象,可以通过切换的CacheManager的实现类,来使用不同的缓存技术。CacheManager的实现有:ConcurrentMapCacheManager、EhCacheCacheManager、RedisCacheManager等。

只有public方法才可以被缓存

  • @Cacheable :触发将数据保存到缓存的操作;
  • @CacheEvict : 触发将数据从缓存删除的操作;
  • @CachePut :不影响方法执行更新缓存;
  • @Cacheing:组合以上多个操作;
  • @CacheConfig:在类级别共享缓存的相同配置;
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
spring:
  cache:
    type: redis  # 使用Redis作为缓存
    redis:
      time-to-live: 3600000  # 缓存过期时间为3600000毫秒(1小时)
      use-key-prefix: true  # 使用key前缀
      cache-null-values: true  # 缓存null值

配置一个自定义security登录过滤器

该过滤器不包含userdetails,使用到的类有。

过滤器的链

filter用于接收网络请求把数据封装到token中,调用manager进行认证

manager管理多个provider,选择合适的provider进行认证

provider负责认证,返回认证后token

token存储认证信息handler负责结果

handler负责token认证的结果处理

// 负责拦截请求并把获取的参数包装成AuthenticationToken交给token指定的provider处理
org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
// 负责封装数据,分别有两个构造器,一个构造器是校验前使用,也就是上面过滤器第一次构造的token,第二个构造器由provider提供,校验后使用
org.springframework.security.authentication.AbstractAuthenticationToken;
// 登录成功后处理的handler,注意,这个handler要配置到自定义的filter上,http对象添加是不生效的
org.springframework.security.web.authentication.AuthenticationSuccessHandler;
// 认证失败后的handler
org.springframework.security.web.authentication.AuthenticationFailureHandler;
// 权限不足,未验证的handler
org.springframework.security.web.access.AccessDeniedHandler;
// 配置类
org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
// 单次fliter,可以单独在某个指定过滤器前进行处理,
org.springframework.web.filter.OncePerRequestFilter;

WebSecurityConfigurerAdapter

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 通过配置http来构造filter,
        // 可以进行csrf防护的配置,请求拦截的配置。
    }
​
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 可以进行权限管理,添加自定义的provider
        auth.authenticationProvider(emailAuthenticationProvider);
    }
​
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        // 必须重写,要不然自定义的filter传入的manager会空指针。
        return super.authenticationManagerBean();
    }
    // 注入自定义的provider
    @Bean
    EmailAuthenticationProvider emailAuthenticationProvider(){
        EmailAuthenticationProvider emailAuthenticationProvider = new EmailAuthenticationProvider();
        return emailAuthenticationProvider;
    }
    //注入密码编码器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    // 如果还有需要的@bean或@autowrite可以自行注入,需要注意的是如果需要进行一些配置的bean不能则直接使用autowrite注入,需要通过bean构造后注入。

AbstractAuthenticationProcessingFilter //自定义的filte

//拦截自定义的请求,构造未验证的token,交给provider
public class EmailAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
​
    // 是否只能post请求
    private boolean postOnly = true;
​
    private AuthenticationManager authenticationManager;
    
    //执行父方法的构造器,指定拦截的请求和方法
    public EmailAuthenticationFilter() {
        //super也是必须要执行的
        super(new AntPathRequestMatcher("/login/email", "POST"));
    }
    
    //拦截到指定请求,进行封装token
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        
        //是否post请求
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
​
        //获取请求信息
        String email = request.getParameter("userEmail");
        String password = request.getParameter("password");
​
        if (StringUtils.isEmpty(email) || StringUtils.isEmpty(password) ) {
            throw new UserException(ResultCodeEnum.ARGUMENT_VALID_ERROR);
        }
​
        /**
         * 使用请求参数传递的邮箱和验证码,封装为一个未认证 EmailVerificationCodeAuthenticationToken 身份认证对象,
         * 然后将该对象交给 AuthenticationManager 进行认证
         */
        EmailAuthenticationToken emailAuthenticationToken = new EmailAuthenticationToken(email,password);
​
        //返回构造的未验证token
        return this.getAuthenticationManager().authenticate(emailAuthenticationToken);
    }
}

AuthenticationProvider

//接收指定token进行认证处理
public class EmailAuthenticationProvider implements AuthenticationProvider, ProviderStrategy {
​
​
    @Resource
    private LoginService loginService;
​
    @Resource
    private BCryptPasswordEncoder passwordEncoder;
​
    @Override
    @Deprecated
    public Authentication authenticate(HttpServletRequest request) {
       //这个方法好像没用,filter拦截已经创建了token,
        //通过请求创建一个未通过验证token
        String email = request.getParameter("email");
        String password = request.getParameter("password");
        return new EmailAuthenticationToken(email, password);
    }
​
​
    /**
     * 验证传入的authentication 认证通过返回认证对象,失败返回null
     *
     * @param authentication the authentication request object.
     * @return 校验authentication,如果正确则返回校验后的token,错误则抛出异常。
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        EmailAuthenticationToken emailToken = (EmailAuthenticationToken) authentication;
        // 检查对象是否为空
        if (emailToken == null || StringUtils.isEmpty(emailToken.getPrincipal().toString()) || StringUtils.isEmpty(emailToken.getCredentials().toString())) {
            return null;
        }
        User user = loginService.getUserByEmail(emailToken.getPrincipal().toString());
        // 如果获取的用户不为空那么进行密码校验
        if (user!=null){
            // 使用密码管理器进行比较,如果为false,或者抛出密码错误异常
            if (!passwordEncoder.matches(emailToken.getCredentials().toString(),user.getPassWord())){
                throw new UserException(ResultCodeEnum.PASSWORD_ERROR);
            }
            //TODO 权限目前没有设置所有 为null
            //构造一个认证后的token返回给manager,交由给successHandler进行处理
            return new EmailAuthenticationToken(user.getUserEmail(),user.getPassWord(),null);
        }else{
            throw new UserException(ResultCodeEnum.ACCOUNT_EMAIL_ERROR);
        }
    }
​
    @Override
    public boolean supports(Class<?> authentication) {
        //Manager传递token给provider,调用本方法判断该provider是否支持该token。不支持则尝试下一个provider
        //本类支持的token类:EmailAuthenticationToken
        return (EmailAuthenticationToken.class.isAssignableFrom(authentication));
    }
}
​

整合redis

配置文件

spring:
  redis:
    database: 1
    # Redis 服务器地址
    host: 127.0.0.1
    # Redis 服务器连接端口
    port: 6379
    #连接池最大连接数《使用负值表示没有限制),默认为8
    jedis:
      pool:
        max-active: 8
        # 连接池最大阻塞等待时间(使用负值表示没有限制),默认为-1
        max-wait: -1
        # 连接池中的最大空闲连接,默认为10
        max-idle: 10
        # 连接池中的最小空闲连接,默认为2
        min-idle: 2
    # 超时时间
    timeout: 6000

使用demo

RedisTemplate类提供了一系列方法,例如:

  • opsForValue():返回一个用于操作字符串类型的ValueOperations对象。
  • opsForHash():返回一个用于操作哈希表类型的HashOperations对象。
  • opsForList():返回一个用于操作列表类型的ListOperations对象。
  • opsForSet():返回一个用于操作集合类型的SetOperations对象。
  • opsForZSet():返回一个用于操作有序集合类型的ZSetOperations对象。

ValueOperations类是RedisTemplate类下的一个接口,用于操作Redis中的字符串类型数据。它提供了一系列方法,例如:

  • set(key, value):设置指定键的值。
  • get(key):获取指定键的值。
  • increment(key, delta):将键的值增加给定的delta值。
  • decrement(key, delta):将键的值减去给定的delta值。
  • append(key, value):将给定值追加到键的值后面。
  • size(key):获取键的值的长度。
@SpringBootTest
class Demo1ApplicationTests {
​
    @Resource
    private RedisTemplate redisTemplate;
​
    @Test
    public void testString() {
        //调用set()方法创建缓存
        Student student1 = new Student(1,"张三","男","一年级","计算机科学与技术");
        // 通过redisTemplate 获取到ValueOperations对象,进行存储
        ValueOperations<String,Student> valueOperations = redisTemplate.opsForValue();
        valueOperations.set("user:ysxq", student1);
        // 调用get()方法获取数据
        Student student2 = valueOperations.get("user:ysxq");
        System.out.println(student2);
​
    }
​
}
​

Redis 5 种数据类型操作

字符串

@GetMapping("/string")
public String stringTest(){
    redisTemplate.opsForValue().set("str","Hello World");
    String str = (String) redisTemplate.opsForValue().get("str");
    return str;
}

列表

@GetMapping("/list")
public List<String> listTest(){
    ListOperations<String,String> listOperations = redisTemplate.opsForList();
    listOperations.leftPush("list","Hello");
    listOperations.leftPush("list","World");
    listOperations.leftPush("list","Java");
    List<String> list = listOperations.range("list",0,2);
    return list;
}

集合

@GetMapping("/set")
public Set<String> setTest(){
  SetOperations<String,String> setOperations = redisTemplate.opsForSet();
  setOperations.add("set","Hello");
  setOperations.add("set","Hello");
  setOperations.add("set","World");
  setOperations.add("set","World");
  setOperations.add("set","Java");
  setOperations.add("set","Java");
  Set<String> set = setOperations.members("set");
  return set;
}

有序集合

@GetMapping("/zset")
public Set<String> zsetTest(){
    ZSetOperations<String,String> zSetOperations = redisTemplate.opsForZSet();
    zSetOperations.add("zset","Hello",1);
    zSetOperations.add("zset","World",2);
    zSetOperations.add("zset","Java",3);
    Set<String> set = zSetOperations.range("zset",0,2);
    return set;
}

哈希

HashMap key value

HashOperations key hashkey value

key 是每一组数据的 ID,hashkey 和 value 是一组完整的 HashMap 数据,通过 key 来区分不同的 HashMap。

HashMap hashMap1 = new HashMap();
hashMap1.put(key1,value1);
HashMap hashMap2 = new HashMap();
hashMap2.put(key2,value2);
HashMap hashMap3 = new HashMap();
hashMap3.put(key3,value3);
HashOperations<String,String,String> hashOperations = redisTemplate.opsForHash();
hashOperations.put(hashMap1,key1,value1);
hashOperations.put(hashMap2,key2,value2);
hashOperations.put(hashMap3,key3,value3);

解决的问题

springboot-test注入对象空指针

先检查test的目录层级,是否在顶层包。

然后检查是否添加注解

如果只有@springBootTest无法解决则添加

@RunWith(SpringJUnit4ClassRunner.class)

security配置多个provider出现找不到处理token的provider

配置多个provider 出现 No AuthenticationProvider found for edu.mkit.mkmusic.system.security.token.EmailAuthenticationToken异常,

原因是在protected void configure(AuthenticationManagerBuilder auth)方法中使用了父方法super.configure(auth);