多种用户定义方式
在前面的章节中,我们定义用户主要是两种方式:
-
第一种方式是 2.4 节中使用的重写 configure(AuthenticationManagerBuilder) 方法的方式。
-
第二种方式是 3.2 节中定义多个数据源时,我们直接向 Spring 容器中注入了 UserDetailsService 对象。
那么这两种用户定义方式有什么区别?
根据前面的源码分析可知,在 Spring Security 中存在两种类型的 AuthenticationManager,一种是全局的 AuthenticationManager,另一种则是局部的 AuthenticationManager。局部的 AuthenticationManager 由 HttpSecurity 进行配置,而全局的 AuthenticationManager 可以不用配置,系统会默认提供一个全局的 AuthenticationManager 对象,也可以通过重写 configure(AuthenticationManagerBuilder) 方法进行全局配置。
当进行用户身份验证时,首先会通过局部的 AuthenticationManager 对象进行验证,如果验证失败,则会调用其 parent 也就是全局的 AuthenticationManager 再次进行验证。
所以开发者在定义用户时,也分为两种情况,一种是针对局部 AuthenticationManager 定义的用户,另一种则是针对全局 AuthenticationManager 定义的用户。
为了演示方便,接下来的案例我们将采用 InMemoryUserDetailsManager 来构建用户对象,读者也可以自行使用基于 MyBatis 或者 Spring Data JPA 定义的 UserDetailsService 实例。
先来看针对局部 AuthenticationManager 定义的用户:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
InMemoryUserDetailsManager users = new InMemoryUserDetailsManager();
users.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin").build());
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.userDetailsService(users)
.csrf().disable();
}
}
在上面这段代码中,我们基于内存来管理用户,并向 users 中添加了一入用户,将配置好的 users 对象添加到 HttpSecurity 中,也就是配置到局部的 AuthenticationManager 中。
配置完成后,启动项目。项目启动成功后,我们就可以使用 javaboy/123 来登录系统了。
但是读者注意,当我们启动项目时,在 IDEA 控制台输出的日志中可以看到如下内容:
Using generated security password: cfc7f8b5-8346-492e-b25c-90c2c4501350
通过第 2 章的介绍可知,这个是系统自动生成的用户,那么我们是否可以使用系统自动生成的用户进行登录呢?答案是可以的,为什么呢?
回顾 2.1.2.1 小节,系统自动提供的用户对象实际上就是往 Spring 容器中注册了一个 InMemoryUserDetailsManager 对象。而在前面的代码中,我们没有重写 configure(AuthenticationManagerBuilder) 方法,这意味着全局的 AuthenticationManager 是通过 AuthenticationConfiguration#getAuthenticationManager 方法自动生成的,在生成的过程中,会从 Spring 容器中查找对应的 UserDetailsService 实例进行配置(具体配置在 InitializeUserDetailsManagerConfigurer 类中)。所以系统自动提供的用户实际上相当于是全局 AuthenticationManager 对应的用户。
以上面的代码为例,当我们开始执行登录后,Spring Security 首先会调用局部 AuthenticationManager 去进行登录校验,如果登录的用户名/密码是 javaboy/123,那就直接登录成功,否则登录失败。当登录失败后,会继续调用局部 AuthenticationManager 的 parent 继续进行校验,此时如果登录的用户名/密码是 user/cfc7f8b5-8346-492e-b25c-90c2c4501350,则登录成功,否则登录失败。
这是针对局部 AuthenticationManager 定义的用户,我们也可以将定义的用户配置给全局的 AuthenticationManager,由于默认的全局 AuthenticationManager 在配置时会从 Spring 容器中查找 UserDetailsService 实例,所以我们如果针对全局 AuthenticationManager 配置用户,只需要往 Spring 容器中注入一个 UserDetailsService 实例即可,代码如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
UserDetailsService us() {
InMemoryUserDetailsManager users = new InMemoryUserDetailsManager();
users.createUser(User.withUsername("江南一点雨")
.password("{noop}123").roles("admin").build());
return users;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
InMemoryUserDetailsManager users = new InMemoryUserDetailsManager();
users.createUser(User.withUsername("javaboy")
.password("{noop}123").roles("admin").build());
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.userDetailsService(users)
.csrf().disable();
}
}
配置完成后,当我们启动项目时,全局的 AuthenticationManager 在配置时会去 Spring 容器中查找 UserDetailsService 实例,找到的就是我们自定义的 UserDetailsService 实例。当我们进行登录时,系统拿着我们输入的用户名/密码,首先和 javaboy/123 进行匹配,如果匹配不上的话,再去和 江南一点雨/123 进行匹配。
当然,开发者也可以不使用 Spring Security 提供的默认的全局 AuthenticationManager 对象,而是通过重写 configure(AuthenticationManagerBuilder) 方法来自定义全局 AuthenticationManager 对象:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("javagirl")
.password("{noop}123")
.roles("admin");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
InMemoryUserDetailsManager users = new InMemoryUserDetailsManager();
users.createUser(User.withUsername("javaboy")
.password("{noop}123").roles("admin").build());
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.userDetailsService(users)
.csrf().disable();
}
}
根据 4.1.5 小节中对 WebSecurityConfigurerAdapter 的源码分析可知,一旦我们重写了 configure(AuthenticationManagerBuilder) 方法,则全局的 AuthenticationManager 对象将不再通过 AuthenticationConfiguration#getAuthenticationManager 方法来构建,而是通过 WebSecurityConfigurerAdapter 中的 localConfigureAuthenticationBldr 变量来构建,该变量也是我们重写的 configure(AuthenticationManagerBuilder) 方法的参数。
配置完成后,当我们启动项目时,全局的 AuthenticationManager 在构建时会直接使用 configure(AuthenticationManagerBuilder) 方法的 auth 变量去构建,使用的用户也是我们配置给 auth 变量的用户。当我们进行登录时,系统会将所输入的用户名/密码,首先和 iavaboy/123 进行匹配,如果匹配不上的话,再去和 javagirl/123 进行匹配。
需要注意的是,一旦重写了 configure(AuthenticationManagerBuilder) 方法,那么全局 AuthenticationManager 对象中使用的用户,将以 configure(AuthenticationManagerBuilder) 方法中定义的用户为准。此时,如果我们还向 Spring 容器中注入了另外一个 UserDetailsService 实例,那么该实例中定义的用户将不会生效(因为 AuthenticationConfiguration#getAuthenticationManager 方法没有被调用)。
这就是 Spring Security 中几种不同的用户定义方式,相信通过这几个案例,读者对于全局 AuthenticationManager 和局部 AuthenticationManager 对象会有更加深刻的理解。