spring security——学习笔记(day05)-实现自定义 AuthenticationProvider身份认证-手机号码认证登录

目录

5.2 自定义 Provider 身份认证

5.2.1 编码思路和疑问

5.2.2 创建用户信息配置类 PhonePasswordAuthenticationToken

5.2.2 修改自定义的 UserDetailsService

5.2.3 创建身份认证提供者 PhoneAuthenticationProvider

5.2.4 创建过滤链 PhonePasswordFilter

5.2.5 修改 security 配置,添加过滤链和相关bean

5.2.6 自定义 PhonePasswordFilter 的bean添加 providerManager 方式


 项目源代码:https://github.com/maojianqiu/Demos/tree/main/springsecurity01/demo02

5.2 自定义 Provider 身份认证

5.2.1 编码思路和疑问

之前学习了 security 默认的表单身份认证思路解析,即用户名密码登录认证,现在要在这个基础上实手机号码密码认证登录。

先不去搜索学习资料,先理一下现有的思路:

首先需要向 security 的过滤链中添加一个过滤器,能够捕捉我们的登录请求,然后通过请求中的手机号码等信息封装一个用户信息 ,并交给用户信息处理者进行处理匹配,这个用户匹配到对应的处理者之后,通过处理者的业务调用,拿到对应的数据源,然后进行密码匹配,匹配成功就返回已认证的用户信息,逐级返回到过滤器中,过滤器中将用户信息存到安全上下文Holder中,方便后续请求使用,并跳过后面过滤链。

转化为我们理解的使用类与接口:

首先向 security 的过滤链中添加一个过滤器(需要继承AbstractAuthenticationProcessingFilter,比如UsernamePasswordAuthenticationFilter)(怎样添加过滤器?),并能够捕捉我们的登录请求(怎样配置请求?),然后通过请求中的手机号码等信息封装一个用户信息(继承 AbstractAuthenticationToken,比如UsernamePasswordAuthenticationToken) ,并交给用户信息处理者(ProviderManager)进行处理匹配,这个用户匹配到自定义的处理者(继承 AbstractUserDetailsAuthenticationProvider,比如 DaoAuthenticationProvider )之后(怎样添加 Provider?),通过处理者的业务调用(UserDetailsService),拿到对应的数据源,然后进行密码匹配,匹配成功就返回已认证的自定义用户信息,逐级返回到过滤器中,过滤器中将用户信息存到安全上下文中(SecurityContext),并放到Holder中(SecurityContextHolder),方便后续请求使用,并跳过后面过滤链。

那我们的开发步骤呢:

1. 首先先创建用户信息配置类 PhonePasswordAuthenticationToken ,并继承 UsernamePasswordAuthenticationToken,思考:为什么不继承 AbstractAuthenticationToken?

2. 然后创建 PhoneAuthenticationProvider,并继承 AbstractUserDetailsAuthenticationProvider;

3. 之后创建 PhonePasswordFilter 并继承 AbstractAuthenticationProcessingFilter;

4. 最后将过滤器添加到过滤链中,并将 Provider 添加到 Manager 中。

带着疑问,一边编码,一边看默认的流程源码,一边搜!

5.2.2 创建用户信息配置类 PhonePasswordAuthenticationToken

看下面的代码:

//继承 UsernamePasswordAuthenticationToken ,不继承 AbstractAuthenticationToken 的原因是后续会进行类型匹配,现在不需要那么高深的配置,所以先继承 UsernamePasswordAuthenticationToken
public class PhonePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
    private static final long serialVersionUID = 6339981734663827267L;

    //注意,Authentication 对象中,会需要密码信息(Credentials)、身份信息(Principal)、权限信息(Authorities)、细节信息(Details),但不一定都需要


    public PhonePasswordAuthenticationToken(Object principal, Object credentials) {
        //调用父类的构造函数,创建一个未认证的 Token
        super(principal, credentials);
    }

    public PhonePasswordAuthenticationToken(Object principal, Object credentials, Collection authorities) {
        //调用父类的构造函数,创建一个已认证的 Token
        super(principal, credentials, authorities);
    }

}

extends UsernamePasswordAuthenticationToken 之后,就只需要创建两个默认的构造器,也就是调用 UsernamePasswordAuthenticationToken 的构造方法创建 Token 对象。

Authentication 有四项重要的变量:

密码信息(Credentials)、密码一般业务都是一样的

身份信息(Principal)、身份信息可以是用户名、手机号码、邮箱,看我们的业务了

权限信息(Authorities)、用户拥有的权限一般业务都是一样的

细节信息(Details)、细节信息一般业务都是一样的

所以后续如果是通过邮箱登录,那么只需要把前端传的邮箱信息赋值给 Principal 就行,其余的一般不用动。

PhonePasswordAuthenticationToken 这个类一般涉及到的修改比较少。

5.2.2 修改自定义的 UserDetailsService

因为我们是通过手机号码认证登录,所以不能使用 loadUserByUsername()了(当然也可以在这个方法中加上 if 判断等逻辑,但是代码就有点耦合了),所以我们需要再新加一个通过 phone 查询的方法,如下:

注意,因为这是直接从demo1中复制的,所以会涉及到增加手机号码 phone 字段,和增加getUserInfoByPhone 查询语句,涉及到 User、UserMapper、UserMapper.xml,以及数据库的更改,所以要记得修改对应的代码!!!见最上方源代码

public class MyUserDetailsService implements UserDetailsService {

...

    //通过 phone 获取用户信息,前提手机号码能定位唯一用户
    public UserDetails loadUserByPhone(String phone) throws UsernameNotFoundException {
        //从持久层获取数据
        User user = userMapper.getUserInfoByPhone(phone);

        if(user == null){
            //这是 security 自带的异常,会在过滤连中捕捉到
            throw new UsernameNotFoundException("用户不存在!");
        }

        //现在 user 对象里面的 roles 是 string 类型,并且用逗号隔开的,我们需要将 roles 设置到 authorities 类型中。
        //我们需要把他修改为 security 可识别的权限类型 ,GrantedAuthority 接口是 security 保存权限的类型,SimpleGrantedAuthority 是它的实现类,也是security 最常使用的。
        //AuthorityUtils.commaSeparatedStringToAuthorityList( String )是 security 提供的用于将逗号隔开的权限字符串切割成权限列表。
        user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));

        return user;
    }

...

}

5.2.3 创建身份认证提供者 PhoneAuthenticationProvider

这里截取的代码是核心代码,其余业务代码会放到最后面介绍,或者也可以直接看源代码,先看代码:


public class PhoneAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    //这里需要注入bean
    @Autowired
    private PasswordEncoder passwordEncoder;

    //这里需要注入bean
    @Autowired
    private MyUserDetailsService userDetailsService;

    //(核心)覆写 supports 方法,里面需要判断是否是自定义的          PhonePasswordAuthenticationToken 类
    public boolean supports(Class authentication) {
        return PhonePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }


    //(核心)这里是去数据源中拿取用户信息,是由 父类 中的 authenticate() 进行调用
    @Override
    protected UserDetails retrieveUser(String phone, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        ...

        try {
            //这里通过 UserDetailsService 类,拿到用户信息,若没有就是 null
            UserDetails loadedUser = this.userDetailsService.loadUserByPhone(phone);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            ...
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }

    //判断密码是否匹配
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        //若请求信息中的密码信息为 null ,则抛出异常
        if (authentication.getCredentials() == null) {
            this.logger.debug("Failed to authenticate since no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            //不为 null 则进行密码判等
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Failed to authenticate since password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }

}

PhoneAuthenticationProvider 这个类中需要覆写三个方法

1.   public boolean supports(Class authentication) 

这里是在 ProviderManager 中进行调用的,目的是将 provider 与 authentication 进行匹配,匹配上了就代表这个 provider 可以处理这个 authentication ,我们需要的就是使用正确的 provider 来处理 authentication。

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

...

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    ...
       //在这里进行匹配,调用 Provider 的 supports() 方法
       if (provider.supports(toTest)) {
    ...
    }
...

}


public class PhoneAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
...
    //(核心)覆写 supports 方法,里面需要判断是否是自定义的 PhonePasswordAuthenticationToken 类
    public boolean supports(Class authentication) {
        return PhonePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

...
}

2.protected UserDetails retrieveUser(String phone, UsernamePasswordAuthenticationToken authentication) 

我们在上面的 provider 中匹配成功之后就会在 providerManager 中调用此 provider#authenticate() ,在这个方法里里面进行获取用户信息以及验证密码是否正确,这个方法就是获取用户信息的。

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

...

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    ...
       //在这里进行匹配,调用 Provider 的 supports() 方法
       if (provider.supports(toTest)) {
           ...
           //匹配对应的 provider 之后,调用 provider 的认证方法,这里一般都是调用 AbstractUserDetailsAuthenticationProvider 的方法!也就是我们自定义的 provider 的父类
           result = provider.authenticate(authentication);
           ...
       }
    ...
    }
...

}


public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
...
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {    
        //注意这里!!!
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
            return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
        });
    ...
         //在这里调用实现类的 retrieveUser() 方法,也就是我们覆写的方法
         user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
    ...
    }
...
}



public class PhoneAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
...
    //注意这里需要注入bean
    @Autowired
    private MyUserDetailsService userDetailsService;


    //(核心)这里是去数据源中拿取用户信息,是由 父类 中的 authenticate() 进行调用
    @Override
    protected UserDetails retrieveUser(String phone, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        ...

        try {
            //这里通过 UserDetailsService 类,拿到用户信息,若没有就是 null,这里的 UserDetailsService 就是我们注入的,注意里面的方法不能使用 loadUserByUsername()了,因为这是通过 username 查询的,所以我们需要再新加一个通过 phone 查询的方法
            UserDetails loadedUser = this.userDetailsService.loadUserByPhone(phone);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            ...
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }
...

}

3.protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) 

在 provider#authenticate()  在这个方法里面获取用户信息后就验证密码是否正确,这个方法就是校验密码的。

public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
...
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {    
        //注意这里!!!
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
            return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
        });
        ...
         //在这里调用实现类的 retrieveUser() 方法,也就是我们覆写的方法
         user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);

        ...
        //获取到用户后,在这里调用实现类的密码校验方法
        this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        ...
        //最终校验成功后,会返回已认证的用户信息
        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }
...
}


public class PhoneAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    //这里需要注入bean
    @Autowired
    private PasswordEncoder passwordEncoder;
...

    //判断密码是否匹配
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        //若请求信息中的密码信息为 null ,则抛出异常
        if (authentication.getCredentials() == null) {
            this.logger.debug("Failed to authenticate since no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            //不为 null 则进行密码判等
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Failed to authenticate since password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }
...
}

所以我们自定义的 provider 类,至少需要提供 1.provider 匹配,2.获取用户信息,3.校验密码。

注意,之前我们为什么要让 PhonePasswordAuthenticationToken  继承 UsernamePasswordAuthenticationToken,为什么不直接继承 AbstractAuthenticationToken?

答:可以直接继承 AbstractAuthenticationToken,这个没关系,不过我并没有打算现在往深层扩展,因为如果我们直接继承 AbstractAuthenticationToken,那么就需要在自定义 provider 中覆写 public Authentication authenticate(Authentication authentication) 方法!

为什么?因为 authenticate() 这个方法现在是父类 AbstractUserDetailsAuthenticationProvider 的,并且方法里面有这样一段代码:

public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
...
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {    
        //注意这里!!!
        //使用了断言,意思是如果 authentication 不是 UsernamePasswordAuthenticationToken.class类型就抛出异常!
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
            return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
        });
        
        ...
    }
...
}

所以,如果我们想要继续使用这个父类的方法(毕竟写的很全面),不覆写这个方法,那么我们传给他的 authentication 就得是 UsernamePasswordAuthenticationToken 类或者其子类的。

所以,我们自定义的 PhonePasswordAuthenticationToken 的作用是标注好什么用户信息类型,并在 prioviderManager 里面判断当前 provider 支不支持这个类型,支持的话就调用这个 provicer 进行认证,此时会调用 provider 的父类的 authenticate() 方法,在这个方法第一行就先判断是不是 UsernamePasswordAuthenticationToken 类型,我们自定义的类是属于这个类型的,所以通过!

当然,我们可以覆写  authenticate() 方法,至少涉及的代码有些多,我要一步一步学,先跑起来在学习后面优化的事情~

5.2.4 创建过滤链 PhonePasswordFilter

现在添加过滤链,过滤链有什么必须添加的吗?

1.请求路径判断,why?还记得 UsernamePasswordAuthenticationFilter 类中就有路径匹配,这是应为我们现在写的是登录认证接口,也只有登录时会使用到,别的请求(如查询列表新增等)是不会进行登录认证的,因为这时候判断的就是是否登录是否有权限,而不是登录认证!

所以我们需要添加匹配的请求路径,如果是登陆请求就走这个过滤链业务,执行完后走下面的过滤链,如果不是就跳过直接走下面的过滤链业务!

2.doFilter() 我们使用父类 AbstractAuthenticationProcessingFilter 的就行,不需要覆写,但里面的尝试认证 attemptAuthentication() 方法需要覆写,我们在这个方法里面构建未认证的 Token 然后交给 providerManager 进行认证。

看代码:

public class PhonePasswordFilter extends AbstractAuthenticationProcessingFilter {

    //定义前端 form 表单传数值时,存到request中的参数名称,一般是 form 表单中的标签的 name 值。
    public static final String SPRING_SECURITY_FORM_PHONE_KEY = "phone";

    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

    private boolean postOnly = true;

    public PhonePasswordFilter(String defaultFilterProcessesUrl) {
        //1.登录匹配的URL和Method
        //这里可以直接写死,也可以通过 @Vlaue 拿取配置类里的数据,也可以通过构造器传值,看实际业务。最好是通过配置类拿取,这样修改比较方便~
        super(new AntPathRequestMatcher(defaultFilterProcessesUrl, "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        //1.判断是不是 post 方法,这一步是校验,不是必须的
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        //2.从request中拿取name为“phone”的参数值,name为“password”的参数值
        String phone = request.getParameter(this.SPRING_SECURITY_FORM_PHONE_KEY);
        phone = (phone != null) ? phone : "";
        phone = phone.trim();
        String password = request.getParameter(this.SPRING_SECURITY_FORM_PASSWORD_KEY);
        password = (password != null) ? password : "";
        //3.封装一个 Authentication ,这里需要自定义
        PhonePasswordAuthenticationToken authRequest = new PhonePasswordAuthenticationToken(phone, password);
        //4.这里是设置细节信息
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    //调用父类的 setDetails() 方法,设置细节信息
    protected void setDetails(HttpServletRequest request, PhonePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }
}

1.匹配的url和方式需要我们提供,我们需要赋值给父类的 requiresAuthenticationRequestMatcher 变量,可以直接写死,也可以通过 @Vlaue 拿取配置类里的数据,也可以通过构造器传值,看实际业务。最好是通过配置类拿取,这样后面修改比较方便~

这里直接通过构造器传值,然后在 security 配置类里面通过构造器构建 bean。

但是匹配的方法是在父类里面的 doFilter() 方法里面匹配的,我们不用关心

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {
...

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
        //进行路径匹配,如果匹配上就执行后面的内容,
		if (!requiresAuthentication(request, response)) {
            //如果匹配不上直接调用后面的过滤链
			chain.doFilter(request, response);
			return;
		}
        ...
        //拿取到用户后接着调用后面的过滤链
        chain.doFilter(request, response);
        ...
    }
...
}

2.这里我们需要从 request 中拿取到 form 表单信息,这里就需要确定传参的属性名称,比如手机号码用“phone”,密码用“password”,通过 request.getParameter("phone");拿到,同时不要忘记前端页面也就是登录页面中,form 表单内容的 name 必须和这里的一致,否则会拿取不到数据的!看登录页面 newLogin.hxml 代码,我写到一起了:




    
    newlogin


...
    
手机号码:
密码:

5.2.5 修改 security 配置,添加过滤链和相关bean

我们需要将自定义过滤链加进去,在这里传入登录拦截的 url ;同时要创建自定义 provider bean。

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    protected void configure(HttpSecurity http) throws Exception {
        //addFilter 就是添加过滤器的,我们直接添加到默认的 UsernamePasswordAuthenticationFilter 前面。
        http.addFilterBefore(getPhonePasswordFilter(),UsernamePasswordAuthenticationFilter.class);
        http.authorizeRequests()
                .antMatchers("/newlogin.html").permitAll()
                //设置自定义手机号码登录请求不设置访问权限
                .antMatchers("/phoneUrl").permitAll()
                .antMatchers("/autho/all/**").permitAll()
                .antMatchers("/autho/admin/**").hasRole("ADMIN")
                .antMatchers("/autho/user/**").hasRole("USER")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/newlogin.html")
                .and()
                .csrf().disable();
    }


    @Bean
    public PhoneAuthenticationProvider getPhoneAuthenticationProvider(){
        PhoneAuthenticationProvider phoneAuthenticationProvider = new PhoneAuthenticationProvider();
        return new PhoneAuthenticationProvider();
    }

    @Bean
    public PhonePasswordFilter getPhonePasswordFilter() throws Exception {
        //这里加上登录拦截的 url
        PhonePasswordFilter phonePasswordFilter = new PhonePasswordFilter("/phoneUrl");
        //AbstractAuthenticationProcessingFilter 类的创建,必须提供 manager。若没有提供编译时不报错,会在运行时报错
        //如果没有提供会报错:Caused by: java.lang.IllegalArgumentException: authenticationManager must be specified
        phonePasswordFilter.setAuthenticationManager(authenticationManager());
        return phonePasswordFilter;
    }

    @Bean
    public MyUserDetailsService getUserDetailsService(){
        return new MyUserDetailsService();
    }

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

AbstractUserDetailsAuthenticationProvider 类的 bean 添加很简单,直接 new 一个就可以。

AbstractAuthenticationProcessingFilter 类的 bean 比较复杂,

1.构建时需要传拦截路径 url ,

2.需要给他注入 一个 AuthenticationManager,这里会涉及到 Provider。

我们注入 AuthenticationManager 时,有多种方式:

//方式1.在 PhonePasswordFilter bean 里面调用 WebSecurityConfigurerAdapter # authenticationManager() 进行添加
@Bean
public PhonePasswordFilter getPhonePasswordFilter() throws Exception {
    PhonePasswordFilter phonePasswordFilter = new PhonePasswordFilter("/phoneUrl");
    //在这里将 security 配置的 ProviderManager  bean 注入给 phonePasswordFilter
    phonePasswordFilter.setAuthenticationManager(authenticationManager());
    return phonePasswordFilter;
}


//方式2.在 PhonePasswordFilter bean 里面 new providerManager(Provider),进行添加
@Bean
public PhonePasswordFilter getPhonePasswordFilter() throws Exception {
    PhonePasswordFilter phonePasswordFilter = new PhonePasswordFilter("/phoneUrl");
    //在这里我们将创建的 provider bean 注入给 ProviderManager 
    ProviderManager providerManager = new ProviderManager(Collections.singletonList(getPhoneAuthenticationProvider()));
    //在这里将我们创建的 ProviderManager  bean 注入给 phonePasswordFilter
    phonePasswordFilter.setAuthenticationManager(providerManager);
    return phonePasswordFilter;
}



注意,方式1 方式 2 在设置 providerManager 时,security 配置都会搜索自定义的 provider bean,若有 bean ,就直接拿取注入,若没有就创建 bean,并添加到 security 默认的 manager 里面。说白了,无论使用这两个哪一种方式, security 都会在默认的配置里面注入搜索到的 provider 实现 bean 。只不过我们能决定自定义的 Filter 的 manager 中使用哪一种 provider 。

此时我们运行登录,使用方式1或方式2都可以成功!

5.2.6 自定义 PhonePasswordFilter bean添加 providerManager 的方式

方式1:是直接使用 security 配置类的方法创建的,这个方法会通过 WebSecurityConfigurerAdapter 创建一个 AuthenticationManager (spring 自动获取到 provider 的实现类),并且是先执行的,然后在这儿之后的 WebSecurityConfigurerAdapter # getHttp() 方法也会创建一个 security 默认的 AuthenticationManager,并且会查找到 authenticationManager() 创建的 provider bean ,也就是将 authenticationManager() 创建的 AuthenticationManager 赋值给 security 默认的 AuthenticationManager 里面的 parent 变量。所以这两个manager之间是有关系的!

也就是自定义的 PhonePasswordFilter 里面的 AuthenticationManager 对象里面的 provider 变量只有注入的 PhoneAuthenticationProvider,他的 parent 变量是 null。

而 security 默认的 AuthenticationManager ,如 UsernamePasswordAuthenticationFilter 里面的 AuthenticationManager 对象里面的 provider 变量还是只有默认的 Provider (例如AnonymousAuthenticationProvider),但是 parent 变量里面的 provider 变量是有 PhoneAuthenticationProvider,并且 parent 变量对象就是这里创建的 AuthenticationManager 变量!

通过手机号码登录请求 "/phoneUrl"的 PhonePasswordFilter 调用:

spring security——学习笔记(day05)-实现自定义 AuthenticationProvider身份认证-手机号码认证登录_第1张图片

通过 security 默认登录请求 "/newLogin.xml"的 UsernamePasswordAuthenticationFilter 调用:

spring security——学习笔记(day05)-实现自定义 AuthenticationProvider身份认证-手机号码认证登录_第2张图片

 所以可以看出,这两个是有关联的!

并且如果security配置中搜索到了自定义provider类(也就是 AbstractAuthenticationProcessingFilter 的继承类)就是创建 bean ,并且不会在创建 DaoAuthenticationProvider bean了!

如果想要 DaoAuthenticationProvider 也能够同时使用,就在 security 配置类的#configure(HttpSecurity http)方法中加上一行代码:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
    protected void configure(HttpSecurity http) throws Exception {
        ...
        //添加自定义provider 之后,security 配置就不会自动注入 DaoAuthenticationProvider 了,如果还想使用,就调用下方代码;
        http.userDetailsService(getUserDetailsService());
        ...
    }
...
}

加上之后我们在看两个 Filter 的调用:

spring security——学习笔记(day05)-实现自定义 AuthenticationProvider身份认证-手机号码认证登录_第3张图片

spring security——学习笔记(day05)-实现自定义 AuthenticationProvider身份认证-手机号码认证登录_第4张图片

可以看到 security 默认配置的 manager 对象里面是有了 DaoAuthenticationProvider 对象的!

所以我们可以同时使用这两个身份认证方法进行的登录!


方式 2:这样的方式是我们主动创建一个新的 AuthenticationManager 以及新的 provider 给 PhonePasswordFilter,那么当我们使用 PhonePasswordFilter 的 AuthenticationManager 时,它里面的 provider 变量只有我们通过构建方法加进去的,并且 parent 变量为 null!同时 security 也会创建一个默认的 AuthenticationManager,并且也会去查找 bean ,那么就会查找到我们创建的 provider bean ,也就是将我们这里创建的 AuthenticationManager 赋值给 security 默认的 AuthenticationManager 里面的 parent 变量。所以这两个manager之间也是有关系的!

注意此时是添加了: http.userDetailsService(getUserDetailsService()) 的。

通过手机号码登录请求 "/phoneUrl"的 PhonePasswordFilter 调用:

spring security——学习笔记(day05)-实现自定义 AuthenticationProvider身份认证-手机号码认证登录_第5张图片

通过 security 默认登录请求 "/newLogin.xml"的 UsernamePasswordAuthenticationFilter 调用:

spring security——学习笔记(day05)-实现自定义 AuthenticationProvider身份认证-手机号码认证登录_第6张图片

 也是可以成功使用的!


另外,我们也可以直接在 security 的 manager 中添加 provider ,我们只需要直接在 WebSecurityConfigurerAdapter # configure(HttpSecurity http) 中调用 http.authenticationProvider(provider) 进行添加:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
    protected void configure(HttpSecurity http) throws Exception {
        //在这里添加 provider ,会添加到security 默认的 manager 里的 providers 里面!
        http.authenticationProvider(getPhoneAuthenticationProvider())
        ...
    }

    @Bean
    public PhonePasswordFilter getPhonePasswordFilter() throws Exception {
        PhonePasswordFilter phonePasswordFilter = new PhonePasswordFilter("/phoneUrl");
        //那么这里设置什么样的 manager 就无所谓了,因为 security 默认的 manager 里的 providers 就已经注入了 PhoneAuthenticationProvider
        phonePasswordFilter.setAuthenticationManager(authenticationManager());
        return phonePasswordFilter;
    }

...
}

注意此时是添加了: http.userDetailsService(getUserDetailsService()) 的。

通过手机号码登录请求 "/phoneUrl"的 PhonePasswordFilter 调用:

spring security——学习笔记(day05)-实现自定义 AuthenticationProvider身份认证-手机号码认证登录_第7张图片

通过 security 默认登录请求 "/newLogin.xml"的 UsernamePasswordAuthenticationFilter 调用:

spring security——学习笔记(day05)-实现自定义 AuthenticationProvider身份认证-手机号码认证登录_第8张图片 可以看到 security 默认的 manager 里的 providers 里面是有加入的自定义 provider 的!

啊,内容有些多,有好多还没有记录,比如 PhoneAuthenticationProvider 类里面可以仿照 DaoAuthenticationProvider 加上计时攻击防护的业务代码,(待学习)

    //(可不添加)通过名字可以解析为:准备计时攻击防护,这个在 #retrieveUser() 方法的第一行就进行调用,
    private void prepareTimingAttackProtection() {
        //这里会将一串字符 "userNotFoundPassword" 进行编码后赋值给 this.userNotFoundEncodedPassword,
        if (this.userNotFoundEncodedPassword == null) {
            this.userNotFoundEncodedPassword = this.passwordEncoder.encode("userNotFoundPassword");
        }

    }

    //(可不添加)通过名字可以解析为:抵御定时攻击,这个在 #retrieveUser() 方法中拿取不到对象时,catch里面第一行进行调用,
    private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
        //这里是如果没有搜索到用户,就会调用请求中的密码和 this.userNotFoundEncodedPassword 进行判等,这一定会为 false !为什么要多此一举呢?
        if (authentication.getCredentials() != null) {
            String presentedPassword = authentication.getCredentials().toString();
            this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
        }

    }

你可能感兴趣的:(springsecurity,spring,安全,java,spring,boot)