目录
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
之前学习了 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 中。
带着疑问,一边编码,一边看默认的流程源码,一边搜!
看下面的代码:
//继承 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 这个类一般涉及到的修改比较少。
因为我们是通过手机号码认证登录,所以不能使用 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;
}
...
}
这里截取的代码是核心代码,其余业务代码会放到最后面介绍,或者也可以直接看源代码,先看代码:
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() 方法,至少涉及的代码有些多,我要一步一步学,先跑起来在学习后面优化的事情~
现在添加过滤链,过滤链有什么必须添加的吗?
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
...
我们需要将自定义过滤链加进去,在这里传入登录拦截的 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都可以成功!
方式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);
}
}