目录

  • 一、Spring Security 原理
  • 二、Spring Security认证流程 (源码跟踪)
  • 1、AuthenticationProvider
  • 2、UserDetailsService
  • 3、PasswordEncoder
  • 三、Spring Security授权流程 (源码跟踪)
  • 1、授权决策

一、Spring Security 原理

跳转到目录

  • 是基于过滤器链拦截用户发送的请求;
  • Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截, 校验每个请求是否能够访问它所期望的资源(就是看用户有没有权限)。根据前边知识的学习,可以通过FilterAOP等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。
  • 初始化Spring Security—> public class SpringSecurityApplicationInitializer
    extends AbstractSecurityWebApplicationInitializer
    ,会创建一个名为 SpringSecurityFilterChain 的Servlet过滤器,类型为 org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类,下图是Spring Security过虑器链结构图

springSercurity怎么权限控制 springsecurity权限控制的原理_认证


上图:

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

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

springSercurity怎么权限控制 springsecurity权限控制的原理_ide_02


springSercurity怎么权限控制 springsecurity权限控制的原理_授权_03

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

跳转到目录

springSercurity怎么权限控制 springsecurity权限控制的原理_认证_04


根据上面的时序图, 在程序中找到UsernamePasswordAuthenticationFilterDaoAuthenticationProvider两个类, 通过断点来分析上面时序图的流程;1、首先进去登录页面, 输入正确的账号密码 zhangsan, 123

springSercurity怎么权限控制 springsecurity权限控制的原理_Security_05


2、因为输入账号密码后, 会进入到AbstractAuthenticationProcessingFilterdoFilter方法,

springSercurity怎么权限控制 springsecurity权限控制的原理_Security_06


springSercurity怎么权限控制 springsecurity权限控制的原理_Security_07

3、认证器(AuthenticationManager)主要委托DaoAuthenticationProvider来认证用户的账号密码; 找到该类的retrieveUser方法

springSercurity怎么权限控制 springsecurity权限控制的原理_ide_08


UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); 这一步, 会调用我们编写的userDetailsService方法, 从数据库中取出用户的账号密码;

springSercurity怎么权限控制 springsecurity权限控制的原理_授权_09


springSercurity怎么权限控制 springsecurity权限控制的原理_原理_10


4、对用户输入的账号密码 和 数据库中的账号密码, 进行匹配, 会进入到DaoAuthenticationProvider的父类AbstractUserDetailsAuthenticationProvider进行判断操作

springSercurity怎么权限控制 springsecurity权限控制的原理_认证_11


进入additionalAuthenticationChecks方法, 进行账号密码的匹配

springSercurity怎么权限控制 springsecurity权限控制的原理_授权_12


springSercurity怎么权限控制 springsecurity权限控制的原理_Security_13


此时就登录成功;


认证核心组件的大体关系如下:

springSercurity怎么权限控制 springsecurity权限控制的原理_认证_14

1、AuthenticationProvider

跳转到目录

springSercurity怎么权限控制 springsecurity权限控制的原理_认证_15


springSercurity怎么权限控制 springsecurity权限控制的原理_ide_16

2、UserDetailsService

跳转到目录

  • 认识UserDetailsService

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

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

springSercurity怎么权限控制 springsecurity权限控制的原理_授权_17


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

springSercurity怎么权限控制 springsecurity权限控制的原理_ide_18

  • 它和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方法进行密码的对比,而具体的密码对比细节取决于实现:

springSercurity怎么权限控制 springsecurity权限控制的原理_原理_19


而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

springSercurity怎么权限控制 springsecurity权限控制的原理_认证_20


2 测试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);
    }
}

3、修改安全配置类

/**
 * 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的授权流程如下:

springSercurity怎么权限控制 springsecurity权限控制的原理_原理_21


AccessDecisiontManager中的Decide()方法中拿到当前访问资源所需要的权限信息用户信息中的权限信息对比, 如果符合, 则授权成功;

springSercurity怎么权限控制 springsecurity权限控制的原理_原理_22

springSercurity怎么权限控制 springsecurity权限控制的原理_ide_23


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采用投票的方式来确定是否能够访问受保护资源。

springSercurity怎么权限控制 springsecurity权限控制的原理_认证_24


springSercurity怎么权限控制 springsecurity权限控制的原理_认证_25


springSercurity怎么权限控制 springsecurity权限控制的原理_ide_26


springSercurity怎么权限控制 springsecurity权限控制的原理_原理_27


注意: 默认是采用AffirmativeBased的方式进入该类, 在decide方法下打断点

springSercurity怎么权限控制 springsecurity权限控制的原理_ide_28

springSercurity怎么权限控制 springsecurity权限控制的原理_ide_29