SpringSecurity的认证授权流程

发布于 2022-08-14  1.49k 次阅读


1. SpringSecurity的基本结构

Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有加入该系统的请求进行拦截,效验每个请求是否能够访问它所期望的资源

在自定义授权时主要依赖Filter和intercept实现或AOP技术,Spring Security对Web资源的保护底层也是使用Filter过滤器来实现

当初始化Spring Security时,会创建一个名为SpringSecurityFilterChain的Servlet过滤器,类行为org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类

Spring Security过滤链结构图:

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

FilterChainProxy相关类的UML简图:

Spring Security功能的实现主要是由一系列过滤链相互配合完成:

SpringSecurity中几个主要的过滤器及其作用:

  1. SecurityContextPersistenceFilter:这个Filter是整个拦截过程的入口和出口 ),会在请求开始时从配置好的SecurityContextRepository中获取SecurityContext,然后把它设置给SecurityContextHolder,在请求完成后将 SecurityContextHolder 持有的SecurityContext再保存到配置好的SecurityContextRepository ,同时清除 securityContextHolder所持有的SecurityContext
  2. UsernamePasswordAuthenticationFilter:用于处理来自表单提交的认证,该表单必须提供对应的用户和密码,其内部还有登录成功或失败后进行处理的AuthenticationSuccessHandler和AuthenticationFailureHandler,这些都可以根据需求做相关改变
  3. FilterSecurityInterceptor:用于保护Web资源,使用AccessDecisionManager对当前用户进行授权访问
  4. ExceptionTranslationFilter:能够捕获来自FilterChain所有的异常,并进行处理。但是它只能处理两类异常:AuthenticationException和AccessDeniedException,其他的异常它会继续抛出

2. SpringSecurity认证流程

2.1 认证流程

认证过程:

  1. 用户提交用户名、密码被SecurityFilterChain中的UsernamePasswordAuthenticationFilter过滤器获取到,封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类
  2. 然后过滤器将Authentication提交至认证过滤器(AuthenticationManager)进行认证
  3. 认证成功后,AuthenticationManager身份管理器返回一个被填充了信息的(包括权限信息、身份信息、细节信息、但密码通常会被移除)Authentication实例
  4. SecurityContextHolder安全上下文容器将第3步填充信息的Authentication,通过SecurityContextHolder.getContext.setAuthentication(…)方法,设置到其中

AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而SpringSecurity支持多种认证方式,因此ProviderManager维护着一个List<AuthenticationProvider>列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的

web表单对应的AuthenticationProcider实现类为DaoAuthenticationProvider,它的内部维护着一个UserDetailsService负责UserDetails获取,最终AuthenticationProvider将UserDetails填充到Authentication

认证核心组件关系:

2.2 AuthenticationProvider

通过认证流程可知,日志管理器(AuthenticationManager)委托AuthenticationProvider完成认证工作

AuthenticationProvider是一个接口,定义如下:

public interface AuthenticationProvider {
    Authentication authenticate(Authentication var1) throws AuthenticationException;

    boolean supports(Class<?> var1);
}

authenticate()方法定义了认证的实现过程,它的参数是一个Authentication,里面包含登录用户所提交的用户、密码等,而返回值也是一个Authentication,这个Authentication则是在认证成功后,将用户的权限及其它信息重新组织后生成

    Spring Security中维护着一个 List<AuthenticationProvider>列表,存放多种认证方式,不同的认证方式使用不同的AuthenticationProvider.如使用用户名密码登录时,使用AuthenticationProvider1,短信登录时使用AuthenticationProvider2等等

    每个AuthenticationProvider需要实现supports ()方法来表明自己支持的认证方式,如我们使用表单方式认证,在提交请求时Spring Security会生成UsernamePasswordAuthenticationToken,它是一个Authentication ,里面封装着用户提交的用户名、密码信息。而对应的,

DaoAuthenticationProvider的基类AbstractUserDetailsAuthenticationProvider以下代码

public boolean supports(Class<?> authentication) {
    return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}

当web表单提交用户密码时,SpringSecurity由DaoAuthenticationProvider处理

Authentication结构,它是一个接口:之前的UsernamePasswordAuthenticationToken就是它的实现之一

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    Object getCredentials();

    Object getDetails();

    Object getPrincipal();

    boolean isAuthenticated();

    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
  1. Authentication是spring security包中的接口,直接继承自Principal类,而Principal是位于java.security包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个getName()方法
  2. getAuthorities():权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串
  3. getCredentials():凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
  4. getDetails():细节信息, web应用中的实现接口通常为WebAuthenticationDetails,它记录了访问者的ip地址和sessionld的值
  5. getPrincipal():身份信息,大部分情况下返回的是UserDetails接口的实现类,UserDetails代表用户的详细信息,那从Authentication中取出来的UserDetails就是当前登录用户信息,它也是框架中的常用接口之一

2.3 UserDetailsService

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

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

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

DaoAuthicationProvider和UserDetailsService职责:

  1. UserDetailsService:负责从特定的地方(通常是数据库)加载用户信息,仅此而已
  2. DaoAuthicationProvider:职责更大,它完成整个认证流程,同时把UserDetails填充到Authentication

UserDetails是用户信息,它的源码如下:

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

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

Spring Security提供的InMemoryUserDetailsManager(内存认证)和JdbcUserDetailsManager(jdbc认证)就是UserDetailsService的实现类,主要区别就是从内存结构中获取用户还是从数据库中

例:自定义UserDetailsService

@Service
public class SpringDataUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //1.将来连接数据库根据账号查询用户信息
            ……
        //2.暂时采用模拟方式
            ……
        //3.登录账号
            ……
        //这里举类,使用硬编码的方式
        UserDetails build = User.withUsername("wql").password("123").authorities("a1").build();
        return build;
    }
}

2.4 PasswordEncoder

    DatAuthenticationProvider认证处理器通过UserDetailsService获取到UserDetails后,在进行密码效验时,就会使用到PasswordEncoder密码编码器

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

public interface PasswordEncoder {
    String encode(CharSequence var1);

    boolean matches(CharSequence var1, String var2);

    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

SpringSecurity提供了很多内置的PasswordEncode,能够开箱即用,使用某种PasswordEncode只需要如下声明即可:

@Configuration
public class WebSecurityContext extends WebSecurityConfigurerAdapter {

    //2,密码编码器
    @Bean
    public PasswordEncoder passwordEncoder(){
        //不需要对密码进行编码
        return NoOpPasswordEncoder.getInstance();
    }}

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

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

实际的项目中推荐使用BCryptPasswordEncode,Pbkdf2PasswordEncode,ScryptPasswordEncode等(具体的PasswordEncode实现还有很多)

例:使用BCryptPasswordEncode进行加密和效验

@Test
public void bcrypt(){

    //对密码进行加密
    String hashpw = BCrypt.hashpw("123", BCrypt.gensalt());

    System.out.println(hashpw);

    //效验
    boolean checkpw = BCrypt.checkpw("123", "$2a$10$YBCz.XizDGJcN.2SGqNQuOf/U.yN4ku6rC8zDgKfHxuDAz8guOkby");
    System.out.println(checkpw);
}

3. SpringSecurity授权流程

 3.1 授权流程

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

Spring Security的授权流程如下:

授权流程:

  1. 拦截请求:已认证用户访问受保护的web资源将被SecurityFilterChain中的FilterSecurityInterceptor的子类拦截
  2. 获取资源访问策略:FilterSecurityInterceptor会从SecurityMetadataSource的子类DefaultFilterInvocationSecurityMetadataSource获取要访问当前资源所需的权限

SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则读取访问策略如:

http.authorizeRequests()
        .antMatchers("/wql/**").authenticated()
        .anyRequest().permitAll()
………………

    3.最后, FilterSecuritylnterceptorg调用 AccessDecisionManager进行授权决策,若决策通过,则允许访问资源,否则将禁止访问

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接口就是用来鉴定当前用户是否有访问对应受保护资源的权限

3.2 授权策略

AccessDecisionManager采用投票的方式来确定是否能够访问受保护的资源

   AccessDecisionManager中包含的一系列AccessDecisionVoter将会被用来对Authentication是否有权访问受保护对象进行投票, AccessDecisionManager根据投票结果,做出最终决策

AccessDecisionVoter是一个接口,某中定义有三个变量:

public interface AccessDecisionVoter<S> {
    int ACCESS_GRANTED = 1;  //同意
    int ACCESS_ABSTAIN = 0;  //弃权
    int ACCESS_DENIED = -1;  //拒绝

    boolean supports(ConfigAttribute var1);

    boolean supports(Class<?> var1);

    int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3);
}

AccessDecisionVoter中定义的三个常量:

  • ACCESS_GRANTED表示同意
  • ACCESS_DENIED表示拒绝,
  • ACCESS_ABSTAIN表示弃权

    如果一个AccessDecisionVoter不能判定当前Authentication是否拥有访问对应受保护对象的权限,则其vote()方法的返回值应当为弃权ACCESS_ABSTAIN

SpringSecurity内置了三个基于投票的AccessDecisionManager的实现类:

  1. AffirmativeBased
  2. ConsensusBased
  3. UnanimousBased

①  AffirmativeBased的授权逻辑:默认使用的是AffirmativeBased

  1. 只要有AccessecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问
  2. 如果全部弃权也表示通过
  3. 如果没有一个人投赞成票,但是有人投反对票,则将抛出AccessDeniedException.Spring security

②  ConsensusBased授权逻辑:

  1. 如果赞成票多于反对票则表示通过
  2. 反过来,如果反对票多于赞成票则将抛出AccessDeniedException
  3. 如果赞成票与反对票相同且不等于0,并且属性allowlfEqualGrantedDeniedDecisions的值为true,则表示通过,否则将抛出异常AccessDeniedException.参数allowlfEqualGrantedDeniedDecisions的值默认为true
  4. 如果所有的AccessDecisionVoter都弃权了,则将视参数allowlfAllAbstainDecisions的值而定,如果该值为true则表示通过,否则将抛出异常AccessDeniedException.参数allowlfAllAbstainDecisions的值默认为false.

③ UnanimousBased授权逻辑

  1. 如果受保护对象配置的某一个ConfigAttribute被任意的AccessDecisionVoter反对了,则将抛出AccessDeniedException
  2. 如果没有反对票,但是有赞成票,则表示通过
  3. 如果全部弃权了,则将视参数allowlfAllAbstaTnDecisions的值而定,true则通过,false则抛出AccessDeniedException.

注:UnanimousBased的逻辑与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性全部传递给AccessDecisionVoter进行投票,而UnanimousBased会一次只传递一个ConfigAttribute给AccessDecisionVoter进行投票。这也就意味着如果我们的AccessDecisionVoter的逻辑是只要传递进来的ConfigAttribute中有一个能够匹配则投赞成票,但是放到UnanimousBased中其投票结果就不一定是赞成了