0%

SpringBoot学习笔记

SpringBoot

1,什么是Springboot

Spring Boot可以帮助您创建可以运行的独立的,基于生产级的基于Spring的应用程序。我们对Spring平台和第三方库持固执己见的观点,这样您就可以以最小的麻烦开始使用。大多数Spring Boot应用程序只需要很少的Spring配置。

您可以使用Spring Boot创建Java应用程序,可以通过使用java -jar或更传统的战争部署来启动Java应用程序。我们还提供了运行“ spring脚本”的命令行工具。

我们的主要目标是:

  • 为所有Spring开发提供根本上更快且可广泛访问的入门体验。
  • 开箱即用,但由于需求开始与默认值有所出入,因此请尽快避开。
  • 提供一系列大型项目通用的非功能性功能(例如嵌入式服务器,安全性,指标,运行状况检查和外部化配置)。
  • 完全没有代码生成,也不需要XML配置。

2,什么是微服务

原文是 Martin Flower 于 2014 年 3 月 25 日写的《Microservices》

文章链接:https://blog.csdn.net/qq_43458555/article/details/108468384

微服务

“微服务架构(Microservice Architecture)”一词在过去几年里广泛的传播,它用于描述一种设计应用程序的特别方式,作为一套独立可部署的服务。目前,这种架构方式还没有准确的定义,但是在围绕业务能力的组织、自动部署(automated deployment)、端智能(intelligence in the endpoints)、语言和数据的分散控制,却有着某种共同的特征。

“微服务(Microservices)”——只不过在满大街充斥的软件架构中的一新名词而已。尽管我们非常鄙视这样的东西,但是这玩意所描述的软件风格,越来越引起我们的注意。在过去几年里,我们发现越来越多的项目开始使用这种风格,以至于我们身边的同事在构建企业级应用时,把它理所当然的认为这是一种默认开发形式。然而,很不幸,微服务风格是什么,应该怎么开发,关于这样的理论描述却很难找到。

简而言之,微服务架构风格,就像是把一个单独的应用程序开发为一套小服务,每个小服务运行在自己的进程中,并使用轻量级机制通信,通常是 HTTP API。这些服务围绕业务能力来构建,并通过完全自动化部署机制来独立部署。这些服务使用不同的编程语言书写,以及不同数据存储技术,并保持最低限度的集中式管理。

在开始介绍微服务风格(microservice style)前,比较一下整体风格(monolithic style)是很有帮助的:一个完整应用程序(monolithic application)构建成一个单独的单元。企业级应用通常被构建成三个主要部分:客户端用户界面(由运行在客户机器上的浏览器的 HTML 页面、Javascript 组成)、数据库(由许多的表构成一个通用的、相互关联的数据管理系统)、服务端应用。服务端应用处理 HTTP 请求,执行领域逻辑(domain logic),检索并更新数据库中的数据,使用适当的 HTML 视图发送给浏览器。服务端应用是完整的 ,是一个单独的的逻辑执行。任何对系统的改变都涉及到重新构建和部署一个新版本的服务端应用程序。

这样的整体服务(monolithic server)是一种构建系统很自然的方式。虽然你可以利用开发语基础特性把应用程序划分成类、函数、命名空间,但所有你处理请求的逻辑都运行在一个单独的进程中。在某些场景中,开发者可以在的笔计本上开发、测试应用,然后利用部署通道来保证经过正常测试的变更,发布到产品中。你也可以使用横向扩展,通过负载均衡将多个应用部署到多台服务器上。

整体应用程序(Monolithic applications)相当成功,但是越来越多的人感觉到有点不妥,特别是在云中部署时。变更发布周期被绑定了——只是变更应用程序的一小部分,却要求整个重新构建和部署。随着时间的推移,很难再保持一个好的模块化结构,使得一个模块的变更很难不影响到其它模块。扩展就需要整个应用程序的扩展,而不能进行部分扩展。描述

这导致了微服务架构风格(microservice architectural style)的出现:把应用程序构建为一套服务。事实是,服务可以独立部署和扩展,每个服务提供了一个坚实的模块边界,甚至不同的服务可以用不同的编程语言编写。它们可以被不同的团队管理。

我们必须说,微服务风格不是什么新东西,它至少可以追溯到 Unix 的设计原则。但是并没有太多人考虑微服务架构,如果他们用了,那么很多软件都会更好。

微服务风格的特性

微服务风格并没有一个正式的定义,但我们可以尝试描述一下微服务风格所具有的共同特点。并不是所有的微服务风格都要具有所有的特性,但我们期望常见的微服务都应该有这些特性。我们的意图是尝试描述我们工作中或者在其它我们了解的组件中所理解的微服务。特别是,我们不依赖于那些已经明确过的定义。

组件化(Componentization )与服务(Services)
自从我们开始软件行业以来,一直希望由组件构建系统,就像我们在物理世界所看到的一样。在过去的几十年里,我们已经看到了公共库的大量简编取得了相当的进步,这些库是大部分语言平台的一部分。

当我们谈论组件时,可能会陷入一个困境——什么是组件。我们的定义是,组件(component)是一个可独立替换和升级的软件单元。

微服务架构(Microservice architectures)会使用库(libraries),但组件化软件的主要方式是把它拆分成服务。我们把库(libraries)定义为组件,这些组件被链接到程序,并通过内存中函数调用(in-memory function calls)来调用,而服务(services )是进程外组件(out-of-process components),他们利用某个机制通信,比如 WebService 请求,或远程过程调用(remote procedure call)。组件和服务在很多面向对象编程中是不同的概念。

把服务当成组件(而不是组件库)的一个主要原因是,服务可以独立部署。如果你的应用程序是由一个单独进程中的很多库组成,那么对任何一个组件的改变都将导致必须重新部署整个应用程序。但是如果你把应用程序拆分成很多服务,那你只需要重新部署那个改变的服务。当然,这也不是绝对的,有些服务会改变导致协调的服务接口,但是一个好的微服务架构的目标就是通过在服务契约(service contracts)中解耦服务的边界和进化机制来避免这些。

另一个考虑是,把服务当组件将拥有更清晰的组件接口。大多数开发语言都没有一个良好的机制来定义一个发布的接口(Published Interface)。发布的接口是指一个类向外公开的成员,比如 Java 中的声明为 Public 的成员,C# 中声明为非 Internal 的成员。通常只有在文档和规范中会说明,这是为了让避免客户端破坏组件的封装性,阻止组件间紧耦合。服务通过使用公开远程调用机制可以很容易避免这些。

像这样使用服务也有不足之处。远程调用比进制内调用更消耗资源,因此远程 API 需要粗粒度(coarser-grained),但这会比较难使用。如果你需要调整组件间的职责分配,当跨越进程边界时,这样做将会很难。

一个可能是,我们看到,服务可以映射到运行时进程(runtime processes)上,但也只是一个可能。服务可以由多个进程组成,它们会同时开发和部署,例如一个应用程序进程和一个只能由这个服务使用的数据库。

3,创建一个SpringBoot项目

方式一(不推荐)

  • 可以通过官方的SpringBoot项目生成器https://start.spring.io/生成项目,然后使用IDEA导入zip包image-20210505155450366

方式二:

  • IDEA内部已经集成了https://start.spring.io/网站,可以在IDEA中直接创建。image-20210505155629256

4,SpringBoot自动装配原理

核心依赖

如果我们想要查看SpringBoot的核心依赖,需要找到pom.xml文件,在pom.xml配置文件中寻找maven的父工程,顶级工程spring-boot-dependencies内部定义了足足两千多行的依赖

所以在我们创建SpringBoot时不需要指定个个详细的依赖,因为在顶级父工程中制定了版本

各个版本的SpringBoot所指定的依赖版本不同,不过都可以在官方文档中查看,2.4.5的SpringBoot可以在网址:https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-dependency-versions.html#dependency-versions-coordinates中查看

启动器

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • 启动器:也就是SpringBoot的启动场景
  • SpringBoot将启动器封装起来,内部包含了该启动器的所有依赖
  • 如果我们想要添加别的启动场景,我们就可以直接添加启动器即可,SprintBoot会自动帮我们导入所需场景的依赖
  • 需要什么功能,直接导入启动器(spring-boot-starter-XXX)即可。

自动装配

帮助文档

CSDN:https://blog.csdn.net/qq_38526573/article/details/107084943

知乎:https://zhuanlan.zhihu.com/p/123343325,https://zhuanlan.zhihu.com/p/95217578(这个挺好)

博客园:https://www.cnblogs.com/hhcode520/p/9450933.html(不错)

SpringBoot完美的诠释了什么叫JavaConfig配置Spring

注解

@SpringBootApplication——(标注该类是一个SpringBoot的应用)

  • @SpringBootConfiguration——(SpringBoot的配置)
    • @Configuration——(Spring的JavaConfig的配置类)
      • @Component——(组件)
  • @ComponentScan——(帮助我们去寻找指定包下的@Component等等标签注册到IOC容器当中)
  • @EnableAutoConfiguration——(重点:启动自动JavaConfig配置类)
    • @AutoConfigurationPackage——(自动扫描@SpringBootApplication注解包下所有组件并注册)
      • @Import(AutoConfigurationPackages.Registrar.class)
    • @Import(AutoConfigurationImportSelector.class)

关键配置类

  • AutoConfigurationPackages

  • AutoConfigurationImportSelector

  • Registrar

关键方法

  • Registrar——determineImports

    • Registrar 类里一共有两个方法,

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
      @Override
      public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
      register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
      }

      @Override
      public Set<Object> determineImports(AnnotationMetadata metadata) {
      return Collections.singleton(new PackageImports(metadata));
      }
      }

      分别是 determineImportsregisterBeanDefinitions

      determineImports 方法在我的项目的启动过程中并没有触发断点,官方的文档描述这个方法返回的是一组代表要导入项的对象。

      registerBeanDefinitions 方法触发断点后发现

      new AutoConfigurationPackages.PackageImport(metadata)).getPackageName() 方法返回的就是 @SpringBootApplication 注解所在的类的包名。

      所以 @AutoConfigurationPackage 注解的作用应该是扫描与 @SpringBootApplication 标注的类同一包下的所有组件。

  • AutoConfigurationImportSelector——selectImports

    • getAutoConfigurationEntry 获取自动配置条目。

      1
      2
      3
      4
      5
      6
      7
      public String[] selectImports(AnnotationMetadata annotationMetadata) {
      if (!isEnabled(annotationMetadata)) {
      return NO_IMPORTS;
      }
      AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
      return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
      }

      然后我们再进入到这个叫做 getCandidateConfigurations 的方法中,这个方法名告诉我们这个方法的作用是获取候选配置。

      1
      2
      3
      4
      5
      6
      //getCandidateConfigurations 的定义
      protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
      List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
      Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
      return configurations;
      }

      从这个方法中的 Assert.notEmpty() 中我们可以反推得出,Spring Boot 除了扫描自己 jar 包中 META-INF/spring.factories 之外,还会去找别的 jar 包中是否存在 META-INF/spring.factories 。这也为第三方开发自己的 spring-boot-starter 提供了便利。

SpringBoot自动装配

图片链接地址:http://assets.processon.com/chart_image/6093bca41e0853762874bea6.png

img

image-20210506183334831

总结

当我们的SpringBoot项目启动的时候,会先导入AutoConfigurationImportSelector,这个类会帮我们选择所有候选的配置,我们需要导入的配置都是SpringBoot帮我们写好的一个一个的配置类,那么这些配置类的位置,存在与META-INF/spring.factories文件中,通过这个文件,Spring可以找到这些配置类的位置,于是去加载其中的配置。

img

SpringBoot所有的自动配置会在启动时去扫描并加载,META-INF/spring.factories文件装载了所有的自动配置类(JavaConfig类),但是并不是所有的自动配置类都会生效,装配,只有项目有对应的启动器时(也就是加入了对应的依赖如:web依赖),对应的自动装配类才会生效,具体实现是:@ConditionalOnXXX注解

  1. SpringBoot在启动时,会在类的路径下META-INF/spring.factories文件获取指定的值
  2. 通过值将指定的自动配置类导入至IOC容器,那么自动配置就会生效,帮助我们进行配置
  3. 整个JavaEE,解救方案和自动配置的东西都在spring-boot-autoconfigure-2.4.5.jar下
  4. 它会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器中
  5. spring.factories文件存在非常多的XXXAutoConfiguration的文件(@Bean),这些类就是自动装配类,存在这个场景需要的所有组件,并自动装配(JavaConfig配置类)

5,多环境多配置

优先级

#application.properties 优先级从高到低,application.yml文件优先级也一样
#级别高的会覆盖级别低的,级别高没有的,级别低有的配置项会同样生效

1、-- 项目根目录config文件夹里面(优先级最高)
./config/

2、–项目根目录
./

3、-- src/main/resources/config/文件夹里面
classpath:/config

4、-- src/main/resources/
classpath:/

1-4优先级从高到低,优先级高的配置内容会覆盖优先级低的配置内容
server.port=8081

如何选择配置文件

在不同的环境下需要选择不同的配置文件(生产环境,测试环境)

那么我们应该如何选择配置文件呢

我们只需要在application.yaml加入

1
2
3
spring:
profiles:
active: dev

其中dev是其他配置文件的后缀名(后缀名可以看作配置文件的别名)

image-20210508172959315

6,SpringBoot自动装配再理解

为什么我们SpringBoot的配置文件application.yaml可以修改SpringBoot原有配置呢

如:可以修改端口

1
2
server:
port: 8082

原理

背后的原理其实就是yaml去给我们的JavaConfig类属性赋值

以字符编码配置类举例

org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\

image-20210508180355140

通过注解@EnableConfigurationProperties(ServerProperties.class)

去指定我们的JavaConfig配置类,也就是ServerProperties类

我们点开这个类来看一看

image-20210508180602998

我们可以看到一个非常熟悉的注解@ConfigurationProperties

该注解的功能就是去yaml配置文件中去寻找server的属性并通过server以下的属性给该类的属性赋值

举例

如:现在我们想给prot属性赋值

image-20210508180852864

那么我需要再yaml配置文件中去写即可

image-20210508180929186

7,@Conditional注解

@Conditional是Spring4新提供的注解,它的作用是按照一定的条件进行判断,满足条件给容器注册bean。

img

8,WEB开发

处理静态资源

在JavaConfig配置类WebMvcAutoConfiguration.java类的addResourceHandlers添加资源处理器方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
super.addResourceHandlers(registry);
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
ServletContext servletContext = getServletContext();
addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (servletContext != null) {
registration.addResourceLocations(new ServletContextResource(servletContext, SERVLET_LOCATION));
}
});
}

第一步判断我们是否自定义配置了类静态资源路径 if (!this.resourceProperties.isAddMappings())

如果配置了则直接返回

然后它将静态资源路径/META-INF/resources/webjars/缩减成了路径/webjars/**

也就是将静态资源请求地址从原先的localhost:8080/META-INF/resources/webjars/**变成了

localhost:8080/webjars/**

哪会有什么静态资源会放在/META-INF/resources/webjars/下呢

以jquery举例(这里我们使用webjars导入jquery,而不是之前的直接导入js文件)

image-20210509152304246

现在我们去寻找jquery.js不需要写全路径,而是写http://localhost:8080/webjars/jquery/3.6.0/jquery.js

因为/webjars已经代替了/META-INF/resources/webjars/

这是jar的静态资源

那我们项目当中的静态资源应该放在那里呢

Resources类中就可以看到我们的静态资源可以放在那了image-20210509153147629

所以我们的静态资源可以放在"classpath:/resources/", “classpath:/static/”, "classpath:/public/"目录

image-20210509153318859

优先级

resources=>static=>public

9,模板引擎

Thymeleaf模板引擎是SpringBoot推荐的模板引擎

(一)Thymeleaf 是个什么?

 简单说, Thymeleaf 是一个跟 Velocity、FreeMarker 类似的模板引擎,它可以完全替代 JSP 。相较与其他的模板引擎,它有如下三个极吸引人的特点:

1.Thymeleaf 在有网络和无网络的环境下皆可运行,即它可以让美工在浏览器查看页面的静态效果,也可以让程序员在服务器查看带数据的动态页面效果。这是由于它支持 html 原型,然后在 html 标签里增加额外的属性来达到模板+数据的展示方式。浏览器解释 html 时会忽略未定义的标签属性,所以 thymeleaf 的模板可以静态地运行;当有数据返回到页面时,Thymeleaf 标签会动态地替换掉静态内容,使页面动态显示。

2.Thymeleaf 开箱即用的特性。它提供标准和spring标准两种方言,可以直接套用模板实现JSTL、 OGNL表达式效果,避免每天套模板、该jstl、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。

3. Thymeleaf 提供spring标准方言和一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能。

image-20210509161046706

使用Thymeleaf

前提

官方文档:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html

在SpringBoot项目的pom文件中导入Thymeleaf

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

在我们创建的所有html文件中加入约束/命名空间

1
<html lang="en" xmlns:th="http://www.thymeleaf.org">

使用

使用和jsp相似

通过model携带数据

返回字符串,经过视图解析器解析

image-20210509164007672

指定页面,通过Thymeleaf语法接收数据

image-20210509164117188

查看配置类

我们可以看一下Thymeleaf的JavaConfig配置类ThymeleafProperties

image-20210509164403500

可以看到Thymeleaf会去扫描资源包下的templates路径,然后拼接.html后缀

所以我们的所有页面必须放在templates路径下,同时页面以html后缀结尾

这里可以深刻体会到约定大于配置

语法

  • 简单表达式:
    • 变量表达式: ${...}
    • 选择变量表达式: *{...}
    • 消息表达: #{...}
    • 链接URL表达式: @{...}
    • 片段表达式: ~{...}
  • 文字
    • 文本文字:'one text''Another one!',…
    • 数字文字:0343.012.3,…
    • 布尔文字:truefalse
    • 空文字: null
    • 文字标记:onesometextmain,…
  • 文字操作:
    • 字符串串联: +
    • 文字替换: |The name is ${name}|
  • 算术运算:
    • 二元运算符:+-*/%
    • 减号(一元运算符): -
  • 布尔运算:
    • 二元运算符:andor
    • 布尔否定(一元运算符): !not
  • 比较和平等:
    • 比较:><>=<=gtltgele
    • 等号运算符:==!=eqne
  • 条件运算符:
    • 如果-则: (if) ? (then)
    • 如果-则-否则: (if) ? (then) : (else)
    • 默认: (value) ?: (defaultvalue)

10,扩展SpringMVC配置

在SpringBoot自定义设置我们的SpringMVC

文档地址:https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-spring-mvc

我们可以先看自动配置视图解析

如果我们要自定义视图解析器的话,可以先看官方默认的视图解析器ContentNegotiatingViewResolver是怎么写的

image-20210511214220487

官方的视图解析器实现了ViewResolver接口

ContentNegotiatingViewResolver实现了ViewResolver接口的resolveViewName方法

image-20210511214459312

getCandidateViews 得到候选视图

getBestView 得到最好视图

自定义视图解析器

那么我们自定义的视图解析器需要实现ViewResolver接口并且实现了ViewResolver接口的resolveViewName方法

image-20210511215019247

最后将自定义的视图解析器注册到IOC容器当中

关于注解@EnableWebMvc

在官方文档我们可以看到这样一句话

1
2
3
4
If you want to keep those Spring Boot MVC customizations and make more [MVC customizations](interceptors, formatters, view controllers, and other features), you can add your own `@Configuration` class of type `WebMvcConfigurer` but **without** `@EnableWebMvc`


如果要保留这些Spring Boot MVC定制并进行更多的MVC定制(拦截器,格式化程序,视图控制器和其他功能),则可以添加自己@Configuration的注解,同时实现WebMvcConfigurer接口,但不添加 @EnableWebMvc注解。

为什么我们扩展配置类的时候不能添加 @EnableWebMvc注解呢

官方是这样解释的

1
2
3
4
If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.


如果你想利用Spring MVC中的完全控制,你可以添加自己配置类添加@Configuration注解和@EnableWebMvc,或者添加自己的@Configuration-annotatedDelegatingWebMvcConfiguration中的Javadoc中所述@EnableWebMvc。

所以一旦我们加上@EnableWebMvc注解我们的配置类就会完全接管MVC配置,官方的自动配置类就会失效

那SpringBoot是怎么实现的呢,看看源码!

源码分析

1,先看看注解@EnableWebMvc

image-20210511221841918

该注解导入了一个DelegatingWebMvcConfiguration

看看DelegatingWebMvcConfiguration

image-20210511222020656

DelegatingWebMvcConfiguration类继承了WebMvcConfigurationSupport

好的,注解@EnableWebMvc先到这里,不过要记住WebMvcConfigurationSupport

2,再来看看SpringBoot官方的Web配置类WebMvcAutoConfiguration

image-20210511222314623

在最上方我们看到了

1
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

之前我们一再强调@ConditionalXXX系列注解的重要性

image-20210511222520036

现在我们就知道了原理

只有容器中不存在WebMvcConfigurationSupport类那么WebMvcAutoConfiguration类才会生效,

那么再看@EnableWebMvc注解,它内部继承了WebMvcConfigurationSupport

也就是现在IOC容器内存在了WebMvcConfigurationSupport类,

所以官方的Web配置类WebMvcAutoConfiguration类不会生效

11,练习

1,国际化

我们先定义一个页面的语言包内部包含中英文

image-20210513002452883

然后再SpringBoot配置文件中配置语言包

1
2
#指定消息国际化的资源包
spring.messages.basename=i18n/login

然后我们自定义配置文件的话我们可以参考官方的国际化包AcceptHeaderLocaleResolver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyLocaleResolver implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {

//获取标志位
String l = request.getParameter("l");
//获取默认配置
Locale locale = Locale.getDefault();
//判断标志是否不合法
if (l!=null){
//得到国家和语言
String[] s = l.split("_");
locale =new Locale(s[0],s[1]);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

}
}

最后将我们自己配置的类装配到扩展配置类中

image-20210513004439044

@Bean

注意方法名必须是localeResolver

2,拦截器

实现连接器接口HandlerInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
public class Interceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
String user = (String) session.getAttribute("user");
if (user==null){
response.sendRedirect("/index.html");
return false;
}
return true;
}
}

将自己的拦截器添加到自己的扩展配置类中

扩展配置类重写addInterceptors方法

1
2
3
4
5
//拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new Interceptor()).addPathPatterns("/**").excludePathPatterns("/user/login","/index.html");
}

new一个自己的拦截器,拦截"/**“的所有请求,排除”/user/login","/index.html"请求

12,数据库的连接与配置

1,JDBC

1,1 创建SpringBoot项目时勾选JDBC和Mysql数据库的支持

image-20210517112512280

1,2 在yaml配置文件中配置数据库的用户名,密码,连接地址,Driver

1
2
3
4
5
6
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&characterEncoding=utf8&useSSl=true
driver-class-name: com.mysql.cj.jdbc.Driver

1,3 如何使用

在SpringBoot中使用JDBC不需要再像之前的JavaEE阶段从获取连接 connection,预编译sql语句PreparedStatement ,结果集ResultSet等等

而是使用SpringBoot集成的模板XXXTemplate

JDBC的模板是jdbcTemplate,使用它可以直接执行sql语句,传入值集合,也可以预编译

image-20210517114952248

2,集成Druid

文档:https://github.com/alibaba/druid/wiki/Druid连接池介绍

Druid 简介

更新时间: 2020-01-18

Apache Druid 是一个分布式内存实时分析系统,用于解决如何在大规模数据集下进行快速的、交互式的查询和分析的问题。Apache Druid 由 Metamarkets 公司(一家为在线媒体或广告公司提供数据分析服务的公司)开发,在2019年春季被捐献给 Apache 软件基金会。

基本特点

Apache Druid 具有以下特点:

  • 亚秒级 OLAP 查询,包括多维过滤、Ad-hoc 的属性分组、快速聚合数据等等。
  • 实时的数据消费,真正做到数据摄入实时、查询结果实时。
  • 高效的多租户能力,最高可以做到几千用户同时在线查询。
  • 扩展性强,支持 PB 级数据、千亿级事件快速处理,支持每秒数千查询并发。
  • 极高的高可用保障,支持滚动升级。

应用场景

实时数据分析是 Apache Druid 最典型的使用场景。该场景涵盖的面很广,例如:

  • 实时指标监控
  • 推荐模型
  • 广告平台
  • 搜索模型

这些场景的特点都是拥有大量的数据,且对数据查询的时延要求非常高。在实时指标监控中,系统问题需要在出现的一刻被检测到并被及时给出报警。在推荐模型中,用户行为数据需要实时采集,并及时反馈到推荐系统中。用户几次点击之后系统就能够识别其搜索意图,并在之后的搜索中推荐更合理的结果。

使用Druid

1,首先在SpringBoot项目中导入Druid的starter,如果我们Druid配置了log4j的日志打,还需要加入log4j的依赖

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>

<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

2,通过yaml配置Druid

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
spring:
datasource:
name: druidDataSource
#指定使用Druid的数据源
type: com.alibaba.druid.pool.DruidDataSource
druid:
#注意版本
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&characterEncoding=utf8&useSSl=true
username: root
#注意此处密码为纯数字需要引用
password: root

#-配置监控统计拦截器

#配置中,stat监控统计,wall防御sql注入,log4日志记录
#如果报错检查是否导入log4j
filters: stat,wall,log4j




#-连接池设置
#最大连接池数量 maxIdle已经不再使用
max-active: 100

#初始化时建立物理连接的个数
initial-size: 1

#获取连接时最大等待时间,单位毫秒
max-wait: 60000

#最小连接池数量
min-idle: 1

#既作为检测的间隔时间又作为testWhileIdel执行的依据
time-between-eviction-runs-millis: 60000

#销毁线程时检测当前连接的最后活动时间和当前时间差大于该值时,关闭当前连接
min-evictable-idle-time-millis: 300000

#用来检测连接是否有效的sql 必须是一个查询语句
#mysql中为 select 'x'
#oracle中为 select 1 from dual
validation-query: select 'x'

#申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效
test-while-idle: true

#申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
test-on-borrow: false

#归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
test-on-return: false

#是否缓存preparedStatement,mysql5.5+建议开启
pool-prepared-statements: true

#当值大于0时poolPreparedStatements会自动修改为true
max-pool-prepared-statement-per-connection-size: 20

#合并多个DruidDataSource的监控数据
use-global-data-source-stat: true

#设置访问druid监控页的账号和密码,默认没有
stat-view-servlet:
login-username: admin
login-password: admin

3,如果报了图片的错误

在这里插入图片描述

则需要在资源包下添加名为:log4j.properties log4j的properties配置文件

并将以下内容复制进去

1
2
3
4
log4j.rootLogger=DEBUG, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

3,整合Mybatis框架

Mybatis框架可以和Druid共存,只需要分别配置即可

导入依赖

这是Maven的mybatis自己写的依赖,所以命名是spring-boot-starter在后面,官方是以spring-boot-starter开头的

1
2
3
4
5
6
7
<!--mybatis SpringBoot的依赖-->
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>

配置Mybatis

image-20210518142941023

编写Mapper接口

image-20210518143051435

值得注意的是我们需要在接口上加上@Repository注解将其交给IOC容器管理

@Mapper注解的的作用

1:为了把mapper这个DAO交給Spring管理 http://412887952-qq-com.iteye.com/blog/2392672

2:为了不再写mapper映射文件 https://blog.csdn.net/weixin_39666581/article/details/103899495

3:为了给mapper接口 自动根据一个添加@Mapper注解的接口生成一个实现类 http://www.tianshouzhi.com/api/tutorials/mapstruct/292

但是在一个dao层接口写上@Mapper明显比较繁琐,那么在工程较大时,可以在SpringBoot的主启动类加上@MapperScan() 注解,只要在注解内部写上dao层接口的路径,就会自动扫描并注册

可参考:https://www.iteye.com/blog/412887952-qq-com-2392672

13,SpringSecurity

Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。

相对于 Shiro,在 SSM/SSH 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。

自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了 自动化配置方案,可以零配置使用 Spring Security。

因此,一般来说,常见的安全管理技术栈的组合是这样的:

  • SSM + Shiro
  • Spring Boot/Spring Cloud + Spring Security

注意,这只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行的。

**帮助文档:**https://www.cnblogs.com/lenve/p/11242055.html

https://www.cnblogs.com/felordcn/p/12142549.html

使用SpringSecurity

在pom配置文件中导入依赖

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.4.5</version>
</dependency>

当我们添加依赖后,Security会直接生效

访问请求时,会要求我们登录

image-20210518161448878

默认的用户名是user ,而密码会在每一次启动项目时生成

image-20210518161540577

配置文件指定用户名和密码

我们也可以自定义密码和用户名,这样就不会每一次靠Security生成

在yaml配置文件中指定用户名和密码

image-20210518163947193

Java配置文件指定用户名和密码

首先我们创建一个JavaConfig的配置类,将配置类继承WebSecurityConfigurerAdapter

并且在配置类上方加上@EnableWebSecurity注解,开启web安全

@EnableWebSecurity注解内部有一个@Configuration注解了,所以不需要在加了

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

//配置请求的权限 过滤器
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//"/","/toLogin" 的请求所有人都可以通过
.antMatchers("/","/toLogin").permitAll()
///views/1/** 请求 必须有权限1
.antMatchers("/views/1/**").hasRole("1")
.antMatchers("/views/2/**").hasRole("2")
.antMatchers("/views/3/**").hasRole("3");
http.formLogin();//.loginPage("/toLogin").loginProcessingUrl("/logon");
}


//认证用户
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//从内存中读取用户
//passwordEncoder(new BCryptPasswordEncoder()) 设置密码加密格式为BCryptPasswordEncoder
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
//添加一个用户 密码通过new BCryptPasswordEncoder().encode("123")加密 roles 拥有的权限
.withUser("admin").password(new BCryptPasswordEncoder().encode("123")).roles("1","2","3")
//如果想在添加一个用户只需在and方法后面添加
.and().withUser("xpp011").password(new BCryptPasswordEncoder().encode("123")).roles("1");
}
}

从数据库得到用户认证,参考文档https://blog.csdn.net/BLU_111/article/details/110727891

注意WebSecurityConfigurerAdapter类有很多的configure重载方法,它们分别是

2.1 认证管理器配置方法

void configure(AuthenticationManagerBuilder auth) 用来配置认证管理器AuthenticationManager。说白了就是所有 UserDetails 相关的它都管,包含 PasswordEncoder 密码机。如果你不清楚可以通过 Spring Security 中的 UserDetail 进行了解。本文对 AuthenticationManager 不做具体分析讲解,后面会有专门的文章来讲这个东西 。 可通过 Spring Security 实战系列 进行学习。

2.2 核心过滤器配置方法

void configure(WebSecurity web) 用来配置 WebSecurity 。而 WebSecurity 是基于 Servlet Filter 用来配置 springSecurityFilterChain 。而 springSecurityFilterChain 又被委托给了 Spring Security 核心过滤器 Bean DelegatingFilterProxy 。 相关逻辑你可以在 WebSecurityConfiguration 中找到。我们一般不会过多来自定义 WebSecurity , 使用较多的使其ignoring() 方法用来忽略 Spring Security 对静态资源的控制。

2.3 安全过滤器链配置方法

void configure(HttpSecurity http) 这个是我们使用最多的,用来配置 HttpSecurityHttpSecurity 用于构建一个安全过滤器链 SecurityFilterChainSecurityFilterChain 最终被注入核心过滤器HttpSecurity 有许多我们需要的配置。我们可以通过它来进行自定义安全访问策略。所以我们单独开一章来讲解这个东西。

13.1,SpringSecurity和thymeleaf整合

文档:https://github.com/thymeleaf/thymeleaf-extras-springsecurity

https://www.thymeleaf.org/doc/articles/springsecurity.html

导入maven依赖

1
2
3
4
5
6
7
<!--thymeleaf和springsecurity5的整合包-->
<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>

在html文件导入正确的约束xmlns:sec="http://www.thymeleaf.org/extras/spring-security"官方推荐的命名空间

1
2
<html lang="en" xmlns:th="http://www.thymeleaf.org" 
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

用来判断是否登录验证

image-20210518184646205

登录页定制和记住我

定制首页只需在configure(HttpSecurity http)方法中加入

1
2
3
4
5
http.formLogin().loginPage("/toLogin")//用户为登录时,访问的所以请求都会转跳到这个页面,即登录页面
.loginProcessingUrl("/login")//处理登录的请求地址,即登录表单的action的值
.usernameParameter("username")//登录表单用户名的name,默认username
.passwordParameter("password")//登录表单密码的name,默认password
.defaultSuccessUrl("/");//登录成功后会转跳的页面 首页

登录退出

1
2
3
http.logout()
.logoutUrl("/logout")//注销表单中请求的地址路径
.logoutSuccessUrl("/");//注销成功后转跳的地址

注意在登录请求/toLogin,/login登录退出请求/logout均为post请求,a标签的href为get,所以不要使用a标签请求,不然会导致404的情况

记住我

1
2
3
http.rememberMe()//记住我
.rememberMeParameter("remember")//登录表单中选择记住我单选框的名字name属性
.tokenValiditySeconds(60*60*24*7);//设置cookie过期时间

关于数据库保存cookie的token值

参考:https://www.jb51.net/article/175334.htm

建议以后项目实现一下,有意义

14,Shiro

简介

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

**官网:**https://shiro.apache.org/index.html

**帮助文档:**https://www.w3cschool.cn/shiro/co4m1if2.html (值得一看)

img

  • Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
  • Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
  • Web Support:Web 支持,可以非常容易的集成到 Web 环境;
  • Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
  • Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
  • Testing:提供测试支持;
  • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
  • Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

记住一点,Shiro 不会去维护用户、维护权限;这些需要我们自己去设计 / 提供;然后通过相应的接口注入给 Shiro 即可。

架构

接下来我们分别从外部和内部来看看 Shiro 的架构,对于一个好的框架,从外部来看应该具有非常简单易于使用的 API,且 API 契约明确;从内部来看的话,其应该有一个可扩展的架构,即非常容易插入用户自定义实现,因为任何框架都不能满足所有需求。

首先,我们从外部来看 Shiro 吧,即从应用程序角度的来观察如何使用 Shiro 完成工作。如下图:

img

可以看到:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject;其每个 API 的含义:

Subject:主体,代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者;

SecurityManager:安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;

Realm:域,Shiro 从从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。

也就是说对于我们而言,最简单的一个 Shiro 应用:

  1. 应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;
  2. 我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。

从以上也可以看出,Shiro 不提供维护用户 / 权限,而是通过 Realm 让开发人员自己注入。

接下来我们来从 Shiro 内部来看下 Shiro 的架构,如下图所示:

img

  • Subject:主体,可以看到主体可以是任何可以与应用交互的 “用户”;
  • SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
  • Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
  • Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
  • Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro 不知道你的用户 / 权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;
  • SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所以呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台 Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器);
  • SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;
  • CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
  • Cryptography:密码模块,Shiro 提供了一些常见的加密组件用于如密码加密 / 解密的。

使用Shiro

首先在pom配置文件中导入Shiro依赖

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-web-starter -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.1</version>
</dependency>

自定义Shiro配置类 MyShiroConfig

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
@Configuration
public class MyShiroConfig {
//自定义过滤器
//将ShiroFilterFactoryBean托管给容器,名字是shiroFilterFactoryBean
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean ShiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean factoryBean=new ShiroFilterFactoryBean();
//设置安全管理器
factoryBean.setSecurityManager(defaultWebSecurityManager);

//定义setFilterChainDefinitionMap方法需要的参数
Map<String, String> filterMap=new LinkedHashMap<>();
/*
anon: 无需认证就可以访问
authc:必须通过认证才能访问
user:必须拥有记住我功能访问
perms:拥有某个资源访问权限才能访问
role: 拥有某个角色权限才能访问
*/
filterMap.put("/add","perms[user:add]");
filterMap.put("/update","perms[user:update]");
filterMap.put("/","anon");

//定义视图链 以map的形式
factoryBean.setFilterChainDefinitionMap(filterMap);
//登录请求地址
factoryBean.setLoginUrl("/toLogin");
//设定权限不够的请求路径
factoryBean.setUnauthorizedUrl("toUnauthorized");
return factoryBean;
}

//定义安全管理器 DefaultWebSecurityManager依赖于UserRealm
//将安全管理器托管给Spring容器,名字是SecurityManager
@Bean("SecurityManager")
public DefaultWebSecurityManager DefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
//关联Realm
securityManager.setRealm(userRealm);
return securityManager;
}


//自定义的Realm
@Bean
public UserRealm userRealm(){
return new UserRealm();
}

//设置thymeleaf-和-shiro的整合包的方言
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}

}

自定义的类需要三个类 分别是

  • ShiroFilterFactoryBean

    • 它是Shiro的过滤器,/Filter工厂,设置对应的过滤条件和跳转条件

    • anon: 无需认证就可以访问
      authc:必须通过认证才能访问
      user:必须拥有记住我功能访问
      perms:拥有某个资源访问权限才能访问
      role: 拥有某个角色权限才能访问
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20

      - `DefaultWebSecurityManager`

      - 安全管理器 它负责所具体的交互,比如,身份认证,授权,控制缓存等,都由他调度
      - 依赖于Realm

      - `Realm`

      - Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;
      - 由于Shiro是不提供用户/授权的,需要我们去自定义Realm



      <font color='red'>依赖关系</font>: `ShiroFilterFactoryBean`==>`DefaultWebSecurityManager`==>自定义`Realm`

      ```java
      //ShiroFilterFactoryBean设置安全管理器defaultWebSecurityManager
      ShiroFilterFactoryBean factoryBean=new ShiroFilterFactoryBean();
      //设置安全管理器
      factoryBean.setSecurityManager(defaultWebSecurityManager);
1
2
3
4
//安全管理器DefaultWebSecurityManager设置自定义Realm userRealm
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
//关联Realm
securityManager.setRealm(userRealm);
1
2
3
4
5
//自定义的Realm 
@Bean //将自定义Realm托管给Spring容器
public UserRealm userRealm(){
return new UserRealm();
}

自定义Realm类 UserRealm

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
public class UserRealm extends AuthorizingRealm {
//连接数据库
@Autowired
UserMapper userMapper;

//授权,查询用户是否有响应权限
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("执行了授权==》"+principals);

//取得方法的返回值 AuthorizationInfo接口的实现类SimpleAuthorizationInfo
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
//获取当前的用户
Subject subject = SecurityUtils.getSubject();
//获取当前用户携带的值 这个值是下面doGetAuthenticationInfo方法
//new SimpleAuthenticationInfo(user,user.getPwd(),"")的参数user
User currentUser = (User)subject.getPrincipal();
//授权用户类内部属性权限值
info.addStringPermission(currentUser.getPerms());
return info;
}

//身份认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了身份认证==》"+token);
//先伪造数据 测试
//String username="admin",password="123";


//将令牌 token转换为可以得到用户名和密码的类型 UsernamePasswordToken
UsernamePasswordToken token1=(UsernamePasswordToken) token;

//从数据库中获取用户
User user = userMapper.selectUserByName(token1.getUsername());

/*
//注意Shiro进行授权时 我们只需要判断用户名即可,密码的匹配交给 SimpleAuthenticationInfo 保证安全
if (!token1.getUsername().equals(username)){
//用户名不正确
//当doGetAuthenticationInfo方法返回null时 会报一个UnknownAccountException异常 也就是用户名不存在
return null;
}
*/

//判断用户是否为空
if (user==null){
return null;//用户名不存在
}

//密码配对交给AuthenticationInfo接口的实现类SimpleAuthenticationInfo去做
//将user存到Subject当前用户中
return new SimpleAuthenticationInfo(user,user.getPwd(),"");
}
}

Controller层的登录请求

image-20210519221703245


Shiro记住我

参考博客:https://blog.csdn.net/qq_43298012/article/details/87890956?utm_medium=distribute.wap_relevant.none-task-blog-baidujs_baidulandingword-0

​ 记住我功能在各各网站是比较常见的,实现起来也都差不多,主要就是利用cookie来实现,而shiro对记住我功能的实现也是比较简单的,只需要几步即可。

Shiro配置记住我步骤:

在MyShiroConfig中添加以下Bean

记住我cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* cookie对象;会话Cookie模板 ,默认为: JSESSIONID 问题: 与SERVLET容器名冲突,重新定义为sid或rememberMe,自定义
* @return
*/
@Bean
public SimpleCookie rememberMeCookie(){
//自定义Cookie
SimpleCookie simpleCookie=new SimpleCookie("rememberMe");
//simpleCookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:

//simpleCookie()的第七个参数
//设为true后,只能通过http访问,javascript无法访问
//防止xss读取cookie
simpleCookie.setHttpOnly(true);
//根目录下的所有路径都可以的到该Cookie
simpleCookie.setPath("/");
//Cookie的有效时间 单位秒
simpleCookie.setMaxAge(60*60*24*3);
return simpleCookie;
}

记住我Cookie的安全管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* cookie管理对象;记住我功能,rememberMe管理器
* @return
*/
@Bean
public CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager rememberMeManager=new CookieRememberMeManager();
//设置自定义的Cookie
rememberMeManager.setCookie(rememberMeCookie());
//rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
//官方推荐使用Base64加密
rememberMeManager.setCipherKey(Base64.getDecoder().decode("4AvVhmFLUs0KTA3Kprsdag=="));
return rememberMeManager;
}

记住我的过滤器

1
2
3
4
5
6
7
8
9
10
11
/**
* FormAuthenticationFilter 过滤器 过滤记住我
* @return
*/
@Bean
public FormAuthenticationFilter formAuthenticationFilter(){
FormAuthenticationFilter filter=new FormAuthenticationFilter();
//对应前端的checkbox的name = rememberMe
filter.setRememberMeParam("rememberMe");
return filter;
}

以上准备完成后,开始和Shiro整合

在核心安全管理器中配置rememberMe的安全管理器

1
2
3
4
5
6
7
8
9
@Bean("SecurityManager")
public DefaultWebSecurityManager DefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
//关联Realm
securityManager.setRealm(userRealm);
//配置记住我的安全管理器!!!
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}

**修改shirFilter中拦截请求的规则,将/从authc 改为user

但是注意前往登录和处理登录的请求就不是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
//自定义过滤器
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean ShiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
...
//定义setFilterChainDefinitionMap方法需要的参数
// 配置访问权限 必须是LinkedHashMap,因为它必须保证有序
// 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 --> : 这是一个坑,一不小心代码就不好使了
Map<String, String> filterMap=new LinkedHashMap<>();
/*
anon: 无需认证就可以访问
authc:必须通过认证才能访问
user:必须拥有记住我功能访问
perms:拥有某个资源访问权限才能访问
role: 拥有某个角色权限才能访问
*/
filterMap.put("/add","perms[user:add]");
filterMap.put("/update","perms[user:update]");
filterMap.put("/","anon");
//设置登录请求为无需认证就可以访问
filterMap.put("/login","anon");
//其他资源都需要认证 authc 表示需要认证才能进行访问 user表示配置记住我或认证通过可以访问的地址
filterMap.put("/**","user");

....
}

更改HelloController中login方法

1
2
3
4
5
6
7
8
9
10
@RequestMapping("/login")
public String login(String username, String password,boolean rememberMe, Model model){
System.out.println("用户信息:"+username+":"+password+":"+rememberMe);
//得到当前的用户
Subject subject = SecurityUtils.getSubject();
//将用户名和密码封装成UsernamePasswordToken 形成令牌token 同时将rememberMe加入登录验证
UsernamePasswordToken token=new UsernamePasswordToken(username,password,rememberMe);

...
}

登陆页面加入rememberMe功能

1
2
3
4
5
6
7
<form th:action="@{/login}">
<label th:text="${msg}" style="color: red"></label>
用户名: <input type="text" name="username"/>
密码: <input type="password" name="password"/>
<input type="checkbox" name="rememberMe">记住我
<input type="submit" value="登录">
</form>

Shiro和thymeleaf整合

导入整合包

1
2
3
4
5
6
7
<!--thymeleaf-和-shiro的整合包-->
<!-- https://mvnrepository.com/artifact/com.github.theborakompanioni/thymeleaf-extras-shiro -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>

配置thymeleaf的Shiro的方言(可以写在Shiro的配置文件中)

1
2
3
4
5
6
//设置thymeleaf-和-shiro的整合包的方言
@Bean//注册到Spring容器中
public ShiroDialect shiroDialect(){
//ShiroDialect就是方言配置类
return new ShiroDialect();
}

thymeleaf的命名空间

1
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"

可以写的一些标签

1
2
3
4
5
6
7
8
9
10
<body>
首页
<!--判断是否已认证 没认证则为false 认证则相反-->
<div shiro:notAuthenticated="">
<a th:href="@{/toLogin}">登录</a>
</div>
<!--判断当前用户是否有user:add的权限-->
<a th:href="@{/add}" shiro:hasPermission="user:add">添加</a>
<a th:href="@{/update}" shiro:hasPermission="user:update">修改</a>
</body>

浏览器中rememberMe的值

image-20210520143510203

其实是将用户信息加密后放到前台的(包含密码),在项目中user对象信息过于庞大,不能全部存入Cookie,Cookie对长度有一定的限制。

15,Swagger

帮助文档:https://zhuanlan.zhihu.com/p/275708279 (比较高级)

https://blog.csdn.net/weixin_44203158/article/details/109137799 (推荐看这个)

1,介绍

Swagger 是一套基于 OpenAPI 规范(OpenAPI Specification,OAS)构建的开源工具,后来成为了 Open API 标准的主要定义者。
对于 Rest API 来说很重要的一部分内容就是文档,Swagger 为我们提供了一套通过代码和注解自动生成文档的方法,这一点对于保证API 文档的及时性将有很大的帮助。

swagger2于17年停止维护,现在最新的版本为17年发布的 Swagger3(Open Api3)。

2,springfox介绍

SpringFox是 spring 社区维护的一个项目(非官方)
由于Spring的流行,Marty Pitt编写了一个基于Spring的组件swagger-springmvc,用于将swagger集成到springmvc中来,而springfox则是从这个组件发展而来。

3,springfox-swagger 2

SpringBoot项目整合swagger2需要用到两个依赖:springfox-swagger2springfox-swagger-ui,用于自动生成swagger文档。

springfox-swagger2:这个组件的功能用于帮助我们自动生成描述API的json文件
springfox-swagger-ui:就是将描述API的json文件解析出来,用一种更友好的方式呈现出来。

4,SpringFox 3.0.0 发布

  • Spring5,Webflux支持(仅支持请求映射,尚不支持功能端点)。
  • Spring Integration支持。
  • SpringBoot支持springfox Boot starter依赖性(零配置、自动配置支持)。
  • 支持OpenApi 3.0.3。
  • 零依赖。几乎只需要spring-plugin,swagger-core ,现有的swagger2注释将继续工作并丰富openapi3.0规范。

5,使用Swagger3.0

导入依赖:

1
2
3
4
5
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>

1, application.yml配置

1
2
3
4
5
6
7
8
9
10
11
12
spring:
application:
name: springfox-swagger
server:
port: 8080
# ===== 自定义swagger配置 ===== #
swagger:
enable: true
application-name: ${spring.application.name}
application-version: 1.0
application-description: springfox swagger 3.0整合Demo
try-host: http://localhost:${server.port}

2, 自定义一个swagger配置类SwaggerProperties.class

注意注解@ConfigurationProperties的使用

代表被修饰的类可以通过前缀prefix = "XXX",yaml赋值.

同时注解@ConfigurationProperties需要搭配注解@Component托管给ioc容器

帮助文档:https://www.cnblogs.com/jimoer/p/11374229.html

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
package cn.xpp011.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
* swagger的属性配置类
*/
@Component
@ConfigurationProperties(prefix = "swagger")
public class SwaggerProperties {
/**
* 是否开启swagger,生产环境一般关闭,所以这里定义一个变量
*/
private Boolean enable;

/**
* 项目应用名
*/
private String applicationName;

/**
* 项目版本信息
*/
private String applicationVersion;

/**
* 项目描述信息
*/
private String applicationDescription;

/**
* 接口调试地址
*/
private String tryHost;

public Boolean getEnable() {
return enable;
}

public void setEnable(Boolean enable) {
this.enable = enable;
}

public String getApplicationName() {
return applicationName;
}

public void setApplicationName(String applicationName) {
this.applicationName = applicationName;
}

public String getApplicationVersion() {
return applicationVersion;
}

public void setApplicationVersion(String applicationVersion) {
this.applicationVersion = applicationVersion;
}

public String getApplicationDescription() {
return applicationDescription;
}

public void setApplicationDescription(String applicationDescription) {
this.applicationDescription = applicationDescription;
}

public String getTryHost() {
return tryHost;
}

public void setTryHost(String tryHost) {
this.tryHost = tryHost;
}
}

3, springfox swagger3配置类SwaggerConfiguration.class

注解@EnableOpenApi大致意思就是**「只有在配置类标注了@EnableOpenApi这个注解才会生成Swagger文档」**。

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

@EnableOpenApi
@Configuration
public class SwaggerConfiguration implements WebMvcConfigurer {
private final SwaggerProperties swaggerProperties;

public SwaggerConfiguration(SwaggerProperties swaggerProperties) {
this.swaggerProperties = swaggerProperties;
}

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.OAS_30).pathMapping("/")
//组名字
.groupName("lighter")

// 定义是否开启swagger,false为关闭,可以通过变量控制
.enable(swaggerProperties.getEnable())

// 将api的元信息设置为包含在json ResourceListing响应中。
.apiInfo(apiInfo("lighter",null,"123456@gmail.com"))

// 接口调试地址
.host(swaggerProperties.getTryHost())

// 选择哪些接口作为swagger的doc发布
.select()
//扫描指定包下的@controller注解 any()是所有 none()是不扫描
.apis(RequestHandlerSelectors.basePackage("cn.xpp011.controller"))

.paths(PathSelectors.any())
.build()

// 支持的通讯协议集合
.protocols(newHashSet("https", "http"))

// 授权信息设置,必要的header token等认证信息
.securitySchemes(securitySchemes())

// 授权信息全局应用
.securityContexts(securityContexts());
}

/**
* API 页面上半部分展示信息
*/
private ApiInfo apiInfo(String name,String url,String email) {
return new ApiInfoBuilder().title(swaggerProperties.getApplicationName() + " Api Doc")
.description(swaggerProperties.getApplicationDescription())
.contact(new Contact(name, url,email ))
.version("Application Version: " + swaggerProperties.getApplicationVersion() + ", Spring Boot Version: " + SpringBootVersion.getVersion())
.build();
}

/**
* 设置授权信息
*/
private List<SecurityScheme> securitySchemes() {
ApiKey apiKey = new ApiKey("BASE_TOKEN", "token", In.HEADER.toValue());
return Collections.singletonList(apiKey);
}

/**
* 授权信息全局应用
*/
private List<SecurityContext> securityContexts() {
return Collections.singletonList(
SecurityContext.builder()
.securityReferences(Collections.singletonList(new SecurityReference("BASE_TOKEN", new AuthorizationScope[]{new AuthorizationScope("global", "")})))
.build()
);
}

@SafeVarargs
private final <T> Set<T> newHashSet(T... ts) {
if (ts.length > 0) {
return new LinkedHashSet<>(Arrays.asList(ts));
}
return null;
}

/**
* 通用拦截器排除swagger设置,所有拦截器都会自动加swagger相关的资源排除信息
*/
@SuppressWarnings("unchecked")
@Override
public void addInterceptors(InterceptorRegistry registry) {
try {
Field registrationsField = FieldUtils.getField(InterceptorRegistry.class, "registrations", true);
List<InterceptorRegistration> registrations = (List<InterceptorRegistration>) ReflectionUtils.getField(registrationsField, registry);
if (registrations != null) {
for (InterceptorRegistration interceptorRegistration : registrations) {
interceptorRegistration
.excludePathPatterns("/swagger**/**")
.excludePathPatterns("/webjars/**")
.excludePathPatterns("/v3/**")
.excludePathPatterns("/doc.html");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

}

配置完成后可访问:http://localhost:8080/swagger-ui/index.html 查看

6,Swagger分组

其实在Swagger中分组就是对应着不同的Docket

我们只需要想IOC容器中注册不同的Docket即可

这里组被分成了两个,分别是root,lighter

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
@Bean
public Docket rootRestApi() {
return new Docket(DocumentationType.OAS_30).pathMapping("/")
//组名字
.groupName("root")

// 定义是否开启swagger,false为关闭,可以通过变量控制
.enable(swaggerProperties.getEnable())

// 将api的元信息设置为包含在json ResourceListing响应中。得到个人信息
.apiInfo(apiInfo("root","https://xpp011.xn","2500176776@qq.com"))

// 接口调试地址
.host(swaggerProperties.getTryHost())

// 选择哪些接口作为swagger的doc发布
.select()
//扫描指定包下的@controller注解 any()是所有 none()是不扫描
.apis(RequestHandlerSelectors.any())

.paths(PathSelectors.any())
.build()

// 支持的通讯协议集合
.protocols(newHashSet("https", "http"))

// 授权信息设置,必要的header token等认证信息
.securitySchemes(securitySchemes())

// 授权信息全局应用
.securityContexts(securityContexts());
}


@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.OAS_30).pathMapping("/")
//组名字
.groupName("lighter")

// 定义是否开启swagger,false为关闭,可以通过变量控制
.enable(swaggerProperties.getEnable())

// 将api的元信息设置为包含在json ResourceListing响应中。
.apiInfo(apiInfo("lighter",null,"123456@gmail.com"))

// 接口调试地址
.host(swaggerProperties.getTryHost())

// 选择哪些接口作为swagger的doc发布
.select()
//扫描指定包下的@controller注解 any()是所有 none()是不扫描
.apis(RequestHandlerSelectors.basePackage("cn.xpp011.controller"))

.paths(PathSelectors.any())
.build()

// 支持的通讯协议集合
.protocols(newHashSet("https", "http"))

// 授权信息设置,必要的header token等认证信息
.securitySchemes(securitySchemes())

// 授权信息全局应用
.securityContexts(securityContexts());
}

7,测试注解

帮助文档:http://c.biancheng.net/view/5533.html

  1. @Api

    @Api 用在类上,说明该类的作用。

    1
    2
    3
    @Api(tags = "用户登录控制类")
    @Controller
    public class HelloController {}
  2. ApiOperation

@ApiOperation 用在 Controller 里的方法上,说明方法的作用,每一个接口的定义。

1
2
3
4
5
6
7
8
@Controller
public class HelloController {
@ApiOperation("首页请求")
@RequestMapping({"/","/index"})
public String index(){
return "index";
}
}
  1. ApiParam

    @ApiParam 用于 Controller 中方法的参数说明。

    1
    2
    3
    @ApiOperation("处理登录请求")
    @RequestMapping("/login")
    public String login(@ApiParam("用户名字") String username,@ApiParam("用户密码") String password,@ApiParam("是否勾选记得我") boolean rememberMe, Model model){}
  2. @ApiModel

    @ApiModel 用在实体类上

    1
    2
    @ApiModel(value = "用户类",discriminator = "实体类")
    public class User implements Serializable {}
  3. ApiModelProperty

@ApiModelProperty() 用于实体类的字段

1
2
3
4
5
6
7
8
@ApiModelProperty("用户id")
private int id;
@ApiModelProperty("用户名字")
private String name;
@ApiModelProperty("用户密码")
private String pwd;
@ApiModelProperty("用户权限")
private String perms;

16,异步任务

异步任务主要帮助用户提升体验,比如发送邮件,后台发送邮件是比较慢的过程,需要联网等等,而用户就会需要等待多时,而我们开启异步任务时,将邮件发送变成一个多线程任务,让用户先做其他事情,就会大大增加用户体验

如何开启异步任务

在需要异步的类上加上一个注解@Async声明这是一个异步任务,那样程序调用该类时就会开启异步

该注解可以加在类上或者方法上

1
2
3
4
5
6
7
8
9
10
11
12
13
@Async//该类是一个异步任务
@Service
public class AsyncTest {

public void test(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello");
}
}

在SpringBoot主启动类上加上@EnableAsync注解,启动异步

1
2
3
4
5
6
7
@EnableAsync//开启异步任务
@SpringBootApplication
public class SpringbootAsyncApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootAsyncApplication.class, args);
}
}

17,文件上传,邮件发送

邮件发送

参考文档:https://blog.csdn.net/qq_43647359/article/details/104638599

首先导入依赖

由于我发现springBoot启动器mail的依赖包中,javax.mail.internet.MimeMessage是缺失的,所有还需要额外导入javax.mail的mail依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-mail -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>2.4.5</version>
</dependency>

<!-- https://mvnrepository.com/artifact/javax.mail/mail -->
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.7</version>
</dependency>

yaml配置文件中配置发送邮件需要的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
mail:
#服务器地址
host: smtp.qq.com
#邮件编码
default-encoding: utf-8
#发送邮件用户名
username: 2500176776@qq.com
#发送邮件的密码,防止密码暴露,填写授权码
password: ddkrojojfylldhic
#设置ssl加密
properties:
mail:
smtp:
socketFactoryClass: javax.net.ssl.SSLSocketFactory

一个简单的邮件

项目中要把它提成一个方法,这里先偷懒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public String SimpleMailSend(){
String msg="邮件发送成功";
try {
//创建一个简单邮件实例
SimpleMailMessage message=new SimpleMailMessage();
//邮件主题
message.setSubject("主题");
//邮件内容
message.setText("邮件内容");
//接收人,可以写多个
message.setTo("2500176776@qq.com");
//发送人 ,注意这里一定要和配置文件中的发送人地址一致
message.setFrom("2500176776@qq.com");

//通过自带的邮件发送类发送
javaMailSender.send(message);
return msg;
}catch (Exception e){
msg="邮件发送失败";
return msg;
}
}

一个复杂邮件

项目中要把它提成一个方法,这里先偷懒

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
public String MimeMailSend()  {
String msg="邮件发送成功";
try {
//复杂邮件实例
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
//复杂邮件的助手实例 构造方法中的Boolean值为是否支持多文件
MimeMessageHelper helper=new MimeMessageHelper(mimeMessage,true);
//邮件主题
helper.setSubject("附件邮件主题");
//邮件内容 Boolean为是否支持html语法
helper.setText("<h1>你好</h1>",true);
//接收人,可以写多个
helper.setTo("2500176776@qq.com");
//发送人 ,注意这里一定要和配置文件中的发送人地址一致
helper.setFrom("2500176776@qq.com");
//添加附件 名字 File中是附件的绝对地址
helper.addAttachment("1.png",new File("D:\\win10桌面存放\\java\\赫夫曼思想.png"));
//代理类发送
javaMailSender.send(mimeMessage);
return msg;
}catch (Exception e){
msg="邮件发送失败";
return msg;
}
}

将图片插入在邮件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void sendImgResMail() throws MessagingException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);
helper.setSubject("这是一封测试邮件");
helper.setFrom("bai211425401@qq.com");
helper.setTo("1712900841@qq.com");
helper.setSentDate(new Date());
helper.setText("<p>hello 大家好,我是一封测试邮件,我包含了两张图片,分别如下</p><p>第一张图片:</p><img src='cid:p01'/><p>第二张图片:</p><img src='cid:p02'/>",true);

helper.addInline("p01",new FileSystemResource(new File("C:\\Users\\bai\\Pictures\\Camera Roll\\img\\1.png")));

helper.addInline("p02",new FileSystemResource(new File("C:\\Users\\bai\\Pictures\\Camera Roll\\img\\2.png")));
javaMailSender.send(mimeMessage);
}

注意:

在实际开发中,我们不可能让用户等待邮件发送完毕,所以我们要将邮件发送变成一个异步任务

在邮件发送的类上加入注解@Async声明是一个异步任务

1
2
@Async
public class MailSend {}

在SpringBoot主启动类中加入注解@EnableAsync开启异步任务

1
2
3
4
5
6
7
@EnableAsync//开启异步任务
@SpringBootApplication
public class SpringbootAsyncApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootAsyncApplication.class, args);
}
}

文件上传

前端

1
2
3
4
<form action="/fileUpload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="上传">
</form>

后端接口

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
@PostMapping("/fileUpload")
@ResponseBody
public String fileUpload(MultipartFile file, HttpServletRequest request) {
String format = sdf.format(new Date());
//getServletContext获取服务器的项目地址 SpringBoot集成的tomcat的地址
String path = request.getServletContext().getRealPath("/img/") + format;
System.out.println("文件保存路径:" + path);
File realPath = new File(path);
if (!realPath.exists()) {
realPath.mkdirs();
}
//得到文件名
String oldName = file.getOriginalFilename();
//设置新文件名更具uuid
String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));

try {
//将文件转移到realPath路径 文件名为newName
file.transferTo(new File(realPath, newName));
//前端可访问的地址 getScheme()得到项目的协议使http还是https
// getServerName()得到项目名
//getServerPort() 得到项目端口
String overPath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + "/img/" + format + newName;
return overPath;
} catch (IOException e) {
e.printStackTrace();
}
return "error";
}

ajax请求

axaj请求后端接口基本不用变

前端(记住需要导入jquery)

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
<div id="msg"></div>
<input type="file" id="file">
<input type="button" onclick="UpLoad()" value="提交">

<script>
function UpLoad() {
var file=$("#file")[0].files[0];
var formDate=new FormData();
formDate.append("file",file);
console.log("开始");
$.ajax({
type:"post",
url:"/fileUpload",
/*!!!!!
processData:处理数据
默认情况下,processData 的值是 true,其代表以对象的形式上传的数据都会被转换为字符串的形式上传。而当上传文件的时候,则不需要把其转换为字符串,因此要改成false */
processData: false,
/*!!!!!
* contentType:发送数据的格式
和 contentType 有个类似的属性是 dataType , 代表的是期望从后端收到的数据的格式,一般会有 json 、text……等
而 contentType 则是与 dataType 相对应的,其代表的是 前端发送数据的格式

默认值:application/x-www-form-urlencoded
代表的是 ajax 的 data 是以字符串的形式 如 id=2019&password=123456
使用这种传数据的格式,无法传输复杂的数据,比如多维数组、文件等

有时候要注意,自己所传输的数据格式和ajax的contentType格式是否一致,如果不一致就要想办法对数据进行转换
把contentType 改成 false 就会改掉之前默认的数据格式,在上传文件时就不会报错了。*/
contentType: false,
data:formDate,
success:function (msg) {
$("#msg").html(msg);
}
}
)
}
</script>

多文件上传

前端

前端只需要设置 input标案加入属性 multiple 实现多文件上传

1
2
3
4
5
<form action="/fileUploads" method="post" enctype="multipart/form-data">
<!-- multiple选择多个值 在file中也就是选择多个文件-->
<input type="file" name="files" multiple>
<input type="submit" value="上传">
</form>

后端

后端大致一样

接收的MultipartFile改为数组形式 存储多个文件

最后遍历该数组并保存

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
@PostMapping("/fileUploads")
@ResponseBody
public String fileUploads(MultipartFile[] files, HttpServletRequest request) {
String format = sdf.format(new Date());
//getServletContext获取服务器的项目地址 SpringBoot集成的tomcat的地址
String path = request.getServletContext().getRealPath("/img/") + format;
System.out.println("文件保存路径:" + path);
File realPath = new File(path);
if (!realPath.exists()) {
realPath.mkdirs();
}

for (MultipartFile file : files) {

String oldName = file.getOriginalFilename();
String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));

try {
file.transferTo(new File(realPath, newName));
String overPath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + "/img/" + format + newName;
System.out.println(overPath);
} catch (IOException e) {
e.printStackTrace();
}

}

return "success";
}

18,定时任务

注解@EnableScheduling

开启定时任务,在主启动类上加

1
2
3
4
5
6
7
@EnableScheduling//开启定时任务
@SpringBootApplication
public class SpringbootAsyncApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootAsyncApplication.class, args);
}
}

注解@scheduled

通过cron表达式进行定时,作用域在方法上,不要加在类上

星期天到星期六的每月每日每时每分每秒执行hello方法

1
2
3
4
@org.springframework.scheduling.annotation.Scheduled(cron = "* * * * * 0-6 ")
public void hello(){
System.out.println("hello");
}

cron表达式

image-20210522161627705

常用表达式例子

(1)0 0 2 1 * ? * 表示在每月的1日的凌晨2点调整任务

(2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业

(3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作

(4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点

(5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时

(6)0 0 12 ? * WED 表示每个星期三中午12点

(7)0 0 12 * * ? 每天中午12点触发

(8)0 15 10 ? * * 每天上午10:15触发

(9)0 15 10 * * ? 每天上午10:15触发

(10)0 15 10 * * ? * 每天上午10:15触发

(11)0 15 10 * * ? 2005 2005年的每天上午10:15触发

(12)0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发

(13)0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发

(14)0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发

(15)0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发

(16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发

(17)0 15 10 ? * MON-FRI 周一至周五的上午10:15触发

(18)0 15 10 15 * ? 每月15日上午10:15触发

(19)0 15 10 L * ? 每月最后一日的上午10:15触发

(20)0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发

(21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发

(22)0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发

学习网址:https://www.cnblogs.com/javahr/p/8318728.html

在线转换器网址:https://qqe2.com/cron 也可以将cron表达式转为看得懂的文字

19,Dubbo

​ Dubbo(读音[ˈdʌbəʊ])是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 [1] Spring框架无缝集成。

​ Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。

架构

Dubbo 架构

dubbo-architucture

节点角色说明

节点 角色说明
Provider 暴露服务的服务提供方
Consumer 调用远程服务的服务消费方
Registry 服务注册与发现的注册中心
Monitor 统计服务的调用次数和调用时间的监控中心
Container 服务运行容器

调用关系说明

  1. 服务容器负责启动,加载,运行服务提供者。
  2. 服务提供者在启动时,向注册中心注册自己提供的服务。
  3. 服务消费者在启动时,向注册中心订阅自己所需的服务。
  4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

实现Dubbo和zookeeper

zookeeper

  1. 前往zookeeper官网下载:https://zookeeper.apache.org/releases.html 3.6.3为稳定版
    • ​ 下载完成后解压,并运行/bin目录下的zkServer.cmd文件启动zookeeper
    • image-20210522214910199

dubbo-admin

  1. 前往下载dubbo在GetHub上托管的dubbo-admin

    • dubbo-admin是是一个可视化管理dubbo和zookeeper注册中心的网页
    • 网址:https://github.com/apache/dubbo-admin/tree/master 建议下载master分支的debbo,最新版是vue分离的比较折腾
    • 在项目dubbo-admin-master下,运行cmd,执行mvn clean package -Dmaven.test.skip=true将项目打成jar包
    • 在子项目dubbo-admin的target目录运行打包好的jar包

    image-20210522215827776

构建项目

  1. 在IDEA中构建两个项目,分别是提供者Provider和消费者Consumer

image-20210522220052435

配置端口,避免端口冲突

server.port=XXXX

分别在两个项目中导入依赖

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
<!--zkclient客户端-->
<!-- https://mvnrepository.com/artifact/com.github.mxsm/zkclient-spring-boot-starter -->
<dependency>
<groupId>com.github.mxsm</groupId>
<artifactId>zkclient-spring-boot-starter</artifactId>
<version>1.0.1</version>
</dependency>

<!--dubbo依赖-->
<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-spring-boot-starter -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.11</version>
</dependency>


<!--解决日志冲突-->
<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-framework -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.1.0</version>
</dependency>


<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.1.0</version>
</dependency>


<!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.7.0</version>
<!--剔除log4日志-->
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>

springboot-provider

编写配置

1
2
3
4
5
6
7
server.port=8081
#服务器的名字
dubbo.application.name=provider-server
#注册中心的地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
#该项目下的那些服务被注册,并暴露给注册中心zookeeper
dubbo.scan.base-packages=cn.xpp011.service

在提供者springboot-provider项目中书写想要暴露的服务

注意TicketServiceImpl类实现接口TicketService,接口TicketService中就一个方法ticket

1
2
3
4
5
6
7
8
9
package cn.xpp011.service;
@DubboService//将该服务类在注册中心注册
@Service
public class TicketServiceImpl implements TicketService{
@Override
public String ticket() {
return "《成都-->上海》";
}
}

在主启动类中加入注解@EnableDubbo启动Dubbo

1
2
3
@SpringBootApplication
@EnableDubbo
public class SpringbootProviderApplication {...}

springboot-consumer

编写配置

1
2
3
4
5
6
7
server.port=8082

#消费者服务的名字
dubbo.application.name=consumer-server

#注册中心的地址
dubbo.registry.address=zookeeper://127.0.0.1:2181

在消费者类中编写服务类,同时调用提供者springboot-provider项目中暴露的服务类

注意如果想要拿到别的项目暴露的服务类,需要编写该服务类实现的接口,包路径也必须相同

image-20210522221032294

拿到其他项目暴露服务类

1
2
3
4
5
6
7
8
9
10
11
@Service
public class ConsumerTicket {

@DubboReference//从注册中心拿取类型为TicketService,同时包路径为cn.xpp011.service

TicketService ticketService;
public void consumer(){
String ticket = ticketService.ticket();
System.out.println("购买了:"+ticket);
}
}

运行消费者springboot-consumer

image-20210522221316160


最后可以在dubbo-admin的网页地址http://localhost:7001

可视化的看到暴露的服务类

image-20210522221433991

思考

1,为什么dubbo-admin可以可视化的看到提供者暴露的服务类呢

image-20210522222006051

在dubbo-admin-master的配置文件中我们看到了,它已经配置了zookeeper注册中心的地址。所以dubbo-admin拿到了注册中心的数据。

所以我们zookeeper注册中心的端口修改时,我们的dubbo-admin-master的配置文件也要修改哦;

2,那zookeeper注册中心是怎么拿到提供者暴露的服务类的呢

其实在提供者springboot-provider 项目中配置了注册中心的地址

image-20210522222331445

并提供服务类的注解@DubboService将该服务类在注册中心注册

3,那消费者是怎么拿到暴露的服务的呢

这就比较简单了

消费者springboot-consumer项目也配置了zookeeper注册中心的地址

image-20210522222545148

并通过注解@DubboReference从注册中心拿取相应类型,相应包路径的服务类

20,跨域请求(cors)

1.什么是跨域?

跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。

例如:a页面想获取b页面资源,如果a、b页面的协议、域名、端口、子域名不同,所进行的访问行动都是跨域的,而浏览器为了安全问题一般都限制了跨域访问,也就是不允许跨域请求资源。注意:跨域限制访问,其实是浏览器的限制。理解这一点很重要!!!

同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域;

img

那SpringBoot如何解决跨域请求呢

注解

在SpringBoot中解决跨域请求是相当简单的

只需要注解@CrossOrigin(“域名地址:端口”)

当我们加上该注解的使用那么该方法或者类下的所有方法 都会被指定域名:端口允许方法

实现Cors 跨域资源共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class HelloController {

//加上该注解后表示该方法 支持地址为http://localhost:8082的跨域请求 作用域为类和方法
//@CrossOrigin("http://localhost:8082")
@GetMapping("/hello")
public String gethello(){
return "hello cors:get";
}

@PutMapping("/hello")
public String puthello(){
return "hello cors:put";
}
}

配置类

但是在项目很大的使用,加注解的方式就显得不是很明智了,那么通过Java配置类的方式解救跨域请求

实现WebMvcConfigurer接口,并实现跨域资源共享映射方法addCorsMappings方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//由于注解方式可能在项目大时比较繁琐
//所以这里可以使用配置类的方式实现 就不需要加很多注解
@Configuration
public class CorsConfig implements WebMvcConfigurer {
//实现跨域资源共享映射方法addCorsMappings
@Override
public void addCorsMappings(CorsRegistry registry) {
//addMapping 设置请求地址
registry.addMapping("/**")
//设置请求域名
.allowedOrigins("http://localhost:8082")
//设置跨域的请求头 *表示任何请求头都可以
.allowedHeaders("*")
//设置请求的方法 get,post,del *表示任何请求方式都可以
.allowedMethods("*")
//设置探测请求后多少秒不需要再探测 默认1800秒
.maxAge(3 * 1000);
}
}

关于探测请求

在put请求方式中,它第一次会发送两次请求

第一次是探测请求,判断服务器是否支持该请求地址,如果支持那么就会发送第二次正式请求

image-20210530131140246

那探测请求其实不用每次都发,我们可以设置一个探测请求有效期,在该有效期内就不用每次都发探测请求了,也就是配置类的maxAge()方法

image-20210530131253860

21,系统启动任务

CommandLineRunner

系统启动任务,可以在系统启动之前做一些事情,比如初始化参数等等

自定义Java配置类 实现启动命令接口CommandLineRunner

实现run方法在其中可以做一些系统启动任务

1
2
3
4
5
6
7
8
9
@Configuration
@Order(100)//系统启动任务的优先级 默认为2的31次方减一 数字越小优先级越高
public class MyComm implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
//这里的参数args为主启动类run方法传递的参数
System.out.println("系统启动任务1,参数:"+ Arrays.toString(args));
}
}

注意注解@Order当我们有多个系统启动任务类时,设置优先级就很必要

系统启动任务的优先级 默认为2的31次方减一 数字越小优先级越高

ApplicationRunner

ApplicationRunner也是一个系统启动类,只不过它可以获取kv键值对形式的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@Order(99)
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
String[] sourceArgs = args.getSourceArgs();
//获取全部参数 包括kv键值对的形式
System.out.println("sourceArgs : "+ Arrays.toString(sourceArgs));

//获取值 只包括kv键值对的形式
List<String> nonOptionArgs = args.getNonOptionArgs();
nonOptionArgs.forEach((v)->{
System.out.println("v : "+v);
});

//获取kv键值对的形式的key
Set<String> optionNames = args.getOptionNames();
optionNames.forEach((k)->{
//getOptionValues更具key得到value值
System.out.println(k+" : "+args.getOptionValues(k));
});
}
}

执行效果

image-20210531135600115

22,SpringBoot—Aop

前置

JoinPoint 对象

JoinPoint对象****封装了SpringAop中切面方法的信息*,在切面方法中添加*JoinPoint参数*,就可以获取到封装了该方法信息的*JoinPoint对象.****
常用api:

方法名 功能
Signature getSignature(); 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
Object[] getArgs(); 获取传入目标方法的参数对象
Object getTarget(); 获取被代理的对象
Object getThis(); 获取代理对象

ProceedingJoinPoint对象

*ProceedingJoinPoint对象是JoinPoint的子接口,该对象只用在@Around的切面方法中,*
添加了
Object proceed() throws Throwable //执行目标方法
Object proceed(Object[] var1) throws Throwable //传入的新的参数去执行目标方法
两个方法.

导入aop依赖

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

自定义切面类

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
package cn.xpp011.Config;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect//切面
public class MyAspect {
//定制方法规则 这个方法只是单纯的指定方法规则 指定一个切点
//返回值 指定包 指定方法 指定参数
@Pointcut("execution(* cn.xpp011.Service.*.*(..))")
public void pc1(){}

//方法执行前
@Before("pc1()")
public void before(JoinPoint jp){
String name = jp.getSignature().getName();
System.out.println(name+"方法执行前");
}

//方法执行后
@After("pc1()")
public void After(JoinPoint jp){
String name = jp.getSignature().getName();
System.out.println(name+"方法执行后");
}

//方法有返回值的aop returning指定返回参数
@AfterReturning(value = "pc1()",returning = "o")
public Object AfterReturning(JoinPoint jp,Object o){
String name = jp.getSignature().getName();
System.out.println(name+"方法的返回值:"+o);

return o;
}

//方法异常aop e为该方法报出的异常 Throwing抛出什么异常
@AfterThrowing(value = "pc1()",throwing = "e")
public void AfterThrowing(JoinPoint jp,Exception e){
String name = jp.getSignature().getName();
System.out.println(name+"方法报出的异常:"+e.getMessage());
}

//环绕通知
@Around("pc1()")
public Object a(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕通知");
//需要在方法执行前做什么只需要在这里写就可以了
Object proceed = pjp.proceed();//proceed执行该方法 返回值就是这个方法的返回值
//需要在方法执行后做什么只需要在这里写就可以了


//需要加入异常只需try catch包裹即可

//这里可以对返回值进行修改
return proceed;
}

}

23,整合Redis

Linux安装redis

1.首先下载 Redis,下载地址https://redis.io/,下载获得 redis-4.0.8.tar.gz 后将它放入我们的 Linux 目录 /opt

img

2./opt 目录下,对文件进行解压,解压命令: tar -zxvf redis-4.0.8.tar.gz ,如下:

img

3.解压完成后出现文件夹:redis-4.0.8,进入到该目录中: cd redis-4.0.8

img

4.在 redis-4.0.8 目录下执行 make 命令进行编译

img

5.如果 make 完成后继续执行 make install 进行安装

img

OK,至此,我们的 redis 就算安装成功了。

6.在我们启动之前,需要先做一个简单的配置:修改 redis.conf 文件,将里面的 daemonize no 改成 yes,让服务在后台启动,如下:

img
img

7.启动,通过redis-server redis.conf命令启动redis,如下:

img

8.测试

首先我们可以通过 redis-cli 命令进入到控制台,然后通过 ping 命令进行连通性测试,如果看到 pong ,表示连接成功了,如下:

img

9.关闭,通过 shutdown 命令我们可以关闭实例,如下:

img

远程连接

1,修改redis服务器的配置文件
vim redis.conf

2,注释以下绑定的主机地址

#bind 127.0.0.1

image-20210608143105417

3,关闭保护模式

image-20210608143204943

组合拳

4,开启端口6379 firewall-cmd --zone=public --add-port=6379/tcp --permanent

5,重启防火墙 firewall-cmd --reload

6,重启redis

整合redis

加入依赖

redis必须和security一起使用

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

配置文件

image-20210608143650149

业务类

image-20210608143522343

24,Linux安装Nginx

  1. 首先下载 Nginx(可去官网nginx.org获取最新下载地址)
1
wget http://nginx.org/download/nginx-1.17.0.tar.gz

然后解压下载的目录,进入解压目录中,在编译安装之前,需要安装两个依赖:(如果没有安装C++编译环境则需要先安装C++编译环境)

1
2
yum -y install pcre-devel
yum -y install openssl openssl-devel

然后开始编译安装:

1
2
3
./configure
make
make install

装好之后,默认安装位置在 :

1
/usr/local/nginx/sbin/nginx

进入到该目录的 sbin 目录下,执行 nginx 即可启动 Nginx

图片

Nginx 启动成功之后,在浏览器中直接访问 Nginx 地址:

图片

看到如上页面,表示 Nginx 已经安装成功了。

如果修改了 Nginx 配置,则可以通过如下命令重新加载 Nginx 配置文件:

1
./nginx -s reload

25,Session共享

Session共享的前提是有两个及以上的服务器,这里就涉及到分布式等操作

image-20210608145652056

在我们创建项目时,记住需要勾选Spring Session依赖,这样SpringBoot就可以帮助我们自动配置一些Session配置

我们启动两个tomcat,这两个tomcat配置了同一个redis

此时我们访问这两个项目就会得到相同的session

image-20210608151109024

我们查看redis,发现他已经存放了相关的session信息

image-20210608151244337

使用Nginx进行负载均衡

将jar包上传至linux,启动两个tomcat,判断tomcat是否可以正常访问

image-20210608154457549

配置Nginx,实现负载均衡,

加入Nginx的默认路径

image-20210608154553630

修改配置文件Nginx.config

image-20210608154624680

image-20210608154338339

解释:

在这段配置中:

  1. upstream 表示配置上游服务器
  2. javaboy.org 表示服务器集群的名字,这个可以随意取名字
  3. upstream 里边配置的是一个个的单独服务
  4. weight 表示服务的权重,意味者将有多少比例的请求从 Nginx 上转发到该服务上
  5. location 中的 proxy_pass 表示请求转发的地址,/ 表示拦截到所有的请求,转发转发到刚刚配置好的服务集群中
  6. proxy_redirect 表示设置当发生重定向请求时,nginx 自动修正响应头数据(默认是 Tomcat 返回重定向,此时重定向的地址是 Tomcat 的地址,我们需要将之修改使之成为 Nginx 的地址)。

配置完成后,将本地的 Spring Boot 打包好的 jar 上传到 Linux ,然后在 Linux 上分别启动两个 Spring Boot 实例:

1
2
nohup java -jar sessionshare-0.0.1-SNAPSHOT.jar --server.port=8080 &
nohup java -jar sessionshare-0.0.1-SNAPSHOT.jar --server.port=8081 &

其中

  • nohup 表示当终端关闭时,Spring Boot 不要停止运行
  • & 表示让 Spring Boot 在后台启动

配置完成后,重启 Nginx:

1
/usr/local/nginx/sbin/nginx -s reload

Nginx 启动成功后,我们首先手动清除 Redis 上的数据,然后访问 192.168.66.128/set 表示向 session 中保存数据,这个请求首先会到达 Nginx 上,再由 Nginx 转发给某一个 Spring Boot 实例:

img

如上,表示端口为 8081Spring Boot 处理了这个 /set 请求,再访问 /get 请求:

img

可以看到,/get 请求是被端口为 8080 的服务所处理的。

26,SpringBoot集成RabbitMQ

简介

RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。

AMQP :Advanced Message Queue,高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。

RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。具体特点包括:

  1. 可靠性(Reliability)
    RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。
  2. 灵活的路由(Flexible Routing)
    在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功能,RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。
  3. 消息集群(Clustering)
    多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。
  4. 高可用(Highly Available Queues)
    队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
  5. 多种协议(Multi-protocol)
    RabbitMQ 支持多种消息队列协议,比如 STOMP、MQTT 等等。
  6. 多语言客户端(Many Clients)
    RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby 等等。
  7. 管理界面(Management UI)
    RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker 的许多方面。
  8. 跟踪机制(Tracing)
    如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。
  9. 插件机制(Plugin System)
    RabbitMQ 提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。

20190903141227300

安装和使用

首先在Linux中使用docker安装RabbitMQ ——执行以下命令

docker run -d --hostname my-rabbit --name some-rabbit -p 15672:15672 -p 5672:5672 rabbitmq:3-management

说明:

-p  端口映射

–name  容器实例名称

-d  后台运行

5672  客户端与rabbitmq通信端口

15672  rabbitmq web管理端口  访问http://ip:15672  默认用户:guest  默认密码:guest

docker查看运行状态

docker ps

image-20210611225137212

端口说明

1
2
3
4
5
6
7
4369 -- erlang发现口

5672 --client端通信口

15672 -- 管理界面ui端口

25672 -- server间内部通信口

在防火墙中开放指定端口

开启指定端口

firewall-cmd --zone=public --add-port=15672/tcp --permanent

重启防火墙

firewall-cmd --reload

交换机模式

常用的交换机有以下三种,因为消费者是从队列获取信息的,队列是绑定交换机的(一般),所以对应的消息推送/接收模式也会有以下几种:

Direct Exchange

直连型交换机,根据消息携带的路由键将消息投递给对应队列。

大致流程,有一个队列绑定到一个直连交换机上,同时赋予一个路由键 routing key 。
然后当一个消息携带着路由值为X,这个消息通过生产者发送给交换机时,交换机就会根据这个路由值X去寻找绑定值也是X的队列。

Fanout Exchange

扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。 这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。

Topic Exchange

主题交换机,这个交换机其实跟直连交换机流程差不多,但是它的特点就是在它的路由键和绑定键之间是有规则的。
简单地介绍下规则:

  • (星号) 用来表示一个单词 (必须出现的)

(井号) 用来表示任意数量(零个或多个)单词

通配的绑定键是跟队列进行绑定的,举个小例子
队列Q1 绑定键为 .TT. 队列Q2绑定键为 TT.#
如果一条消息携带的路由键为 A.TT.B,那么队列Q1将会收到;
如果一条消息携带的路由键为TT.AA.BB,那么队列Q2将会收到;

主题交换机是非常强大的,为啥这么膨胀?
当一个队列的绑定键为 “#”(井号) 的时候,这个队列将会无视消息的路由键,接收所有的消息。
当 * (星号) 和 # (井号) 这两个特殊字符都未在绑定键中出现的时候,此时主题交换机就拥有的直连交换机的行为。
所以主题交换机也就实现了扇形交换机的功能,和直连交换机的功能。

另外还有 Header Exchange 头交换机 ,Default Exchange 默认交换机,Dead Letter Exchange 死信交换机,这几个该篇暂不做讲述。

SpringBoot配置信息

1
2
3
4
spring.rabbitmq.host=192.168.32.131
spring.rabbitmq.username=xpp011
spring.rabbitmq.password=xpp011
spring.rabbitmq.port=5672

代码实现

直连交换机模式(Direct Exchange)

RabbitDirectConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//直连交换机配置
@Configuration
public class RabbitDirectConfig {
private static final String DIRECTNAME="directExchange";

@Bean
public Queue queue(){
//消息队列名字
return new Queue("directQueue");
}

@Bean
DirectExchange directExchange(){
//DIRECTNAME 消息队列交换机的名字 true 重启后交换机是否有效 false 长期未使用是否删除
return new DirectExchange(DIRECTNAME,true,false);
}

//将消息队列和交换机绑定在一起
@Bean
Binding binding(){
//bind消息队列 to 交换机 with 绑定后的名字
return BindingBuilder.bind(queue()).to(directExchange()).with("direct");
}
}

消费者

1
2
3
4
5
6
7
8
9
10
11
//消费者监听者
@Component
public class DirectReceiver {

//监听那个队列
@RabbitListener( queues = "directQueue")
//处理方法1
public void handler1(String msg){
System.out.println("handler1 -> "+ msg);
}
}

测试

1
2
3
4
5
@Test
void contextLoads() {
//通过SpringBoot自带的模板进行消息发送 exchange 交换机的名字 routingKey 路由的名字 object 发送的消息体
rabbitTemplate.convertAndSend("directExchange","direct","你好 rabbit消息队列");
}

扇形交换机(Fanout Exchange)

RabbitFanoutConfig配置类

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
@Configuration
public class RabbitFanoutConfig {
public static final String FANOUTNAME="fanoutExchange";

@Bean
public Queue queueOne(){
return new Queue("fanoutQueueOne");
}

@Bean
public Queue queueTwo(){
return new Queue("fanoutQueueTwo");
}

@Bean
FanoutExchange fanoutExchange(){
return new FanoutExchange(FANOUTNAME,true,false);
}

//扇形交换机转发时不需要绑定routingKey 它是直接群发到它绑定的路由上
@Bean
Binding bindingOne(){
return BindingBuilder.bind(queueOne()).to(fanoutExchange());
}

@Bean
Binding bindingTwo(){
return BindingBuilder.bind(queueTwo()).to(fanoutExchange());
}
}

监听类FanoutReceiver

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

@RabbitListener(queues = "fanoutQueueOne")
public void ListenerOne(String msg){
System.out.println("ListenerOne -> "+ msg);
}

@RabbitListener(queues = "fanoutQueueTwo")
public void ListenerTwo(String msg){
System.out.println("ListenerTwo -> "+ msg);
}
}

测试

1
2
3
4
5
@Test
void FanoutTest(){
//扇形交换机 无效写入routingKey
rabbitTemplate.convertAndSend(RabbitFanoutConfig.FANOUTNAME,null,"你好 扇形交换机转发");
}

主题交换机(Topic Exchange)

* (星号) 用来表示一个单词 (必须出现的)
# (井号) 用来表示任意数量(零个或多个)单词

同时我们在写入toutingKey时,通配符连接点必须加. 不然无法识别通配符

image-20210612110138478

RabbitTopicConfig配置类

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 RabbitTopicConfig {

public static final String TOPICNAME="topicExchange";

@Bean
public Queue queueXiaoMi(){
return new Queue("XiaoMiQueue");
}

@Bean
public Queue queueSanXin(){
return new Queue("SanXinQueue");
}

@Bean
public Queue queueHuaVei(){
return new Queue("HuaWeiQueue");
}

@Bean
public Queue Topicqueue(){
return new Queue("TopicQueue");
}

@Bean
TopicExchange topicExchange(){
return new TopicExchange(TOPICNAME,true,false);
}

@Bean
Binding Topicbinding(){
return BindingBuilder.bind(Topicqueue()).to(topicExchange()).with("Topic#");
}

@Bean
Binding XiaoMibinding(){
return BindingBuilder.bind(queueXiaoMi()).to(topicExchange()).with("#.XiaoMi");
}

@Bean
Binding SanXinbinding(){
return BindingBuilder.bind(queueSanXin()).to(topicExchange()).with("#.SanXin");
}

@Bean
Binding HuaVeibinding(){
return BindingBuilder.bind(queueHuaVei()).to(topicExchange()).with("#.HuaWei");
}
}

监听类TopicReceiver

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

@RabbitListener(queues = "XiaoMiQueue")
public void XiaoMi(String msg){
System.out.println("XiaoMi -> "+ msg);
}

@RabbitListener(queues = "SanXinQueue")
public void SanXin(String msg){
System.out.println("SanXin -> "+ msg);
}

@RabbitListener(queues = "HuaWeiQueue")
public void HuaWei(String msg){
System.out.println("HuaWei -> "+ msg);
}

@RabbitListener(queues = "TopicQueue")
public void queue(String msg){
System.out.println("queue -> "+ msg);
}
}

测试类

当我们写入toutingKey时 它会去寻找符合匹配的toutingKey,并发送到绑定的路由上

1
2
3
4
5
6
@Test
void TopicTest(){
rabbitTemplate.convertAndSend(RabbitTopicConfig.TOPICNAME,"Topic.XiaoMi","你好小米");
rabbitTemplate.convertAndSend(RabbitTopicConfig.TOPICNAME,"Topic.HuaWei","你好华为");
rabbitTemplate.convertAndSend(RabbitTopicConfig.TOPICNAME,"Topic.SanXin","你好三星");
}

SpringSecurity

configure(HttpSecurity http)方法设置登录成功处理器和登录失败处理器,返回JSON数据

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
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasAnyRole("admin","user")
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/doLogin")
.loginPage("/login")
.usernameParameter("username")
.passwordParameter("password")
.successHandler(new AuthenticationSuccessHandler() {//登录成功处理器
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");//前后端分离返回JSON
Map<String,Object> map=new HashMap<>();
map.put("status",200);
map.put("msg",authentication.getPrincipal());
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
})
.failureHandler(new AuthenticationFailureHandler() {//登录失败处理器
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
Map<String,Object> map=new HashMap<>();
map.put("status",401);//登录失败 状态吗401
if (e instanceof LockedException){
map.put("msg","账户被锁住,登录失败");
}else if (e instanceof BadCredentialsException){
map.put("msg","用户名或者密码错误,登录失败");
}else if (e instanceof DisabledException){
map.put("msg","账户被锁定,登录失败");
}else {
map.put("msg","登录失败");
}
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
})
.permitAll()//注意书写顺序 permitAll一定放在最后
.and()
.csrf().disable();
}

角色继承

该方法直接写在Security的配置类中即可

意思为角色为admin的可以干角色user的事

角色user的可以干visitor的事

注意两个关系之间需要以\n风格,可以看一眼源码

1
2
3
4
5
6
7
8
//Security角色继承
@Bean
public RoleHierarchy roleHierarchy(){
RoleHierarchyImpl roleHierarchy=new RoleHierarchyImpl();
String role="ROLE_admin > ROLE_user \n ROLE_user > ROLE_visitor";
roleHierarchy.setHierarchy(role);
return roleHierarchy;
}

Security动态权限管理

关键类

  • AntPathMatcher --Spring自带的路径匹配类
  • FilterInvocation --调用过滤器类,可获得请求地址
  • Authentication --包含用户登录信息
  • UserDetails --用户的详细说明接口,创建用户实体类时继承该接口
  • UserDetailsService --用户的服务层接口
  • WebSecurityConfigurerAdapter --自定义Security配置类需要继承的类
  • FilterInvocationSecurityMetadataSource --自定义过滤器类实现的接口(匹配当前路径的权限)
  • AccessDecisionManager --访问决策管理器(做出最终的访问控制(授权)决定。)

AuthenticationException子类异常详解

这个异常是在登录的时候出现错误时抛出的异常,比如账户锁定,证书失效等,先来看下AuthenticationException常用的的子类:

UsernameNotFoundException 用户找不到

BadCredentialsException 坏的凭据

AccountStatusException 用户状态异常它包含如下子类

AccountExpiredException 账户过期

LockedException账户锁定

DisabledException 账户不可用

CredentialsExpiredException 证书过期

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
38
39
40
41
42
43
44
45
46
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean locked;
private List<Role> roles;


//getAuthorities获取权限
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> list=new ArrayList<>();
for (Role role : roles) {
list.add(new SimpleGrantedAuthority(role.getName()));
}
return list;
}

//账户是否过期 true是没有
@Override
public boolean isAccountNonExpired() {
return true;
}

//用户是否被锁住 true是没有
@Override
public boolean isAccountNonLocked() {
return !locked;
}

//凭证是否过期 true是没有
@Override
public boolean isCredentialsNonExpired() {
return true;
}

//用户是否被激活 true是被激活
@Override
public boolean isEnabled() {
return enabled;
}


//get and set .....
}

UserService

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

@Autowired
private UserMapper userMapper;

//调用dao层接口 得到用户
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名得到用户
User user=userMapper.loadUserByUsername(username);
//用户不存在抛异常
if (user==null) throw new UsernameNotFoundException("用户不存在");
//根据方法得到用户的权限
user.setRoles(userMapper.getRoleById(user.getId()));
//返回用户
return user;
}
}

MyFilert

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
@Component
//Security的过滤器管理器 调用
public class MyFilter implements FilterInvocationSecurityMetadataSource {

@Autowired
MenuMapper menuMapper;

//Spring自带的路径匹配列类 支持通配符
static AntPathMatcher antPathMatcher=new AntPathMatcher();


@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {

//转换为过滤器
FilterInvocation invocation=(FilterInvocation) object;
//得到当前的请求地址
//注意这里不用调用getFullRequestUrl方法 会得到全路径导致匹配不成功
String requestUrl=invocation.getRequestUrl();
System.out.println("当前请求地址 : "+requestUrl);
//得到数据所有请求地址所需要的权限
List<Menu> menuAll = menuMapper.getMenuAll();
for (Menu menu : menuAll) {
//判断当前的请求地址是否在数据库中记录
//由于我们存在数据中的路径是带通配符的,所以使用antPathMatcher匹配
if (antPathMatcher.match(menu.getPattern(),requestUrl)){
//得到当前地址需要的权限
List<Role> roles = menu.getRoles();
//由于该方法返回Collection类型 所以先将权限的名字取出 放到String数组中
String [] res=new String[roles.size()];
for (int i = 0; i < roles.size(); i++) {
res[i]=roles.get(i).getName();
}
//通过SecurityConfig的createList的类型转换后得到Collection类型
return SecurityConfig.createList(res);
}
}
//如果当前地址不在数据库记录 则该地址只需要登录即可浏览
return SecurityConfig.createList("ROLE_login");
}

@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}

//返回值为true
//这里clazz表示安全对象的类型,该方法用于告知调用者当前SecurityMetadataSource是否支持此类安全对象,只有支持的时候,才能对这类安全对象调用getAttributes方法。
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}

MyAccessDecisionManager

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
//访问决策管理器
//做出最终的访问控制(授权)决定。
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {

//configAttributes参数 即MyFilter类的getAttributes方法返回的权限名字
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
//获取当前用户的权限
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (ConfigAttribute configAttribute : configAttributes) {
if ("ROLE_login".equals(configAttribute.getAttribute())){
//AnonymousAuthenticationToken匿名认证失败的令牌
if (authentication instanceof AnonymousAuthenticationToken){
//拒绝访问异常
throw new AccessDeniedException("非法请求");
}else {
return;
}
}

for (GrantedAuthority authority : authorities) {
//configAttribute.getAttribute() 获取属性
//authority.getAuthority() 获取权限
if (configAttribute.getAttribute().equals(authority.getAuthority())){
return;
}
}
}
throw new AccessDeniedException("非法请求");
}

//支持安全类型对象
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}

@Override
public boolean supports(Class<?> clazz) {
return true;
}
}

SecurityConfig

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

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
UserService userService;

@Autowired
PasswordEncoder passwordEncoder;

@Autowired
MyAccessDecisionManager myAccessDecisionManager;

@Autowired
MyFilter myFilter;

@Bean
PasswordEncoder passwordEncoder(){
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置加密方式
auth.userDetailsService(userService).passwordEncoder(passwordEncoder);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
//withObjectPostProcessor 写入处理器
//FilterSecurityInterceptor Security的调用过滤器
http.authorizeRequests().withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
//设置自定的访问决策管理器
object.setAccessDecisionManager(myAccessDecisionManager);
//设置Security的数据源 即从数据库中的得到访问地址所需权限
object.setSecurityMetadataSource(myFilter);
return object;
}
})
.and()
.formLogin()
.permitAll()
.and().csrf().disable();
}
}

Security JSON登录

通过阅读UsernamePasswordAuthenticationFilter源码,我们得知,Security是通过request请求得到Username的,这种形式经常在servlet出现,那也就是Security是能通过表单登录,而无法通过JSON登录,这样对前后端分离是不友好的

image-20210610182151951

更改UsernamePasswordAuthenticationFilter,使其支持JSON登录,注意只是扩展功能,不是重写

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
//自定义的Username和Password过滤器 增加了JSON登录的格式
public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
//判断前端是否传递过来的格式是否为JSON格式
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
Map<String,String> map;
try {
//通过IO的形式解析request的Username和Password数据
map = new ObjectMapper().readValue(request.getInputStream(), Map.class);
} catch (IOException e) {
e.printStackTrace();
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}

String username = map.get("username");

username = (username != null) ? username : "";
username = username.trim();
String password = map.get("password");
password = (password != null) ? password : "";
System.out.println("JSON解析:"+username+":"+password);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
return super.attemptAuthentication(request, response);
}
}

编写好自定的登录过滤器后,将其添加在UsernamePasswordAuthenticationFilter内部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
...
//添加自定义过滤器到UsernamePasswordAuthenticationFilter
http.addFilterAt(myAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
}

//将自定义的登录过滤器创建
MyUsernamePasswordAuthenticationFilter myAuthenticationFilter() throws Exception {
MyUsernamePasswordAuthenticationFilter filter = new MyUsernamePasswordAuthenticationFilter();
//设置权限管理
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}
}

Security整合WebSocket

文档参考:https://developer.aliyun.com/article/763747

https://blog.csdn.net/achenyuan/article/details/80851512#java-config配置

HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。这种通信模型有一个弊端:HTTP 协议无法实现服务器主动向客户端发起消息。
这种单向请求的缺点,如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数 Web 应用程序将通过频繁的异步JavaScript和XML(AJAX)请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。

WebSocket 连接允许客户端和服务器之间进行全双工通信,以便任一方都可以通过建立的连接将数据推送到另一端。WebSocket 只需要建立一次连接,就可以一直保持连接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。

点对面(对所有在线的用户广播消息)

首先导入依赖,maven导入方便管理

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
<!--导入WebSocket的依赖包  maven导入方便管理-->

<!-- https://mvnrepository.com/artifact/org.webjars/sockjs-client -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>sockjs-client</artifactId>
<version>1.5.1</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.webjars/jquery -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.6.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.webjars/stomp-websocket -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>stomp-websocket</artifactId>
<version>2.3.4</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.webjars/webjars-locator-core -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
<version>0.47</version>
</dependency>

WebSocket的配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//WebSocket的配置类
@Configuration
//开启对WebSocket的支持
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {


//配置消息代理
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//配置一个广播式的消息代理 前缀是"/topic"
registry.enableSimpleBroker("/topic");
//上面的是通过代理类处理消息 而这里是配置使用方法处理消息 凡是前缀为/app开头的就是通过方法处理消息
registry.setApplicationDestinationPrefixes("/app");
}


//配置连接点 注册STOMP协议的节点,并映射到指定的URL
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//注册一个STOMP协议的endpoint,并指定使用SockJS协议
registry.addEndpoint("/chat").withSockJS();
}
}

处理消息的Controller类

1
2
3
4
5
6
7
8
9
10
@Controller
public class GreetingController {
//当浏览器向服务器发送STOMP请求时,通过@MessageMapping注解来映射/hello地址
@MessageMapping("/hello")
//当服务器有消息时,会对订阅了@SenfTo的路径的客户端发送消息
@SendTo("/topic/greetings")
public Message greeting(Message message){
return message;
}
}

前端页面

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
</head>
<body>
<table>
<tr>
<td>请输入用户名</td>
<td><input type="text" id="name"></td>
</tr>
<tr>
<td><input type="button" id="connect" value="连接"></td>
<td><input type="button" disabled="disabled" id="disconnect" value="断开连接"></td>
</tr>
</table>


<div id="chat" style="display: none">
<table>
<tr>
<td>请输入聊天内容</td>
<td><input type="text" id="content"></td>
<td><input type="button" id="send" value="发送"></td>
</tr>
</table>
<div id="conversation">群聊进行中....</div>
</div>


<script>
$(function (){
$("#connect").click(
function (){
/*开启连接方法*/
connect()
}
)
//断开连接点击事件
$("#disconnect").click(
function (){
if (stompClient != null){
//断开连接
stompClient.disconnect();
}
//设置连接和断开连接按钮的状态
setConnected(false);
}
)
//发送按钮点击事件
$("#send").click(
function (){
//连接点代理类 发送连接 将JSON解析成字符串
stompClient.send("/app/hello",{},JSON.stringify({"name":$("#name").val(),"content":$("#content").val()}))
}
)
}
)

var stompClient =null;

function connect() {
/*name为空时直接返回*/
if (!$("#name").val()){
return;
}
//创建SockJS 并连接 连接点(/chat)
var socket=new SockJS("/chat");
//创建STOMP客户端代理 地址为chat的代理
stompClient=Stomp.over(socket);
//开启连接
stompClient.connect({},function (success){//成功的回调函数
//调整连接按钮和断开连接按钮
setConnected(true);
//订阅广播地址 当该地址发送消息时 回调函数 msg
stompClient.subscribe("/topic/greetings",function (msg){
//将msg的body的信息解析成JSON
//showGreeting方法 将接收的信息设置到页面上
showGreeting(JSON.parse(msg.body));
});
})
}

//连接和断开连接按钮的状态
function setConnected(flag) {
$("#connect").prop("disabled",flag);
$("#disconnect").prop("disabled",!flag);
//消息列表的隐藏和显示
if (flag){
$("#chat").show();
}else {
$("#chat").hide();
}
}

//将消息添加到末尾
function showGreeting(msg) {
$("#conversation").append("<div>"+msg.name+":"+msg.content+"</div>");
}
</script>
</body>
</html>

点对点(将消息发送到指定用户上)

由于引入了用户的概念 ,导入 Security依赖

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

配置点对点发送的请求路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//WebSocket的配置类
@Configuration
//开启对WebSocket的支持
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {


//配置消息代理
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//配置一个广播式的消息代理 前缀是"/topic" 点对点消息路径 前缀是/queue
registry.enableSimpleBroker("/topic","/queue");
//上面的是通过代理类处理消息 而这里是配置使用方法处理消息 凡是前缀为/app开头的就是通过方法处理消息
registry.setApplicationDestinationPrefixes("/app");
}


//配置连接点 注册STOMP协议的节点,并映射到指定的URL
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//注册一个STOMP协议的endpoint,并指定使用SockJS协议
registry.addEndpoint("/chat").withSockJS();
}
}

点对点的Controller类

这里使用了SpringBoot的自动配置类 ,SimpMessagingTemplate消息模板,它可以指定用户发送

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
@Controller
public class GreetingController {

//Spring自带的消息模板
@Autowired
SimpMessagingTemplate messagingTemplate;

//当浏览器向服务器发送STOMP请求时,通过@MessageMapping注解来映射/hello地址
@MessageMapping("/hello")
//当服务器有消息时,会对订阅了@SenfTo的路径的客户端发送消息
@SendTo("/topic/greetings")
public Message greeting(Message message){
return message;
}


@MessageMapping("/one")
//点对点聊天
//Principal当前用户
public void one(Principal principal, Chat chat){
//设置当前用户的名字
chat.setFrom(principal.getName());
//通过convertAndSendToUser方法将消息发送给指定用户名字 接收人的名字 发送的请求 发送的请求体
messagingTemplate.convertAndSendToUser(chat.getTo(),"/queue/greetings",chat);
}
}

前端页面

注意一旦使用点对点 就要在指定的发送路径上加上/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
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
</head>
<body>
<table>
<tr>
<td><input type="button" id="connect" value="连接"></td>
<td><input type="button" disabled="disabled" id="disconnect" value="断开连接"></td>
</tr>

<tr>
<td>接送人</td>
<td><input type="text" id="toname"></td>
</tr>

</table>


<div id="chat" style="display: none">
<table>
<tr>
<td>请输入聊天内容</td>
<td><input type="text" id="content"></td>
<td><input type="button" id="send" value="发送"></td>
</tr>
</table>
<div id="conversation">群聊进行中....</div>
</div>


<script>
$(function (){
$("#connect").click(
function (){
/*开启连接方法*/
connect()
}
)
//断开连接点击事件
$("#disconnect").click(
function (){
if (stompClient != null){
//断开连接
stompClient.disconnect();
}
//设置连接和断开连接按钮的状态
setConnected(false);
}
)
//发送按钮点击事件
$("#send").click(
function (){
//接收人未指定直接返回
if (!$("#toname").val()){
return;
}
//连接点代理类 发送连接 将JSON解析成字符串
stompClient.send("/app/one",{},JSON.stringify({"to":$("#toname").val(),"content":$("#content").val()}))
}
)
}
)

var stompClient =null;

function connect() {
//创建SockJS 并连接 连接点(/chat)
var socket=new SockJS("/chat");
//创建STOMP客户端代理 地址为chat的代理
stompClient=Stomp.over(socket);
//开启连接
stompClient.connect({},function (success){//成功的回调函数
//调整连接按钮和断开连接按钮
setConnected(true);
//订阅广播地址 当该地址发送消息时 回调函数 msg
//注意一旦使用点对点 就要在指定的发送路径上加上/user前缀!!!!!!!!
stompClient.subscribe("/user/queue/greetings",function (msg){
//将msg的body的信息解析成JSON
//showGreeting方法 将接收的信息设置到页面上
showGreeting(JSON.parse(msg.body));
});
})
}

//连接和断开连接按钮的状态
function setConnected(flag) {
$("#connect").prop("disabled",flag);
$("#disconnect").prop("disabled",!flag);
//消息列表的隐藏和显示
if (flag){
$("#chat").show();
}else {
$("#chat").hide();
}
}

//将消息添加到末尾
function showGreeting(msg) {
$("#conversation").append("<div>"+msg.from+":"+msg.content+"</div>");
}
</script>
</body>
</html>

报错

There is no PasswordEncoder mapped for the id “null”

该报错是因为我们在往数据库写入密码是没有加上BCryptPasswordEncoder特有的ID加密方式

在每一个经过BCryptPasswordEncoder加密的字符串都会有{id}

我们来看一下官方文档:

The general format for a password is:

{id}encodedPassword

这样,id是一个标识符,用于查找应该使用哪个PasswordEncoder,而encodedPassword是所选PasswordEncoder的原始编码密码。id必须在密码的开头,以{开头,密码}结尾。如果找不到id, 密码将为空。例如,下面可能是使用不同id编码的密码列表。所有原始密码都是“password”

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

{noop}password

{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc

{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=

{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

解决方法

在数据库的密码字段上{id}的方式

尝试{bcrypt},{pbkdf2},{scrypt}

文章作者:xpp011

发布时间:2021年11月06日 - 15:11

原始链接:http://xpp011.cn/2021/11/06/afea1789.html

许可协议: 转载请保留原文链接及作者。