一、Spring Security 原理

核心机制

  • 基于 过滤器链拦截用户发送的请求
  • Spring Security解决的核心问题是 安全访问控制对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源
  • Spring Security对Web资源的保护是靠Filter实现的

过滤器链初始化

初始化Spring Security时,会创建一个名为 SpringSecurityFilterChain 的Servlet过滤器:

  • 类型:org.springframework.security.web.FilterChainProxy
  • 实现:javax.servlet.Filter
  • 作用:所有外部请求都会经过此类

在这里插入图片描述
上图:

  • UsernamePasswordAuthenticationFilter —> 对应实际干活的是 AuthenenticationManager
  • FilterSecurityInterceptor —> 对应实际干活的是 AccessDecisionManager

FilterChainProxy是一个代理真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager决策(授权)管理器 (AccessDecisionManager) 进行处理,下图是FilterChainProxy相关类的UML图示。

在这里插入图片描述
在这里插入图片描述

二、Spring Security认证流程 (源码跟踪)

在这里插入图片描述

在这里插入图片描述
根据上面的时序图, 在程序中找到UsernamePasswordAuthenticationFilterDaoAuthenticationProvider两个类, 通过断点来分析上面时序图的流程;

1、首先进去登录页面, 输入正确的账号密码 zhangsan, 123
在这里插入图片描述
2、因为输入账号密码后, 会进入到AbstractAuthenticationProcessingFilterdoFilter方法,
在这里插入图片描述
在这里插入图片描述

3、认证器(AuthenticationManager)主要委托DaoAuthenticationProvider来认证用户的账号密码; 找到该类的retrieveUser方法
在这里插入图片描述
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); 这一步, 会调用我们编写的userDetailsService方法, 从数据库中取出用户的账号密码;
在这里插入图片描述
在这里插入图片描述
4、对用户输入的账号密码 和 数据库中的账号密码, 进行匹配, 会进入到DaoAuthenticationProvider的父类AbstractUserDetailsAuthenticationProvider进行判断操作
在这里插入图片描述
进入additionalAuthenticationChecks方法, 进行账号密码的匹配
在这里插入图片描述
在这里插入图片描述
此时就登录成功;


认证核心组件的大体关系如下:
在这里插入图片描述

1、AuthenticationProvider

AuthenticationProvider接口

public interface AuthenticationProvider {
    // 执行认证,返回认证结果
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
    
    // 判断是否支持某种Authentication类型
    boolean supports(Class<?> authentication);
}

核心实现类

  • DaoAuthenticationProvider:最常用的实现,基于UserDetailsService进行认证
  • AnonymousAuthenticationProvider:处理匿名用户认证
  • RememberMeAuthenticationProvider:处理记住我功能的认证

认证流程

  1. AuthenticationManager接收认证请求
  2. 委托给合适的AuthenticationProvider处理
  3. AuthenticationProvider调用UserDetailsService获取用户信息
  4. 使用PasswordEncoder验证密码
  5. 认证成功返回Authentication对象

2、UserDetailsService

  • 认识UserDetailsService

DaoAuthenticationProvider处理了web表单的认证逻辑,认证成功后得到一个Authentication(UsernamePasswordAuthenticationToken实现),里面包含了身份信息(Principal)。这个身份信息就是一个Object ,大多数情况下它可以被强转为UserDetails对象。

DaoAuthenticationProvider中包含了一个UserDetailsService实例,它负责根据用户名提取用户信息 UserDetails(包含密码),而后DaoAuthenticationProvider会去对比UserDetailsService提取的用户密码与用户提交的密码是否匹配作为认证成功的关键依据,因此可以通过将自定义的 UserDetailsService 公开为spring bean来定 义自定义身份验证。

在这里插入图片描述
重要 : 很多人把DaoAuthenticationProviderUserDetailsService的职责搞混淆,其实UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已。而DaoAuthenticationProvider的职责更大,它完成完整的认证流程,同时会把UserDetails填充至Authentication。

在这里插入图片描述

  • 它和Authentication接口很类似,比如它们都拥有username,authorities。Authentication的getCredentials()与 UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证 其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形 成的。还记得Authentication接口中的getDetails()方法吗?其中的UserDetails用户详细信息便是经过了 AuthenticationProvider认证之后被填充的。

  • 通过实现UserDetailsService和UserDetails,我们可以完成对用户信息获取方式以及用户信息字段的扩展。

  • Spring Security提供的InMemoryUserDetailsManager(内存认证)JdbcUserDetailsManager(jdbc认证)就是 UserDetailsService的实现类,主要区别无非就是从内存还是从数据库加载用户。

测试:

自定义UserDetailsService

@Service
public class MyUserDetailService implements UserDetailsService {

    // 根据账号查询信息
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 将来连接数据库根据账号来查询用户信息
        // 现在先模拟
        System.out.println("username = " + username);
        UserDetails userDetails = User.withUsername("zhangsan1").password("123").authorities("p1").build();
        return userDetails;
    }
}

屏蔽安全配置类中UserDetailsService的定义

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 配置用户信息服务
//    @Bean
//    public UserDetailsService userDetailsService() {
//        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
//        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
//        return manager;
//    }

重启工程,请求认证,MyUserDetailsService的loadUserByUsername方法被调用 ,查询用户信息。

3、PasswordEncoder

认识PasswordEncoder

DaoAuthenticationProvider认证处理器通过UserDetailsService获取到UserDetails后,它是如何与请求 Authentication中的密码做对比呢?

在这里Spring Security为了适应多种多样的加密类型,又做了抽象,DaoAuthenticationProvider通过 PasswordEncoder接口的matches方法进行密码的对比,而具体的密码对比细节取决于实现:

在这里插入图片描述
而Spring Security提供很多内置的PasswordEncoder,能够开箱即用,使用某种PasswordEncoder只需要进行如 下声明即可,如下

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

NoOpPasswordEncoder采用字符串匹配方法,不对密码进行加密比较处理,密码比较流程如下:

1、用户输入密码(明文 )
2、DaoAuthenticationProvider获取UserDetails(其中存储了用户的正确密码)
3、DaoAuthenticationProvider使用PasswordEncoder对输入的密码和正确的密码进行校验,密码一致则校验通 过,否则校验失败。

  • NoOpPasswordEncoder的校验规则拿 输入的密码和UserDetails中的正确密码进行字符串比较,字符串内容一致 则校验通过,否则校验失败

  • 实际项目中推荐使用BCryptPasswordEncoder, Pbkdf2PasswordEncoder, SCryptPasswordEncoder等,感兴趣的大家可以看看这些PasswordEncoder的具体实现

使用BCryptPasswordEncoder
在这里插入图片描述

测试BCrypt

@SpringBootTest
public class TestBCrypt {

    @Test
    public void testBCrypt() {
        // 对密码进行加密
        String hashpw = BCrypt.hashpw("123", BCrypt.gensalt());
        String hashpw2 = BCrypt.hashpw("456", BCrypt.gensalt());
        System.out.println("hashpw = " + hashpw);
        System.out.println("hashpw2 = " + hashpw2);

        // 校验密码
        boolean checkpw1 = BCrypt.checkpw("123", "$2a$10$QfQYXOtc/2oSgiuYi.9x6.8VcFZ4RuQOq7WmzwkkhXoiD.hB5swP.");
        boolean checkpw2 = BCrypt.checkpw("123", "$2a$10$ptyf4yyfbc1oL.OJPfSKMOk.hO4eRS1SQj44MBhhHnSZFrphjGHK.");
        System.out.println("checkpw1 = " + checkpw1);
        System.out.println("checkpw2 = " + checkpw2);
    }
}

修改安全配置类

/**
 * Description: 安全配置的内容包括:用户信息、密码编码器、安全拦截机制。
 *
 * @author zygui
 * @date Created on 2020/7/22 15:11
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 配置用户信息服务
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan").password("$2a$10$QfQYXOtc/2oSgiuYi.9x6.8VcFZ4RuQOq7WmzwkkhXoiD.hB5swP.").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("$2a$10$LYa/9GkXYzhc/UjD7S/D5OWE2F7RXHVgANsDHC4XSp8OiEfi1Fk4e").authorities("p2").build());
        return manager;
    }

    // 对密码进行编码, 使用不加密的对比
//    @Bean
//    public PasswordEncoder passwordEncoder() {
//        return NoOpPasswordEncoder.getInstance();
//    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 配置安全拦截机制


    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/r/r1").hasAuthority("p1")
                .antMatchers("/r/r2").hasAuthority("p2")
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .successForwardUrl("/login-success");//自定义登录成功的页面地址

    }
}

三、Spring Security授权流程 (源码跟踪)

在这里插入图片描述

Spring Security可以通过 http.authorizeRequests() 对web请求进行授权保护。Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。

Spring Security的授权流程如下:
在这里插入图片描述
AccessDecisiontManager中的Decide()方法中拿到当前访问资源所需要的权限信息用户信息中的权限信息对比, 如果符合, 则授权成功;
在这里插入图片描述

在这里插入图片描述
AccessDecisionManager(访问决策管理器)的核心接口如下:

public interface AccessDecisionManager {
    void decide(Authentication var1, Object var2, Collection<ConfigAttribute> var3) throws AccessDeniedException, InsufficientAuthenticationException;

    boolean supports(ConfigAttribute var1);

    boolean supports(Class<?> var1);
}

这里着重说明一下decide的参数:

  • authentication:要访问资源的访问者的身份
  • object:要访问的受保护资源,web请求对应FilterInvocation
  • configAttributes:是受保护资源的访问策略,通过SecurityMetadataSource获取。

decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。

1、授权决策

AccessDecisionManager采用投票的方式来确定是否能够访问受保护资源。
在这里插入图片描述

三种决策方式

1. AffirmativeBased(一票通过)

  • 决策规则:只要有一个投票器投赞成票,就允许访问
  • 使用场景:默认方式,适合大多数场景
  • 特点:宽松的访问控制

2. ConsensusBased(少数服从多数)

  • 决策规则:赞成票多于反对票时允许访问
  • 使用场景:需要多数同意的严格控制
  • 特点:民主决策方式

3. UnanimousBased(一票否决)

  • 决策规则:所有投票器都必须投赞成票才允许访问
  • 使用场景:高安全要求的场景
  • 特点:最严格的访问控制

注意: 默认是采用AffirmativeBased的方式

进入该类, 在decide方法下打断点
在这里插入图片描述

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐