1.前言

路哥要学习SpringSecurity,我就跟随他的脚步重温一下SpringSecurity。
这次要学习看SpringSecurity的文档学习。。。。

2.文档整体浏览

image.png

文档的整体逻辑:
①介绍SpringSecurity框架
②使用SpringSecurity框架的环境要求
③SpringSecurity5.4的新增功能
④如何使用SpringSecurity
⑤各个模块的依赖介绍
⑥使用实例:Servlet 和 Reactive 两种应用使用样例
重点章节:
image.png

3.Getting Spring Security

文档中介绍了各种引入方式,我们关注的就是SpirngBoot如何引入SpringSecurity框架。
引入方式:

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

完了,这一节最有用的信息就是这个依赖了。。。。。。。。。。。。。。。。。。。

4.Servlet Application Sample

这一章中介绍的就是SpringSecurity如何应用于Servlet程序中,SpringSecurity集成到Servlet程序中的方式就是使用过滤器的方式。

4.1HelloWorld

1.引入依赖包

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

2.编写一个index.html页面放在如下目录下
image.png
3.启动程序
image.png
4.浏览器输入localhost:8080,输入用户名,输入控制台上的密码
image.png
5.点击Sign in成功登录到自己index.html页面
image.png

4.2SpringBoot帮我们配置了啥?

引入SpringSecurity后,SpringBoot会帮我自动配置,于是,我们可以快速的使用SpringSecurity,那么,SpringBoot帮我们配置了哪些东西呢?
1.SpringBoot将一个filter注册到Spring的Bean容器中,名字为:springSecurityFilterChain 他负责的就是所有安全相关的内容:保护我们的程序的各个接口、校验用户的账号和密码、跳转登录页等。
2.SpringBoot创建了一个UserDetailsService的Bean,这个Bean拥有一个用户名为 :user ,密码为随机生成的用户,且密码会打印在控制台上。
SpringSecurity重温(一) - 图7

4.3SpringSecurity的宏观设计

4.3.1过滤器代理 DelegatingFilterProxy

这个类的作用是注册为Servlet容器的过滤器,将具体的工作委托给其他的Spring容器Bean。这个Bean的名称就叫做:FilterChainProxy 它决定使用哪个 SecurityFilterChain (包含一堆具体的过滤器)。使用FilterChaninProxy的好处是: 可以在这个类中打断点调试。可以执行一些必须执行的操作。可以选择使用那一个 SecurityFilterChain(一组Filter)

4.3.2关系图

image.png

4.4各个过滤器的工作原理

4.4.1ExceptionTranslationFilter 过滤器

权限异常或者认证异常会返回响应。

流程:
image.png

5.SpringSecurity的认证流程

修改记录 日期
第一次修改 2021/11/13

Spring提供了全面的支持。身份认证就是验证访问者是谁,通常的方式是通过 用户名和密码的方式。认证结束后就可以进行授权。

5.1PasswordStorage 密码存储

用户名密码方式的身份验证方式,一个问题就是,系统如何保存用户设置的密码。使用明文的方式。如果数据库数据被盗取,密码将被泄露。
解决方式:将密码加密后存储在数据库中,这样即使数据被盗取,这样侵略者就算得到了数据,也无法得到密码。
在SpringSecurity框架中,PasswordEncoder接口被用作进行密码的转换以至于密码可以安全的存储。PasswordEncoder对密码的转换时单向的,只能加密不可解密。因此,用户输入的密码会通过PasswordEncoder再次加密后与数据库中加密后的密码比较。

5.1.1密码存储方式的变化

①使用明文存储在数据库中
②使用hash散列 SHA-256 方式加密密码,用户输入明文后也散列后比较数据库中的值。这样通过一个叫RainBow table 可以破解
③引入盐值,用户设置的密码+salt(随机字符)之后加密后存入密码。同时保存盐值,登录时,用户输入密码,系统将密码拼接盐值后加密,并与数据库中的密码进行比较。
如今的计算机运行速度快,这种SHA-256的加密手段已经很容易破解了
④现在使用自适应-单向函数,SpringSecurity提供了 bcrypt、等

5.1.2密码加密器代表

在SpringSecurity5.0之前的版本,默认的密码加密器是NoOpPasswordEncoder,就是不加密的。但是在5.0版本时,我们期待的是默认加密方式就是BCrypt这种方式。但是我们忽略了现实情况。如果贸然的改动后,许多项目的用户密码信息都需要迁移。于是,SpringSecurity推荐使用DelegatingPasswordEncoder。在SpringBoot使用SpringSecurity框架时,这个类被默认创建了 。
这个类其实是多个密码加密器的一个门面、代理。内部初始化多个加密方式,通过Map的方式保存起来。然后根据密码上的id,来判断使用哪种加密器。
其密码加密结果如下:

  1. {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
  2. {noop}password
  3. {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
  4. {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
  5. {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

默认的如果进行密码匹配时,不指定 {加密方式id} 会报异常,解决方式可以通过
DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder) 设置默认的加密器。

5.1.3加密器类别有哪些

image.png

5.1.4SpringBoot如何配置密码加密器

默认的是使用 DelegatingPasswordEncoder ,如果想自定义使用其他的加密器可以通过以下方式:

  1. @Bean
  2. public static NoOpPasswordEncoder passwordEncoder() {
  3. return NoOpPasswordEncoder.getInstance();
  4. }

5.2认证有关的组件

①SecurityContextHolder:记录着所有登录用户的认证信息
②SecurityContext:记录当前访问者的登录信息
③Authentication:是一个用于封装用户认证输入信息的类作为AuthenticationManager的入参,也可能是从SecurityContext中取得的用户认证信息
④GrantedAuthority:是赋予登录人的角色功能信息,被复制到 Authentication的principle中
⑤AuthenticationManager :定义SpringSecurity如何认证一套接口
⑥ProviderManager:是AuthenticationManager的一种实现方式
⑦AuthenticationProvider :被ProviderManager使用,去执行具体的一种认证方式
⑧AuthenticationEntryPoint :用于发送验证成功的响应和验证失败重定向到登录页面的操作
⑨AbstractAuthenticationProcessingFilter:是一个过滤器,控制整个认证流程,规定以上组件如何协同工作共同完成认证工作。

5.2.1SecurityContextHolder

image.png
SecurityContext记录这SecurityContext,使用ThreadLocal存储用户认证信息。可以切换存储模式。

5.2.2 SecurityContext

可以从SecurityContextHolder中获取里面包含Authentication信息

5.2.3Authentication

在未认证前,作为用户认证数据的封装参数,可以调用 isAuthenticated()方法校验是否被认证
包含:
principle:用户的身份信息,用户密码认证方式通常是userDetail
credentials:通常是密码,在认证结束后通常清除
authorities:通常是权限、角色。

5.2.4GrantedAuthority

可以获得通过Authentication的 getAuthorities() 方法 ,用户密码登录方式通常是在userDetailService中国被授予的

5.2.5AuthenticationManager

AuthenticationManger规划如何执行认证,返回的Authentication被设置到SecurityContextHolder中。也就是说你可以直接设置一个Authentication到SecurityContextHolder。这样也代表完成了认证。

5.2.6ProviderManager

是AuthenticationManger的一种常见实现方式。它将认证任务委托给其手下的AuthenticationProvider们,每一个手下都可以回答认证是否成功,或者说不能做。如果没手下能够认证则会报一个 ProviderNotFoundException 也是 AuthenticationException的一种(这样就会到4.4.1的一个过滤器中了)
image.png
可以指定父 AuthenticationManager。用于处理无法处理认证任务的情况

5.2.7AuthenticationProvider

主要有DaoAuthenticationProvider 和 JwtAuthenticationProvider

5.2.8 AuthenticationEntryPoint

当用户无权或未认证时给个响应的。

5.2.9AbstractAuthenticationProcessingFilter

image.png

5.3用户名密码认证流程

5.3.1表单登录流程

image.png
如果配置没有被提供,则默认是formlogin,但是一旦被提供,则需要做简单配置
如何配置,文档没有说清楚,就给了一个配置,草我去哪配置?MD 。因此此时先看JavaConfiguration(说实话没学过前,看这篇文档,鬼能看懂???),也许以后学习新的Spring相关框架就默认有一个配置方式。
我草,文档中的配置没有一个是对的,可能是我没配置对吧。
经过我的尝试以下配法是可以的:

  1. @Override
  2. protected void configure(HttpSecurity http) throws Exception {
  3. super.configure(http);
  4. http
  5. // ...
  6. .formLogin(form -> form
  7. .loginPage("/login")
  8. .permitAll()
  9. );
  10. ; }

整体这篇讲的的就是:
①如何配置formPage登录方式
②如何自定义登录页面。完了。。

5.3.2基本认证

会返回一个有 WWW-Authentication:Basic realm=”Realm”的头的响应。
会弹出一个认证框 alert

5.4基于内存数据库等的用户认证

基于内存或者说基于数据库的用户认证方式,就是将用户的信息存储在数据库或者内存中

5.4.1存储在内存中的配置方式

InmemoryUserDetailManager 实现了 UserDetailService (是啥之后再说,就是一个执行认证逻辑的类)允许从内存中获取用户信息并执行认证逻辑。这个类 InmomeryUDM 实现了UserDetailManager接口 可以管理 UserDetails 。(具体是啥,回头再讲)

配置方式:

  1. @Bean
  2. public UserDetailsService users() {
  3. UserDetails user = User.builder()
  4. .username("user")
  5. .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
  6. .roles("USER")
  7. .build();
  8. UserDetails admin = User.builder()
  9. .username("admin")
  10. .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
  11. .roles("USER", "ADMIN")
  12. .build();
  13. return new InMemoryUserDetailsManager(user, admin);
  14. }

可以发现密码是密文。
如果使用明文,登录时会报下面的错误:
image.png
如果想设置成明文的话需要指定默认的加密方式

  1. @Bean
  2. public UserDetailsService users() {
  3. //添加下面一行即可完成指定默认加密方式
  4. User.UserBuilder users = User.withDefaultPasswordEncoder();
  5. UserDetails user = users
  6. .username("user")
  7. .password("password")
  8. .roles("USER")
  9. .build();
  10. UserDetails admin = users
  11. .username("admin")
  12. //指定默认加密方式就不能再 {}xxxxxxx 这样了
  13. .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
  14. .roles("USER", "ADMIN")
  15. .build();
  16. return new InMemoryUserDetailsManager(user, admin);
  17. }

5.4.2基于数据库的认证

JdbcDaoImpl实现了UserDetialService,JdbcUserDetailManager 管理UserDetails。
既然支持Jdbc的话,就又数据表喽,SpringSecurity提供默认表结构,其sql语句在路径

The default schema is also exposed as a classpath resource named org/springframework/security/core/userdetails/jdbc/users.ddl.

image.png
官方例子需要引入的依赖包 h2依赖包。。。。

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-security</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-web</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>com.h2database</groupId>
  11. <artifactId>h2</artifactId>
  12. <scope>runtime</scope>
  13. </dependency>
  14. <dependency>
  15. <groupId>org.springframework.boot</groupId>
  16. <artifactId>spring-boot-starter-jdbc</artifactId>
  17. </dependency>

代码,官方丢了一行要哟

  1. @Bean
  2. DataSource dataSource() {
  3. return new EmbeddedDatabaseBuilder()
  4. .setType(EmbeddedDatabaseType.H2)
  5. .addScript("classpath:org/springframework/security/core/userdetails/jdbc/users.ddl")
  6. .build();
  7. }
  8. @Bean
  9. UserDetailsManager users(DataSource dataSource) {
  10. UserDetails user = User.builder()
  11. .username("user")
  12. .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
  13. .roles("USER")
  14. .build();
  15. UserDetails admin = User.builder()
  16. .username("admin")
  17. .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
  18. .roles("USER", "ADMIN")
  19. .build();
  20. JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
  21. users.createUser(user);
  22. users.createUser(admin);
  23. return users;
  24. }

其实基于的是数据库了,但是h2这个是不是还是基于 内存的呀。。哈哈。。。
自定义数据源
1.在本机数据库执行脚本

  1. create table users(
  2. username varchar(50) not null primary key,
  3. password varchar(500) not null,
  4. enabled boolean not null
  5. );
  6. create table authorities (
  7. username varchar(50) not null,
  8. authority varchar(50) not null,
  9. constraint fk_authorities_users foreign key(username) references users(username)
  10. );
  11. create unique index ix_auth_username on authorities (username,authority);

2.导入依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-security</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-web</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>mysql</groupId>
  11. <artifactId>mysql-connector-java</artifactId>
  12. </dependency>
  13. <dependency>
  14. <groupId>org.springframework.boot</groupId>
  15. <artifactId>spring-boot-starter-jdbc</artifactId>
  16. </dependency>

3.配置文件

  1. spring.datasource.url=jdbc:mysql://localhost:3306/test_security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
  2. spring.datasource.username=root
  3. spring.datasource.password=888888
  4. spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

4.代码

  1. @Bean
  2. UserDetailsManager users(DataSource dataSource) {
  3. UserDetails user = User.builder()
  4. .username("user")
  5. .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
  6. .roles("USER")
  7. .build();
  8. UserDetails admin = User.builder()
  9. .username("admin")
  10. .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
  11. .roles("USER", "ADMIN")
  12. .build();
  13. UserDetails user2 = User.builder()
  14. .username("user2")
  15. .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
  16. .roles("USER")
  17. .build();
  18. JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
  19. users.createUser(user);
  20. users.createUser(admin);
  21. users.createUser(user2);
  22. return users;
  23. }

数据库就到这了。。。。

5.5UserDetail和UserDetailService

前两天上班偷偷的刷leetcode,今天不想刷了,就偷偷的说一下 这俩个东西。
UserDetail:
①被UserDetailService返回的一个对象
②DaoAuthenticationProvider ,它将 返回的UserDetail放入 Authentication的 principle字段中。
UserDetailService:
①被DaoAuthenticationProvider使用,用来获取相关登录人的登录信息,UserDetail。

可以暴露一个 自定义的UserDetailService ,暴露方式如下:

  1. @Bean
  2. CustomUserDetailsService customUserDetailsService() {
  3. return new CustomUserDetailsService();
  4. }

customUserDetailService必须继承 UserDetailService。

5.6PasswordEncoder

自定义PasswordEncoder的方式:

  1. @Bean
  2. public static NoOpPasswordEncoder passwordEncoder() {
  3. return NoOpPasswordEncoder.getInstance();
  4. }

5.7DaoAuthenticationProvider

它使用UserDetailService和PasswordEncoder进行认证一个用户名和密码。
image.png

5.8SessionManagement

认证完成后,认证的信息默认是存储在Session中的。
Session的相关功能是 SessionManagementFilter和SessionAuthenticationStrategy接口联合实现。
典型的应用:
①防护session被攻击
②检测session的超时。
③定义一个用户可以拥有几个登录认证session信息

5.8.1SessionManagementFilter

它检查SecurityContextRepository的内容在SecurityContextHolder去决定是否用户被认证之前。如果说有在SecuirtyContextRepo中有被认证的信息,这个过滤器啥都不做,如果没有,但是SecurityContext是个非匿名的认证信息,它会认为别的过滤器已经认证这个人,需要我存储这个人的认证信息。于是触发 SessionStrategy。
用户未登录,过滤器将检查是否是一个不合法的sessionId,如果是返回配置的 一个策略。

5.8.2SessionAuthenticationStrategy

有两处用到它,SessionManagementFilter和AbstractAuthenticationProcessingFilte,如果自定义这两类,需要在这自定的类中引入他们俩。

5.9记住我认证方式

6.SpringSecurity的配置方式

SpringSecurity引入项目后,考虑简单的用户名密码校验,我框架怎么知道你们公司有哪些用户。
第一步:配置过滤器
配置的目的就是配置一个 SpringSecurityFilterChain,也就是一组过滤器。
配置方式:

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.context.annotation.*;
  3. import org.springframework.security.config.annotation.authentication.builders.*;
  4. import org.springframework.security.config.annotation.web.configuration.*;
  5. @EnableWebSecurity
  6. public class WebSecurityConfig {
  7. @Bean
  8. public UserDetailsService userDetailsService() {
  9. InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
  10. manager.createUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build());
  11. return manager;
  12. }
  13. }

第二步:注册过滤器
使用SpringMVC的话就以下代码注释

  1. import org.springframework.security.web.context.*;
  2. public class SecurityWebApplicationInitializer
  3. extends AbstractSecurityWebApplicationInitializer {
  4. }

6.1一个默认的配置类

我们的配置类仅仅包含了系统有哪些用户,那么使用表单认证还是基础认证。其他的一些配置,没有配置如何帮我们配置的呢?其实背后还有一个叫 WebSecurityConfigurerAdapter ,它偷偷的帮我们配置了。
默认的配置是下面这样的:

  1. protected void configure(HttpSecurity http) throws Exception {
  2. http
  3. //1.所有的接口认证后访问
  4. .authorizeRequests(authorize -> authorize
  5. .anyRequest().authenticated()
  6. )
  7. //允许表单登录
  8. .formLogin(withDefaults())
  9. //允许Basic登录
  10. .httpBasic(withDefaults());
  11. }

6.2配置一个自定义过滤器

自定义过滤器

  1. public class MyCustomDsl extends AbstractHttpConfigurer<MyCustomDsl, HttpSecurity> {
  2. private boolean flag;
  3. @Override
  4. public void init(H http) throws Exception {
  5. // any method that adds another configurer
  6. // must be done in the init method
  7. http.csrf().disable();
  8. }
  9. @Override
  10. public void configure(H http) throws Exception {
  11. ApplicationContext context = http.getSharedObject(ApplicationContext.class);
  12. // here we lookup from the ApplicationContext. You can also just create a new instance.
  13. MyFilter myFilter = context.getBean(MyFilter.class);
  14. myFilter.setFlag(flag);
  15. http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class);
  16. }
  17. public MyCustomDsl flag(boolean value) {
  18. this.flag = value;
  19. return this;
  20. }
  21. public static MyCustomDsl customDsl() {
  22. return new MyCustomDsl();
  23. }
  24. }

这是使用SpringSecurity的常规配置方式

  1. @EnableWebSecurity
  2. public class Config extends WebSecurityConfigurerAdapter {
  3. @Override
  4. protected void configure(HttpSecurity http) throws Exception {
  5. http
  6. .apply(customDsl())
  7. .flag(true)
  8. .and()
  9. ...;
  10. }
  11. }

eeee,这张写的不好哦。。。。。。