本章内容
覆盖自动配置的Bean
用外置属性进行配置
自定义错误页
一般来说,如果不用配置就能得到和显式配置一样的结果,那么不写配置是直接的选择。
既然如此,那干嘛还要多做额外的工作呢?如果不用编写和维护额外的配置代码也行,那何必还要它们呢? 大多数情况下,自动配置的Bean刚好能满足你的需要,不需要去覆盖它们。但某些情况下, Spring Boot在自动配置时还不能很好地进行推断。这里有个不错的例子:当你在应用程序里添加安全特性时,自动配置做得还不够好。
安全配置并不是放之四海而皆准的,围绕应用程序安全有很多决策要做,Spring Boot不能替你做决定。
虽然Spring Boot为安全提供了一些基本的自动配置,但是你还是需要自己覆盖一些配置以满足特 定的安全要求。
想知道如何用显式的配置来覆盖自动配置,我们先从为阅读列表应用程序添加Spring Security入手。
在了解自动配置提供了什么之后,我们再来覆盖基础的安全配置,以满足特定的 场景需求
3.1.1 保护应用程序
Spring Boot自动配置让应用程序的安全工作变得易如反掌,
你要做的只是添加Security起步 依赖。以Gradle为例,应添加如下依赖:
compile(“org.springframework.boot:spring-boot-starter-security”)
如果使用Maven,那么你要在项目的块中加入如下:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
这样就搞定了!重新构建应用程序后运行即可,现在这就是一个安全的Web应用程序了!
Security起步依赖在应用程序的Classpath里添加了Spring Secuirty(和其他一些东西)。
Classpath里 有Spring Security后,自动配置就能介入其中创建一个基本的Spring Security配置。
试着在浏览器里打开该应用程序,你马上就会看到HTTP基础身份验证对话框。
此处的用户 名是user,密码就有点麻烦了。密码是在应用程序每次运行时随机生成后写入日志的,
你需要查 找日志消息(默认写入标准输出),找到此类内容:
Using default security password: d9d8abe5-42b5-4f20-a32a-76ee3df658d9
package com.example.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.EnableWebSecurity;
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;
import org.springframework.security.provisioning.UserDetailsManager;
/**
* 覆盖自动配置很简单,就当自动配置不存在,直接显式地写一段配置。
* 这段显式配置的形式 不限,Spring支持的XML和Groovy形式配置都可以。
* 在编写显式配置时,我们会专注于Java形式的配置。在Spring Security的场景下,
* 这意味着写 一个扩展了WebSecurityConfigurerAdapter的配置类
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ReaderRepository readerRepository;
@Override
protected void configure(HttpSecurity http)throws Exception{
http
.authorizeRequests()
.antMatchers("/").access("hasRole('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 {
//eturn readerRepository.getOne(username);
return readerRepository.getOne(username);
}
});
}
}
package com.example.readinglist;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
/**
* 接下来,我们就要定义用于把Book对象持久化到数据库的仓库了。
* ①因为用了Spring Data JPA, 所以我们要做的就是简单地定义一个接口,
* 扩展一下Spring Data JPA的JpaRepository接口:
*/
/**
* 通过扩展JpaRepository,ReadingListRepository直接继承了18个执行常用持久化操作 的方法。
* JpaRepository是个泛型接口,有两个参数:仓库操作的领域对象类型,及其ID属性的 类型
*/
public interface ReadingListRepository extends JpaRepository<Book,Long> {
/**
* 可以根据读者的用户名来查找阅读列表。
* @param reader
* @return List
*/
List<Book> findByReader(String reader);
}
package com.example.readinglist;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.List;
//这样组件扫描会自动将ReadingListController注册为 Spring应用程序上下文里的一个Bean
@Controller
//将其中所有的处理器 方法都映射到了“/”这个URL路径上。
@RequestMapping("/")
public class ReadingListController {
private ReadingListRepository readingListRepository;
public ReadingListController(ReadingListRepository readingListRepository){
this.readingListRepository = readingListRepository;
}
/**
* 处理/{reader}上的HTTP GET请求,根据路径里指定的读者,从(通 过控制器的构造器注入的)仓库获取Book列表。
* 随后将这个列表塞入模型,用的键是 books,后返回readingList作为呈现模型的视图逻辑名称。
* @param reader
* @param model
* @return String
*/
@RequestMapping(value="/{reader}",method = RequestMethod.GET)
public String readersBooks(
@PathVariable("reader") String reader, Model model){
List<Book> readingList = readingListRepository.findByReader(reader);
if(readingList != null){
model.addAttribute("books",readingList);
}
return "readingList";
}
/**
* addToReadingList():处理/{reader}上的HTTP POST请求,将请求正文里的数据绑定 到一个Book对象上。
* 该方法把Book对象的reader属性设置为读者的姓名,
* 随后通过仓 库的save()方法保存修改后的Book对象,
* 后重定向到/{reader}(控制器中的另一个方 法会处理该请求)。
* @param reader
* @param book
* @return
*/
@RequestMapping(value = "/{reader}",method = RequestMethod.POST)
public String addToReadingList(
@PathVariable("reader") String reader,Book book){
book.setReader(reader);
readingListRepository.save(book);
return "redirect:/{reader}";
}
}
package com.example.readinglist;
import org.springframework.data.jpa.repository.JpaRepository;
//通过JPA持久化读者 Java Persistence API
public interface ReaderRepository extends JpaRepository<Reader,String> {
}
package com.example.readinglist;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.Entity;
import javax.persistence.Id;
import java.util.Arrays;
import java.util.Collection;
//Reader用了@Entity注解,所以这是一个JPA实体
@Entity
//你应该还注意到Reader实现了UserDetails接口以及其中的方法,这样Reader就能代表 Spring Security里的用户了
public class Reader implements UserDetails {
private static final long serialVersionUID = 1L;
//此外,它的username字段 上有@Id注解,表明这是实体的ID
@Id
private String username;
private String fullname;
private String password;
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public String getUsername() {
return username;
}
public String getFullname() {
return fullname;
}
public void setFullname(String fullname) {
this.fullname = fullname;
}
public void setPassword(String password) {
this.password = password;
}
/**getAuthorities()方法被覆盖过了,始终会为用户授予READER 权限
* 授予Reader权限
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.asList(new SimpleGrantedAuthority("READER"));
}
//不过期,不加锁,不禁用
//isAccountNonExpired()、 isAccountNonLocked()、isCredentialsNonExpired()
// 和isEnabled()方法都返回true,这样读者账户就不会过期,不会被锁定,也不会被撤销。
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
/*
在一个大型应用程序里,赋予用户的授权本身也可能是实体,它们被 维护在独立的数据表里。
同样,表示一个账户是否为非过期、非锁定且可用的布尔值也 是数据库里的字段。
但是,出于演示考虑,我决定让这些细节保持简单,
以免分散我们 的注意力,影响正在讨论的话题——我说的是覆盖Spring Boot自动配置。
*/
/*
再重申一次,想要覆盖Spring Boot的自动配置,你所要做的仅仅是编写一个显式的配置。
Spring Boot会发现你的配置,随后降低自动配置的优先级,以你的配置为准。
想弄明白这是如何 实现的,让我们揭开Spring Boot自动配置的神秘面纱,
看看它是如何运作的,以及它是怎么允许 自己被覆盖的。
*/
package com.example.readinglist;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.List;
//这样组件扫描会自动将ReadingListController注册为 Spring应用程序上下文里的一个Bean
@Controller
//将其中所有的处理器 方法都映射到了“/”这个URL路径上。
@RequestMapping("/")
public class ReadingListController {
private ReadingListRepository readingListRepository;
public ReadingListController(ReadingListRepository readingListRepository){
this.readingListRepository = readingListRepository;
}
/**
* 处理/{reader}上的HTTP GET请求,根据路径里指定的读者,从(通 过控制器的构造器注入的)仓库获取Book列表。
* 随后将这个列表塞入模型,用的键是 books,后返回readingList作为呈现模型的视图逻辑名称。
* @param reader
* @param model
* @return String
*/
@RequestMapping(value="/{reader}",method = RequestMethod.GET)
public String readersBooks(
@PathVariable("reader") String reader, Model model){
List<Book> readingList = readingListRepository.findByReader(reader);
if(readingList != null){
model.addAttribute("books",readingList);
}
return "readingList";
}
/**
* addToReadingList():处理/{reader}上的HTTP POST请求,将请求正文里的数据绑定 到一个Book对象上。
* 该方法把Book对象的reader属性设置为读者的姓名,
* 随后通过仓 库的save()方法保存修改后的Book对象,
* 后重定向到/{reader}(控制器中的另一个方 法会处理该请求)。
* @param reader
* @param book
* @return
*/
@RequestMapping(value = "/{reader}",method = RequestMethod.POST)
public String addToReadingList(
@PathVariable("reader") String reader,Book book){
book.setReader(reader);
readingListRepository.save(book);
return "redirect:/{reader}";
}
}
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,但并不是每次都要做到这种程度。
让我们来看看怎么通过设置几个简单的配置 属性调整自动配置组件吧。
待续…