Bootstrap

Spring Security —06—自定义认证数据源

6.1认证流程

弄清楚认证原理之后我们来看下具体认证时数据源的获取:默认情况下,AuthenticationProvider是由DaoAuthenticationProvider类来实现认证的,在DaoAuthenticationProvider认证时又通过UserDetailsService完成数据源的校验。他们之间调用关系如下:
在这里插入图片描述
AuthenticationManager 是认证管理器,在 Spring Security 中有全局AuthenticationManager,也可以有局部AuthenticationManager。全局的AuthenticationManager用来对全局认证进行处理,局部的AuthenticationManager用来对某些特殊资源认证处理。当然无论是全局认证管理器还是局部认证管理器都是由 ProviderManger 进行实现。 每一个ProviderManger中都代理一个AuthenticationProvider的列表,列表中每一个实现代表一种身份认证方式,认证时底层数据源需要调用 UserDetailService 来实现。

6.2配置全局 AuthenticationManager

6.2.1默认的全局 AuthenticationManager

springboot 对 security 进行自动配置时,自动在工厂中使用AuthenticationManagerBuilder 创建一个全局AuthenticationManager,配置的是原有的全局AuthenticationManager

 //springboot对security默认配置中,在工厂中默认创建AuthenticationManager
 @Autowired //将容器中自动创建的AuthenticationManagerBuilder注入进来
 //这里AuthenticationManagerBuilder是spring工厂中给我们创建好了的默认AuthenticationManager,拿到后就可以进行修改
 public void initialize(AuthenticationManagerBuilder builder) throws Exception {
     System.out.println("springboot默认配置: " + builder);
 }

输出结果:

springboot默认配置:org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder@60297f36

总结:默认自动配置创建的全局AuthenticationManager

  • 默认查找当前项目中是否存在自定义的UserDetailService 实例,如果存在实例,原有的UserDetailService自动配置的内存将会失效,自动将当前项目UserDetailService 实例设置为数据源。

  • 在工厂中使用时直接在代码中注入即可。

6.2.2自定义全局 AuthenticationManager

覆盖原有的全局AuthenticationManager,也就是构建出一个空的AuthenticationManager也没有指定的UserDetailService。

 @Configuration
 public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   @Override//重写的父类WebSecurityConfigurerAdapter中的configure方法,使用的也是父类的AuthenticationManagerBuilder 
   public void configure(AuthenticationManagerBuilder builder) {
     //builder ....
   }
 }

总结自定义全局 AuthenticationManager

  • 一旦通过 configure 方法自定义AuthenticationManager的实现就会将工厂中自动配置的AuthenticationManager进行覆盖,也就是构建出一个空的AuthenticationManager也没有指定的UserDetailService。

  • 需要在实现中指定认证数据源对象UserDetailService实例;

  • 这种方式创建的AuthenticationManager对象是工厂内部本地的一个 AuthenticationManager对象,没有暴露出来,不允许在其他自定义组件中进行注入使用。

在工厂中暴露自定义AuthenticationManager 实例

 @Configuration
 public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     //1.自定义AuthenticationManager  推荐使用这种方式,而不是直接使用默认自动配置的,但这种方式并没有在工厂中暴露出来
     @Override
     public void configure(AuthenticationManagerBuilder builder) throws Exception {
         System.out.println("自定义AuthenticationManager: " + builder);
         builder.userDetailsService(userDetailsService());
     }//作用: 用来将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入
     @Override
     @Bean //记得加上此注解,将AuthenticationManager 对象注入工厂
     public AuthenticationManager authenticationManagerBean() throws Exception {
         return super.authenticationManagerBean();
     }
 }

6.3测试代码总结

 package com.study.config;import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.provisioning.InMemoryUserDetailsManager;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.OrRequestMatcher;/**
  * @ClassName WebSecurityConfigurer
  * @Description TODO
  * @Date 2022/7/21 22:34
  * @Version 1.0
  */
 @Configuration
 public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
     //1.默认AuthenticationManager:springboot对security默认配置中,在工厂中默认创建AuthenticationManager
 //    @Autowired //代码从官网复制而来
 //    public void initialize(AuthenticationManagerBuilder builder) throws Exception {
 //        System.out.println("springboot默认配置:" + builder);
 //        builder.userDetailsService(userDetailsService());
 //    }/**
      * 未定义UserDetailsService前:启动服务,重新访问hello路径,发现原先的用户名、密码组合失效,需要使用master、123456才能登录成功
      * 输出结果:
      *    springboot默认配置:org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder@68ace111
      *///在工厂中自定义userDetailsService
     @Bean
     public UserDetailsService userDetailsService() {
         InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
         userDetailsService.createUser(User.withUsername("master").password("{noop}123456").roles("admin").build());//noop表示明文
         return userDetailsService;
     }/**
      * 定义UserDetailsService后启动服务测试结果:
      *    上面这段(1.默认AuthenticationManager:)默认可以不写,写的时候会报错:
      *    org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'webSecurityConfigurer':
      *    Requested bean is currently in creation: Is there an unresolvable circular reference?
      * 这段代码显得多余了
      *///2.自定义AuthenticationManager 推荐使用这种自定义方式,但并没有在工厂中进行暴露
     @Override
     public void configure(AuthenticationManagerBuilder builder) throws Exception {
         System.out.println("自定义AuthenticationManager:" + builder);
         builder.userDetailsService(userDetailsService());
     }
     /**
      * 启动服务,重新访问hello路径,发现原先的用户名、密码组合失效,需要使用master、123456才能登录成功
      * 输出结果:
      *    自定义AuthenticationManager:org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$2@24fabd0f
      *///作用: 用来将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入
     @Override
     @Bean//加上Bean注解才会有AuthenticationManager具体实例
     public AuthenticationManager authenticationManagerBean() throws Exception {
         return super.authenticationManagerBean();
     }
     
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
                 .mvcMatchers("/index").permitAll() //放行/index请求
                 .mvcMatchers("/login.html").permitAll() //放行/login.html请求
                 .anyRequest().authenticated() //其它请求需要登录认证后才能访问
                 .and()
                 .formLogin() //默认form表单页面登录
                 .loginPage("/login.html") //使用自定义登录页面登录页面登录
                 .loginProcessingUrl("/doLogin") //使用自定义登录页面时需要重新指定url,对应login.html中的action路径
                 .usernameParameter("uname") //重新指定用户名名称
                 .passwordParameter("pwd") //重新指定密码名称
                 //.successForwardUrl("/index") //认证成功后跳转路径
                 //forward 跳转路径  始终在认证成功之后跳转到指定请求 地址栏不变
                 //.defaultSuccessUrl("/hello") //默认认证成功后跳转路径
                 //.defaultSuccessUrl("/hello",true) //第二个参数设置为true时总是跳转,效果同successForwardUrl一致,默认false
                 //redirect 重定向  注意:如果之前有请求过的路径,会优先跳转之前的请求路径 地址栏改变
                 .successHandler(new MyAuthenticationSuccessHandler())//认证成功时处理,前后端分离解决方案
                 //.failureForwardUrl("/login.html")//认证失败之后,forward跳转
                 //.failureUrl("/login.html") //默认认证失败之后,redirect跳转
                 .failureHandler(new MyAuthenticationFailureHandler())//认证失败时处理,前后端解决方案
                 .and()
                 .logout()
                 //.logoutUrl("/logout")//指定注销登录URL,默认请求方式必须为GET
                 .logoutRequestMatcher(new OrRequestMatcher(
                         new AntPathRequestMatcher("/aaa", "GET"),//通过以get方式访问/aaa也可以实现登出
                         new AntPathRequestMatcher("/bbb", "POST")//通过以delete方式访问/bbb也可以实现登出
                 ))
                 .invalidateHttpSession(true)//默认开启会话失效
                 .clearAuthentication(true)//默认清除认证标志
                 //.logoutSuccessUrl("/login.html")//注销登录成功后跳转的页面
                 .logoutSuccessHandler(new MyLogoutSuccessHandler())
                 .and()
                 .csrf().disable();//此处先关闭CSRF跨站保护
     }
 }

6.4自定义内存数据源

通过上面分析可以重点在于UserDetailsService,查看UserDetailsService接口如下:

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

里面只有一个要实现的方法loadUserByUsername,返回类型为UserDetails,查看UserDetails定义如下:

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

UserDetails的实现类User
在这里插入图片描述
User定义

 //
 // Source code recreated from a .class file by IntelliJ IDEA
 // (powered by FernFlower decompiler)
 //package org.springframework.security.core.userdetails;import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
 import java.util.function.Function;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.springframework.security.core.CredentialsContainer;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.crypto.factory.PasswordEncoderFactories;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.util.Assert;public class User implements UserDetails, CredentialsContainer {
     private static final long serialVersionUID = 560L;
     private static final Log logger = LogFactory.getLog(User.class);
     private String password;
     private final String username;
     private final Set<GrantedAuthority> authorities;
     private final boolean accountNonExpired;
     private final boolean accountNonLocked;
     private final boolean credentialsNonExpired;
     private final boolean enabled;public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
         this(username, password, true, true, true, true, authorities);
     }public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
         Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor");
         this.username = username;
         this.password = password;
         this.enabled = enabled;
         this.accountNonExpired = accountNonExpired;
         this.credentialsNonExpired = credentialsNonExpired;
         this.accountNonLocked = accountNonLocked;
         this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
     }public Collection<GrantedAuthority> getAuthorities() {
         return this.authorities;
     }public String getPassword() {
         return this.password;
     }public String getUsername() {
         return this.username;
     }public boolean isEnabled() {
         return this.enabled;
     }public boolean isAccountNonExpired() {
         return this.accountNonExpired;
     }public boolean isAccountNonLocked() {
         return this.accountNonLocked;
     }public boolean isCredentialsNonExpired() {
         return this.credentialsNonExpired;
     }public void eraseCredentials() {
         this.password = null;
     }private static SortedSet<GrantedAuthority> sortAuthorities(Collection<? extends GrantedAuthority> authorities) {
         Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
         SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet(new AuthorityComparator());
         Iterator var2 = authorities.iterator();while(var2.hasNext()) {
             GrantedAuthority grantedAuthority = (GrantedAuthority)var2.next();
             Assert.notNull(grantedAuthority, "GrantedAuthority list cannot contain any null elements");
             sortedAuthorities.add(grantedAuthority);
         }return sortedAuthorities;
     }public boolean equals(Object obj) {
         return obj instanceof User ? this.username.equals(((User)obj).username) : false;
     }public int hashCode() {
         return this.username.hashCode();
     }public String toString() {
         StringBuilder sb = new StringBuilder();
         sb.append(this.getClass().getName()).append(" [");
         sb.append("Username=").append(this.username).append(", ");
         sb.append("Password=[PROTECTED], ");
         sb.append("Enabled=").append(this.enabled).append(", ");
         sb.append("AccountNonExpired=").append(this.accountNonExpired).append(", ");
         sb.append("credentialsNonExpired=").append(this.credentialsNonExpired).append(", ");
         sb.append("AccountNonLocked=").append(this.accountNonLocked).append(", ");
         sb.append("Granted Authorities=").append(this.authorities).append("]");
         return sb.toString();
     }public static UserBuilder withUsername(String username) {
         return builder().username(username);
     }public static UserBuilder builder() {
         return new UserBuilder();
     }/** @deprecated */
     @Deprecated
     public static UserBuilder withDefaultPasswordEncoder() {
         logger.warn("User.withDefaultPasswordEncoder() is considered unsafe for production and is only intended for sample applications.");
         PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
         UserBuilder var10000 = builder();
         Objects.requireNonNull(encoder);
         return var10000.passwordEncoder(encoder::encode);
     }public static UserBuilder withUserDetails(UserDetails userDetails) {
         return withUsername(userDetails.getUsername()).password(userDetails.getPassword()).accountExpired(!userDetails.isAccountNonExpired()).accountLocked(!userDetails.isAccountNonLocked()).authorities(userDetails.getAuthorities()).credentialsExpired(!userDetails.isCredentialsNonExpired()).disabled(!userDetails.isEnabled());
     }public static final class UserBuilder {
         private String username;
         private String password;
         private List<GrantedAuthority> authorities;
         private boolean accountExpired;
         private boolean accountLocked;
         private boolean credentialsExpired;
         private boolean disabled;
         private Function<String, String> passwordEncoder;private UserBuilder() {
             this.passwordEncoder = (password) -> {
                 return password;
             };
         }public UserBuilder username(String username) {
             Assert.notNull(username, "username cannot be null");
             this.username = username;
             return this;
         }public UserBuilder password(String password) {
             Assert.notNull(password, "password cannot be null");
             this.password = password;
             return this;
         }public UserBuilder passwordEncoder(Function<String, String> encoder) {
             Assert.notNull(encoder, "encoder cannot be null");
             this.passwordEncoder = encoder;
             return this;
         }public UserBuilder roles(String... roles) {
             List<GrantedAuthority> authorities = new ArrayList(roles.length);
             String[] var3 = roles;
             int var4 = roles.length;for(int var5 = 0; var5 < var4; ++var5) {
                 String role = var3[var5];
                 Assert.isTrue(!role.startsWith("ROLE_"), () -> {
                     return role + " cannot start with ROLE_ (it is automatically added)";
                 });
                 authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
             }return this.authorities((Collection)authorities);
         }public UserBuilder authorities(GrantedAuthority... authorities) {
             return this.authorities((Collection)Arrays.asList(authorities));
         }public UserBuilder authorities(Collection<? extends GrantedAuthority> authorities) {
             this.authorities = new ArrayList(authorities);
             return this;
         }public UserBuilder authorities(String... authorities) {
             return this.authorities((Collection)AuthorityUtils.createAuthorityList(authorities));
         }public UserBuilder accountExpired(boolean accountExpired) {
             this.accountExpired = accountExpired;
             return this;
         }public UserBuilder accountLocked(boolean accountLocked) {
             this.accountLocked = accountLocked;
             return this;
         }public UserBuilder credentialsExpired(boolean credentialsExpired) {
             this.credentialsExpired = credentialsExpired;
             return this;
         }public UserBuilder disabled(boolean disabled) {
             this.disabled = disabled;
             return this;
         }public UserDetails build() {
             String encodedPassword = (String)this.passwordEncoder.apply(this.password);
             return new User(this.username, encodedPassword, !this.disabled, !this.accountExpired, !this.credentialsExpired, !this.accountLocked, this.authorities);
         }
     }private static class AuthorityComparator implements Comparator<GrantedAuthority>, Serializable {
         private static final long serialVersionUID = 560L;private AuthorityComparator() {
         }public int compare(GrantedAuthority g1, GrantedAuthority g2) {
             if (g2.getAuthority() == null) {
                 return -1;
             } else {
                 return g1.getAuthority() == null ? 1 : g1.getAuthority().compareTo(g2.getAuthority());
             }
         }
     }
 }


主要关注User的几个成员变量:

 private String password;
 private final String username;
 private final Set<GrantedAuthority> authorities;
 private final boolean accountNonExpired;
 private final boolean accountNonLocked;
 private final boolean credentialsNonExpired;
 private final boolean enabled;

举例:

 @Configuration
 public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Bean
     public UserDetailsService userDetailsService(){
         InMemoryUserDetailsManager inMemoryUserDetailsManager
                 = new InMemoryUserDetailsManager();
         UserDetails u1 = User.withUsername("zhangs")
                 .password("{noop}111").roles("USER").build();
         inMemoryUserDetailsManager.createUser(u1);
         return inMemoryUserDetailsManager;
     }@Override
     protected void configure(AuthenticationManagerBuilder auth) 
       throws Exception {
         auth.userDetailsService(userDetailsService());
     }   
 }
;