相关推荐recommended
【SpringSecurity】五、UserDetails接口和UserDetailsService接口(原理、流程)
作者:mmseoamin日期:2024-04-29

文章目录

  • 1、SpringSecurity原理
  • 2、重要接口源码
    • AuthenticationProvider
    • Authentication
    • UserDetails
    • UserDetailsService
    • 3、授权流程
    • 4、UserDetails接口
    • 5、UserDetailService接口
    • 6、权限配置

      1、SpringSecurity原理

      Spring Security是做安全访问控制,对所有进入系统的请求进行拦截,并做校验,这可以通过Filter或者AOP实现,Spring Security靠Filter。

      【SpringSecurity】五、UserDetails接口和UserDetailsService接口(原理、流程),在这里插入图片描述,第1张

      • 初始化Spring Security时,创建一个名为SpringSecurityFilterChain的Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类
      • FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理
      • 这些Filter不负责认证或者授权,而是交给认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理。(过滤器嘛,就是拦截下,获取点东西、修改点东西、干点操作)

        Spring Security功能的实现就是一系列过滤器链相互配合。

        【SpringSecurity】五、UserDetails接口和UserDetailsService接口(原理、流程),在这里插入图片描述,第2张

        • SecurityContextPersistenceFilter 这个Filter是整个拦截过程的入口和出口,也就是第一个和最后一个拦截器。会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;

        • UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证,该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变;(这些处理器的逻辑可自定义,重新实现一下这个处理器)

        • ExceptionTranslationFilter 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。

        • FilterSecurityInterceptor 是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问

          SpringSecurity的执行流程:

          【SpringSecurity】五、UserDetails接口和UserDetailsService接口(原理、流程),在这里插入图片描述,第3张

          • 用户提交用户名密码,被安全过滤器链中的UsernamePasswordAuthenticationFilter过滤器拿到,并封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类
          • 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
          • 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例
          • SecurityContextHolder安全上下文容器将上一步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中
          • 以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager
          • 而Spring Security支持多种认证方式,因此ProviderManager维护着一个List列表,存放多种认证方式,不同的认证方式用不同的AuthenticationProvider实现类,最终实际的认证工作是由AuthenticationProvider完成的。而web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。当UserDetailsService接口的loadUserByUsername()方法获取到的User Details对象中的密码和Authentication中的密码一致时,认证成功,最后AuthenticationProvider将UserDetails填充至Authentication

            总之,整个框架中,AuthenticationProvider(DaoAuthenticationProvider)承上启下,而通过实现UserDetailsService和UserDetails,可以完成对用户信息获取方式以及用户信息字段的扩展。

            2、重要接口源码

            AuthenticationProvider

            认证管理器AuthenticationManager委托AuthenticationProvider完成认证工作,AuthenticationProvider接口定义如下:

            public interface AuthenticationProvider {
            	Authentication authenticate(Authentication authentication) throws AuthenticationException;
            	
            	//supports方法来表明自己支持的认证方式
            	boolean supports(Class var1);
            }
            

            authenticate()方法定义了认证的实现过程。传参为Authentication,里面包含登录用户所提交的用户名密码等。返回也是Authentication对象,且是一个认证通过后,被填充权限等信息的Authentication。

            【SpringSecurity】五、UserDetails接口和UserDetailsService接口(原理、流程),在这里插入图片描述,第4张

            supports方法来表明自己支持的认证方式。如使用表单方式认证,在提交请求时Spring Security会生成UsernamePasswordAuthenticationToken(Authentication的子类),里面封装着用户提交的用户名、密码信息。

            //当web表单提交用户名密码时,Spring Security由DaoAuthenticationProvider处理
            //其supports方法中就是UsernamePasswordAuthenticationToken.class
            public boolean supports(Class authentication) {
            	return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
            }
            

            Authentication

            Authentication继承自Principal,Principal是框架用来表示一个抽象主体的类(如人、用户、公司),任何主体都有名称,因此其有个getName方法

            【SpringSecurity】五、UserDetails接口和UserDetailsService接口(原理、流程),在这里插入图片描述,第5张

            Authentication源码如下:

            public interface Authentication extends Principal, Serializable { 
            	
            	//获取权限信息列表,默认是GrantedAuthority接口的一些实现类
            	Collection getAuthorities(); 
            	
            	//获取凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全
            	Object getCredentials(); 
            	
            	//细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值
            	Object getDetails(); 
            	
            	//身份信息,大部分情况下返回的是UserDetails接口的实现类,UserDetails代表用户的详细信息,那从Authentication中取出来的UserDetails就是当前登录用户信息,它也是框架中的常用接口之一
            	Object getPrincipal(); 
            	
            	boolean isAuthenticated();
            	
            	void setAuthenticated(boolean var1) throws IllegalArgumentException;
            }
            

            UserDetails

            public interface UserDetails extends Serializable {
            	Collection getAuthorities();
            	
            	String getPassword();
            	
            	String getUsername();
            	
            	boolean isAccountNonExpired();
            	
            	boolean isAccountNonLocked();
            	
            	boolean isCredentialsNonExpired();
            	
            	boolean isEnabled();
            }
            

            UserDetails和Authentication相似看着。而实际前者是库里存的信息,后者是用户提交的信息。

            Authentication的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证其实就是对这两者的比对。

            【SpringSecurity】五、UserDetails接口和UserDetailsService接口(原理、流程),在这里插入图片描述,第3张

            再看这个流程图,即AuthenticationProvider通过loadUserByUsername拿到的UserDetails,如果和表单提交封装的Authentication里的密码相等,则填充权限信息到Authentication。而Authentication中的权限信息,实际就是由UserDetails的getAuthorities()传递而形成的。

            UserDetailsService

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

            DaoAuthenticationProvider聚合UserDetailsService对象,this.userDetailsService.loadUserByUsername去根据用户名获取到库里的用户信息(包括密码、权限)。然后DaoAuthenticationProvider对比表单提交后封装的Authentication对象里的用户信息是否等于loadUserByUsername到的UserDetails的用户信息。

            UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已。DaoAuthenticationProvider的职责更大,它完成完整的认证流程,同时会把UserDetails信息填充至Authentication。(然后一开始的过滤器把填充后的Authentication给set到了SecurityContextHolder,因此才有了后面从SecurityContextHolder获取当前登录用户)

            Spring Security提供的InMemoryUserDetailsManager(内存认证),JdbcUserDetailsManager(jdbc认证),就是

            UserDetailsService的实现类,区别就是从内存还是从数据库加载用户。

            3、授权流程

            【SpringSecurity】五、UserDetails接口和UserDetailsService接口(原理、流程),在这里插入图片描述,第7张

            已认证的用户访问受保护的web资源:

            • SecurityFilterChain中的FilterSecurityInterceptor拦截(其实现了servlet的Filter)
            • 拦截器获取访问该web资源所需的权限Collection
            • 拦截器调用AccessDecisionManager来决策是否允许访问

              而决策接口为:

              public interface AccessDecisionManager {
              	/**
              	* 通过传递的参数来决定用户是否有访问对应受保护资源的权限
              	* authentication:要访问资源的访问者的身份
              	* object:要访问的受保护资源,web请求对应FilterInvocation
              	* configAttributes:是受保护资源的访问策略,通过SecurityMetadataSource获取。
              	*/
              	void decide(Authentication authentication , Object object, Collection
              	configAttributes ) throws AccessDeniedException, InsufficientAuthenticationException;
              	//略..
              }
              

              【SpringSecurity】五、UserDetails接口和UserDetailsService接口(原理、流程),在这里插入图片描述,第8张

              AccessDecisionManager的实现类中聚合了投票者接口AccessDecisionVoter的一系列子实现类。通过vote方法的返回结果分析投票结果:

              public interface AccessDecisionVoter {
              	//同意访问
              	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 var3);
              }
              

              对AccessDecisionVoter不同的投票结果,AccessDecisionManager的三个实现类,分析的结果也不同。如ConsensusBased的逻辑是赞成票多于反对票则表示通过。

              4、UserDetails接口

              看完上面的UserDetails对象和UserDetailsService接口的loadUserByUsername方法后,再看之前在内存中定义用户,是这样的:

              UserDetails user2 = User.builder()
                      .username("liu")
                      .password(passwordEncoder().encode("123456"))
                      .authorities("teacher:add","teacher:update")
                      .roles("teacher")
                      .build();
              

              查看User类的源码,其实现了UserDetails接口,因此上面才可以直接创建User对象。

              【SpringSecurity】五、UserDetails接口和UserDetailsService接口(原理、流程),在这里插入图片描述,第9张

              接下来自己定义一个用户类SecurityUser类,也去实现UserDetails接口,重写UserDetails接口方法时,直接写死一个用户信息,一会儿new这个自定义的SecurityUser类,也就和上面的User.builder一个意思。

              public class SecurityUser implements UserDetails {
                  @Override
                  public Collection getAuthorities() {
                      return null;   //未给权限
                  }
                  @Override
                  public String getPassword() {
                      //明文为123456
                      //return "$2a$10$KyXAnVcsrLaHMWpd3e2xhe6JmzBi.3AgMhteFq8t8kjxmwL8olEDq";
                      return new BCryptPasswordEncoder().encode("123456");
                  }
                  @Override
                  public String getUsername() {
                      return "liu";
                  }
                  @Override
                  public boolean isAccountNonExpired() {
                      return true;
                  }
                  @Override
                  public boolean isAccountNonLocked() {
                      return true;
                  }
                  @Override
                  public boolean isCredentialsNonExpired() {
                      return true;
                  }
                  @Override
                  public boolean isEnabled() {
                      return true;
                  }
              }
              

              UserDetails 接口的7个方法如下图:

              方法名作用
              getAuthorities()获取当前用户对象所具有的角色信息
              getPassword()获取当前用户对象的密码
              getUsername()获取当前用户对象的用户名
              isAccountNonExpired()当前账户是否未过期
              isAccountNonLocked()当前账户是否未锁定
              isCredentialsNonExpired()当前账户密码是否未过期
              isEnabled()当前账户是否可用

              5、UserDetailService接口

              UserDetails的用户对象建好了,继续看之前在内存中创建用户的实现思路:

              InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
              userDetailsManager.createUser(user1);
              userDetailsManager.createUser(user2);
              

              InMemoryUserDetailsManager类实现了UserDetailsManager接口,UserDetailsManager接口又继承了UserDetailService接口:

              【SpringSecurity】五、UserDetails接口和UserDetailsService接口(原理、流程),在这里插入图片描述,第10张

              自己新建一个类UserServiceImpl去实现UserDetailService接口。重写loadUserByUsernam方法,并当用户名等于自定义的SecurityUser对象中的用户名时,返回SecurityUser对象。

              @Service
              public class UserServiceImpl implements UserDetailsService {
                  @Override
                  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                      SecurityUser securityUser= new SecurityUser();
                      if(username==null || !username.equals(securityUser.getUsername())){
                          throw new UsernameNotFoundException("该用户不存在");
                      }
                      return securityUser;
                  }
              }
              

              以上其实是自己玩了下5.6两步:

              【SpringSecurity】五、UserDetails接口和UserDetailsService接口(原理、流程),在这里插入图片描述,第11张

              重启服务,登录下,一切正常。

              【SpringSecurity】五、UserDetails接口和UserDetailsService接口(原理、流程),在这里插入图片描述,第12张

              6、权限配置

              上面自定义的SecurityUser类中,关于权限的方法返回null,写个接口返回认证信息,可以看到权限字段确实为null:

              【SpringSecurity】五、UserDetails接口和UserDetailsService接口(原理、流程),在这里插入图片描述,第13张

              【SpringSecurity】五、UserDetails接口和UserDetailsService接口(原理、流程),在这里插入图片描述,第14张

              加权:

                  @Override
                  public Collection getAuthorities() {
                     GrantedAuthority g1=()->"student:query"; //使用lambda表达式
              	   //GrantedAuthority g1=new SimpleGrantedAuthority("student:query");
                     List grantedAuthorityList=new ArrayList<>();
                     grantedAuthorityList.add(g1);
                     return grantedAuthorityList;
                  }
              

              顺便使用@PreAuthorize注解方便后面看下效果:

              @RestController
              @RequestMapping("/student")
              public class StudentController {
                  @GetMapping("/query")
                  @PreAuthorize("hasAuthority('student:query')")
                  public String queryInfo(HttpServletRequest request){
                      return "I am a student,My name is XXX";
                  }
              }
              

              重启后查看认证信息:

              【SpringSecurity】五、UserDetails接口和UserDetailsService接口(原理、流程),在这里插入图片描述,第15张

              【SpringSecurity】五、UserDetails接口和UserDetailsService接口(原理、流程),在这里插入图片描述,第16张

              梳理完UserDetails和UserDetailsService接口之间的流程和细节,方便后面理解SpringSecurity基于数据库认证。