覆盖Spring Boot自动配置
一般来说,如果不用配置就能得到和显式配置一样的结果,那么不写配置是最直接的选择。既然如此,那干嘛还要多做额外的工作呢?如果不用编写和维护额外的配置代码也行,那何必还要它们呢?
大多数情况下,自动配置的 Bean 刚好能满足你的需要,不需要去覆盖它们。但某些情况下,Spring Boot 在自动配置时还不能很好地进行推断。
这里有个不错的例子:当你在应用程序里添加安全特性时,自动配置做得还不够好。安全配置并不是放之四海而皆准的,围绕应用程序安全有很多决策要做,Spring Boot 不能替你做决定。虽然 Spring Boot 为安全提供了一些基本的自动配置,但是你还是需要自己覆盖一些配置以满足特定的安全要求。
想知道如何用显式的配置来覆盖自动配置,我们先从为阅读列表应用程序添加 Spring Security 入手。在了解自动配置提供了什么之后,我们再来覆盖基础的安全配置,以满足特定的场景需求。
保护应用程序
Spring Boot 自动配置让应用程序的安全工作变得易如反掌,你要做的只是添加 Security 起步依赖。以 Gradle 为例,应添加如下依赖:
compile("org.springframework.boot:spring-boot-starter-security")
如果使用 Maven,那么你要在项目的 <dependencies> 块中加入如下 <dependency>:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
这样就搞定了!重新构建应用程序后运行即可,现在这就是一个安全的 Web 应用程序了!Security 起步依赖在应用程序的 Classpath 里添加了 Spring Secuirty(和其他一些东西)。Classpath 里有 Spring Security 后,自动配置就能介入其中创建一个基本的 Spring Security 配置。
试着在浏览器里打开该应用程序,你马上就会看到 HTTP 基础身份验证对话框。此处的用户名是 user,密码就有点麻烦了。密码是在应用程序每次运行时随机生成后写入日志的,你需要查找日志消息(默认写入标准输出),找到此类内容:
Using default security password: d9d8abe5-42b5-4f20-a32a-76ee3df658d9
我不能肯定,但我猜这个特定的安全配置并不是你的理想选择。首先,HTTP 基础身份验证对话框有点粗糙,对用户并不友好。而且,我敢打赌你一般不会开发这种只有一个用户的应用程序,而且他还要从日志文件里找到自己的密码。因此,你会希望修改 Spring Security 的一些配置,至少要有一个好看一些的登录页,还要有一个基于数据库或 LDAP(Lightweight Directory Access Protocol)用户存储的身份验证服务。
让我们看看如何写出 Spring Secuirty 配置,覆盖自动配置的安全设置吧。
创建自定义的安全配置
覆盖自动配置很简单,就当自动配置不存在,直接显式地写一段配置。这段显式配置的形式不限,Spring 支持的 XML 和 Groovy 形式配置都可以。
在编写显式配置时,我们会专注于 Java 形式的配置。在 Spring Security 的场景下,这意味着写一个扩展了 WebSecurityConfigurerAdapter 的配置类。代码清单3-1中的 SecurityConfig 就是我们需要的东西。
package readinglist;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ReaderRepository readerRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").access("hasRole('READER')") // 要求登录者有READER角色
.antMatchers("/**").permitAll()
.and()
.formLogin()
.loginPage("/login") // 设置登录表单的路径
.failureUrl("/login?error=true");
}
@Override
protected void configure(
AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException { // 定义自定义UserDetailService
UserDetails userDetails = readerRepository.findOne(username);
if (userDetails != null) {
return userDetails;
}
throw new UsernameNotFoundException("User '" + username + "' not found.");
}
});
}
}
SecurityConfig 是个非常基础的 Spring Security 配置,尽管如此,它还是完成了不少安全定制工作。通过这个自定义的安全配置类,我们让 Spring Boot 跳过了安全自动配置,转而使用我们的安全配置。
扩展了 WebSecurityConfigurerAdapter 的配置类可以覆盖两个不同的 configure() 方法。在 SecurityConfig 里,第一个 configure() 方法指明,“/”(ReadingListController 的方法映射到了该路径)的请求只有经过身份认证且拥有 READER 角色的用户才能访问。其他的所有请求路径向所有用户开放了访问权限。这里还将登录页和登录失败页(带有一个 error 属性)指定到了 /login。
Spring Security 为身份认证提供了众多选项,后端可以是 JDBC(Java Database Connectivity)、LDAP 和内存用户存储。在这个应用程序中,我们会通过 JPA 用数据库来存储用户信息。第二个 configure() 方法设置了一个自定义的 UserDetailsService,这个服务可以是任意实现了 UserDetailsService 的类,用于查找指定用户名的用户。代码清单3-2提供了一个匿名内部类实现,简单地调用了注入 ReaderRepository(这是一个 Spring Data JPA 仓库接口)的 findOne() 方法。
package readinglist;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ReadingListRepository extends JpaRepository<Book, Long> {
List<Book> findByReader(Reader reader);
}
和 BookRepository 类似,你无需自己实现 ReaderRepository。这是因为它扩展了 JpaRepository, Spring Data JPA 会在运行时自动创建它的实现。这为你提供了 18 个操作 Reader 实体的方法。
说到 Reader 实体,Reader 类(如代码清单3-3所示)就是最后一块拼图了,它就是一个简单的 JPA 实体,其中有几个字段用来存储用户名、密码和用户全名。
package readinglist;
import java.util.Arrays;
import java.util.Collection;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
@Entity
public class Reader implements UserDetails {
private static final long serialVersionUID = 1L;
// Reader字段
@Id
private String username;
private String fullname;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getFullname() {
return fullname;
}
public void setFullname(String fullname) {
this.fullname = fullname;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { // 授予READER权限
return Arrays.asList(new SimpleGrantedAuthority("ROLE_READER"));
}
// 不过期,不加锁,不禁用
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
如你所见,Reader 用了 @Entity 注解,所以这是一个 JPA 实体。此外,它的 username 字段上有 @Id 注解,表明这是实体的 ID。这个选择无可厚非,因为 username 应该能唯一标识一个 Reader。
你应该还注意到 Reader 实现了 UserDetails 接口以及其中的方法,这样 Reader 就能代表 Spring Security 里的用户了。getAuthorities() 方法被覆盖过了,始终会为用户授予 READER 权限。isAccountNonExpired()、isAccountNonLocked()、isCredentialsNonExpired() 和 isEnabled() 方法都返回 true,这样读者账户就不会过期,不会被锁定,也不会被撤销。
重新构建并重启应用程序后,你应该就能以读者身份登录应用程序了。
保持简单在一个大型应用程序里,赋予用户的授权本身也可能是实体,它们被维护在独立的数据表里。同样,表示一个账户是否为非过期、非锁定且可用的布尔值也是数据库里的字段。但是,出于演示考虑,我决定让这些细节保持简单,以免分散我们的注意力,影响正在讨论的话题——我说的是覆盖 Spring Boot 自动配置。
在安全配置方面,我们还能做更多事情,但此刻这样就足够了,上面的例子足以演示如何覆盖 Spring Boot 提供的安全自动配置。
再重申一次,想要覆盖 Spring Boot 的自动配置,你所要做的仅仅是编写一个显式的配置。Spring Boot 会发现你的配置,随后降低自动配置的优先级,以你的配置为准。想弄明白这是如何实现的,让我们揭开 Spring Boot 自动配置的神秘面纱,看看它是如何运作的,以及它是怎么允许自己被覆盖的。
掀开自动配置的神秘面纱
正如我们在 2.3.3 节里讨论的那样,Spring Boot 自动配置自带了很多配置类,每一个都能运用在你的应用程序里。它们都使用了 Spring 4.0 的条件化配置,可以在运行时判断这个配置是该被运用,还是该被忽略。
大部分情况下,表2-1里的 @ConditionalOnMissingBean 注解是覆盖自动配置的关键。Spring Boot 的 DataSourceAutoConfiguration 中定义的 JdbcTemplate Bean 就是一个非常简单的例子,演示了 @ConditionalOnMissingBean 如何工作:
@Bean
@ConditionalOnMissingBean(JdbcOperations.class)
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(this.dataSource);
}
jdbcTemplate() 方法上添加了 @Bean 注解,在需要时可以配置出一个 JdbcTemplate Bean。但它上面还加了 @ConditionalOnMissingBean 注解,要求当前不存在 JdbcOperations 类型(JdbcTemplate 实现了该接口)的Bean 时才生效。如果当前已经有一个 JdbcOperations Bean 了,条件即不满足,不会执行 jdbcTemplate() 方法。
什么情况下会存在一个 JdbcOperations Bean 呢?Spring Boot 的设计是加载应用级配置,随后再考虑自动配置类。因此,如果你已经配置了一个 JdbcTemplate Bean,那么在执行自动配置时就已经存在一个 JdbcOperations 类型的 Bean 了,于是忽略自动配置的 JdbcTemplate Bean。
关于 Spring Security,自动配置会考虑几个配置类。在这里讨论每个配置类的细节是不切实际的,但覆盖 Spring Boot 自动配置的安全配置时,最重要的一个类是 SpringBootWebSecurity-Configuration。以下是其中的一个代码片段:
@Configuration
@EnableConfigurationProperties
@ConditionalOnClass({ EnableWebSecurity.class })
@ConditionalOnMissingBean(WebSecurityConfiguration.class)
@ConditionalOnWebApplication
public class SpringBootWebSecurityConfiguration {
...
}
如你所见,SpringBootWebSecurityConfiguration 上加了好几个注解。看到 @Condi-tionalOnClass 注解后,你就应该知道 Classpath 里必须要有 @EnableWebSecurity 注解。@ConditionalOnWebApplication 说明这必须是个 Web 应用程序。@ConditionalOn-MissingBean 注解才是我们的安全配置类代替 SpringBootWebSecurityConfiguration 的关键所在。
@ConditionalOnMissingBean注解要求当下没有 WebSecurityConfiguration 类型的 Bean。虽然表面上我们并没有这么一个 Bean,但通过在 SecurityConfig 上添加 @EnableWeb-Security 注解,我们实际上间接创建了一个 WebSecurityConfiguration Bean。所以在自动配置时,这个 Bean 就已经存在了,@ConditionalOnMissingBean 条件不成立,SpringBoot-WebSecurityConfiguration 提供的配置就被跳过了。
虽然 Spring Boot 的自动配置和 @ConditionalOnMissingBean 让你能显式地覆盖那些可以自动配置的 Bean,但并不是每次都要做到这种程度。让我们来看看怎么通过设置几个简单的配置属性调整自动配置组件吧。