目录

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<? extends GrantedAuthority> 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 代码,我写到一起了:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>newlogin</title>
</head>
<body>
...
    <form action="/phoneUrl" method="post" enctype="application/x-www-form-urlencoded">
        <div >
            <!-- input 标签的 name 需要与后端拿取数据时的 getParameter 名称一致-->
            手机号码:<input type="text" name="phone" placeholder="请输入手机号码"  />
        </div>
        <div >
            <!-- input 标签的 name 需要与后端拿取数据时的 getParameter 名称一致-->
            密码:<input type="password"   name="password" placeholder="请输入密码" />
        </div>
        <div >
            <input type="submit" value="登录"  >&emsp;
        </div>
    </form>
</body>
</html>

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 调用:

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

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

并且如果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 的调用:

可以看到 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 调用:

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

 也是可以成功使用的!


另外,我们也可以直接在 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 调用:

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

 可以看到 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);
        }

    }

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐