讀古今文學網 > Spring Boot實戰 > 3.1 覆蓋Spring Boot自動配置 >

3.1 覆蓋Spring Boot自動配置

一般來說,如果不用配置就能得到和顯式配置一樣的結果,那麼不寫配置是最直接的選擇。既然如此,那幹嘛還要多做額外的工作呢?如果不用編寫和維護額外的配置代碼也行,那何必還要它們呢?

大多數情況下,自動配置的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,那麼你要在項目的<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配置,覆蓋自動配置的安全設置吧。

3.1.2 創建自定義的安全配置

覆蓋自動配置很簡單,就當自動配置不存在,直接顯式地寫一段配置。這段顯式配置的形式不限,Spring支持的XML和Groovy形式配置都可以。

在編寫顯式配置時,我們會專注於Java形式的配置。在Spring Security的場景下,這意味著寫一個擴展了WebSecurityConfigurerAdapter的配置類。代碼清單3-1中的SecurityConfig就是我們需要的東西。

代碼清單3-1 覆蓋自動配置的顯式安全配置

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.
                                         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;

@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\')\")    ←---要求登錄者有READER角色
        .antMatchers(\"/**\").permitAll

      .and

      .formLogin
        .loginPage(\"/login\")     ←---設置登錄表單的路徑
        .failureUrl(\"/login?error=true\");
  }

@Override
protected void configure(
            AuthenticationManagerBuilder auth) throws Exception {
  auth
    .userDetailsService(new UserDetailsService {    ←---定義自定義UserDetailsService
      @Override
      public UserDetails loadUserByUsername(String username)
          throws UsernameNotFoundException {
        return readerRepository.findOne(username);
      }
    });
  }

}

 

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方法。

代碼清單3-2 用來持久化讀者信息的倉庫接口

package readinglist;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ReaderRepository
         extends JpaRepository<Reader, String> {  ←---通過JPA持久化讀者
}

  

BookRepository類似,你無需自己實現ReaderRepository。這是因為它擴展了JpaRepository,Spring Data JPA會在運行時自動創建它的實現。這為你提供了18個操作Reader實體的方法。

說到Reader實體,Reader類(如代碼清單3-3所示)就是最後一塊拼圖了,它就是一個簡單的JPA實體,其中有幾個字段用來存儲用戶名、密碼和用戶全名。

代碼清單3-3 定義Reader的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;

  @Id
  private String username; (以下三行)Reader字段
  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;
  }

  // UserDetails methods

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities {  ←---授予READER權限
    return Arrays.asList(new SimpleGrantedAuthority(\"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權限。isAccountNonExpiredisAccountNonLockedisCredentialsNonExpired isEnabled方法都返回true,這樣讀者賬戶就不會過期,不會被鎖定,也不會被撤銷。

重新構建並重啟應用程序後,你應該就能以讀者身份登錄應用程序了。

保持簡單 在一個大型應用程序裡,賦予用戶的授權本身也可能是實體,它們被維護在獨立的數據表裡。同樣,表示一個賬戶是否為非過期、非鎖定且可用的布爾值也是數據庫裡的字段。但是,出於演示考慮,我決定讓這些細節保持簡單,以免分散我們的注意力,影響正在討論的話題——我說的是覆蓋Spring Boot自動配置。

在安全配置方面,我們還能做更多事情1,但此刻這樣就足夠了,上面的例子足以演示如何覆蓋Spring Boot提供的安全自動配置。

1想要深入瞭解Spring Security,可以參考《Spring實戰(第4版)》中的第9章和第14章。

再重申一次,想要覆蓋Spring Boot的自動配置,你所要做的僅僅是編寫一個顯式的配置。Spring Boot會發現你的配置,隨後降低自動配置的優先級,以你的配置為準。想弄明白這是如何實現的,讓我們揭開Spring Boot自動配置的神秘面紗,看看它是如何運作的,以及它是怎麼允許自己被覆蓋的。

3.1.3 掀開自動配置的神秘面紗

正如我們在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自動配置的安全配置時,最重要的一個類是SpringBootWebSecurityConfiguration。以下是其中的一個代碼片段:

@Configuration
@EnableConfigurationProperties
@ConditionalOnClass({ EnableWebSecurity.class })
@ConditionalOnMissingBean(WebSecurityConfiguration.class)
@ConditionalOnWebApplication
public class SpringBootWebSecurityConfiguration {

...

}

  

如你所見,SpringBootWebSecurityConfiguration上加了好幾個註解。看到@ConditionalOnClass註解後,你就應該知道Classpath裡必須要有@EnableWebSecurity註解。@ConditionalOnWebApplication說明這必須是個Web應用程序。@ConditionalOnMissingBean註解才是我們的安全配置類代替SpringBootWebSecurityConfiguration的關鍵所在。

@ConditionalOnMissingBean註解要求當下沒有WebSecurityConfiguration類型的Bean。雖然表面上我們並沒有這麼一個Bean,但通過在SecurityConfig上添加@EnableWebSecurity註解,我們實際上間接創建了一個WebSecurityConfiguration Bean。所以在自動配置時,這個Bean就已經存在了,@ConditionalOnMissingBean條件不成立,SpringBootWebSecurityConfiguration提供的配置就被跳過了。

雖然Spring Boot的自動配置和@ConditionalOnMissingBean讓你能顯式地覆蓋那些可以自動配置的Bean,但並不是每次都要做到這種程度。讓我們來看看怎麼通過設置幾個簡單的配置屬性調整自動配置組件吧。