Spring Security教程 Vol 7. 訪問規則ConfigAttribute

廢柴大叔阿基拉發表於2019-04-17

前言

從這章開始的三個章節主要介紹訪問控制最重要的三塊元件:

  1. 訪問規則ConfigAttribute
  2. 訪問決策AccessDecisionVoter
  3. 訪問控制整體排程AccessDecisionManager 最基礎的背景知識的鋪墊已經在上一個章節說明,如閱讀中又不理解相關功能模組職責的話可以再翻到上一章節複習下。

第七期 訪問規則ConfigAttribute

  1. ConfigAttribute的常用元件
  2. WebExpressionConfigAttribute
  3. SecurityConfig
  4. PostInvocationExpressionAttribute

一、ConfigAttribute的常用元件

ConfigAttribute作為訪問控制模板用於“描述”規則的元件,最常見的方式主要是兩種一種是基於註解的一種是基於JavaConfig在Web配置中進行配置的。 其中Spring Security對應方法級的註解主要又可以分為兩類: 第一類 @Secured 註解 - secured-annotations 第二類 @PreAuthorize, @PreFilter, @PostAuthorize and @PostFilter - pre-post-annotations

ConfigAttribute的常用元件

1.1 WebExpressionConfigAttribute

WebExpressionConfigAttribute是我們最早也是最常見的訪問控制方式。比如下面的程式碼形式

 http.authorizeRequests()
        .antMatchers("/").access("hasAnyRole('ANONYMOUS', 'USER')")
        .antMatchers("/login/*").access("hasAnyRole('ANONYMOUS', 'USER')")
        .antMatchers("/logout/*").access("hasAnyRole('ANONYMOUS', 'USER')")
        .antMatchers("/admin/*").access("hasRole('ADMIN')")
        .antMatchers("/events/").access("hasRole('ADMIN')")
        .antMatchers("/**").access("hasRole('USER')")
複製程式碼

基於Web表達是可以對目標URL的模式進行訪問控制,而控制檢查的規則最常見兩種方式一種是基於角色(Role-Based),另一種是基於表示式(Expressions-Based)。 演示程式碼中使用的是access的表示式進行控制,同樣的也可以直接使用下面的形式通過角色來達到同樣的效果。

        .antMatchers("/logout/*").hasAnyRole("USER","ANONYMOUS")
複製程式碼

可以說基於Web表示式對於Web資源的控制是我們最長見的方式。 除此以外,access還有另外一個很酷炫的功能就是通過表示式將判斷的方法委託表示式內的方法進行判斷。如果最終表示式返回false則會返回403禁止訪問,true則表示授權訪問。 舉個例子,假設我們系統內針對/user路徑需要對使用者名稱進行判斷,如果使用者名稱不是‘user’則不可以訪問。我們把對應判斷的程式碼寫在一個CustomWebSecurity的Bean中。

@Component()
public class CustomWebSecurity {
        public boolean check(Authentication authentication) {
            return  (authentication.getName().equals("user"));
        }
}
複製程式碼

然後我們通過修改Web表示式我訪問控制向/user的路徑增加這樣一個訪問控制的表示式。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/user").access("@customWebSecurity.check(authentication)")
                .and()
                .formLogin();
    }
複製程式碼

最後,我們為了達到測試的目的對應的新增兩個使用者

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser(User.withUsername("user").password("{noop}password").roles("USER"));
        auth.inMemoryAuthentication().withUser(User.withUsername("test").password("{noop}password").roles("USER"));
    }
複製程式碼

整個測試的Security配置類如下:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig  extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/user").access("@customWebSecurity.check(authentication)")
                .and()
                .formLogin();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser(User.withUsername("user").password("{noop}password").roles("USER"));
        auth.inMemoryAuthentication().withUser(User.withUsername("test").password("{noop}password").roles("USER"));
    }
}
複製程式碼

然後,我們分別登入使用者進行測試,首先我們登入user的賬戶。

登入介面
輸入user和password之後,我們可以正確的訪問/user的頁面。
正確的/user頁面
但當我們使用test和password以test使用者身份登入時,因為無法通過access中的check方法對使用者名稱的檢查,所以我們便會得到一個403禁止訪問的錯誤。

image.png

1.2 SecurityConfig

說完了,最常用的WebExpressionConfigAttribute瞭解了基於角色與表示式對目標地址進行訪問控制的配置之後,接下來我們分別對兩個基於方法級進行控制的配置展開介紹。 首先我們需要在相關的JavaConfig中啟用相關的方法級的驗證。

@EnableGlobalMethodSecurity(securedEnabled = true) // <--開啟Secured註解配置
public class MethodSecurityConfig {
// ...
}
複製程式碼

之後我們便可以在我們需要進行許可權驗證的地方增加相關的註解和規則:

public interface BankService {

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}
複製程式碼

@Secured("ROLE_TELLER")中的表示式便是驗證規則,所有的AccessDecisionVoter都會一次檢查是否對該表示式支援,比如這個例子裡,RoleVoter便會對該規則進行表決:如果當前訪問的使用者擁有TELLER的角色,那麼就可以繼續執行該方法;如果不沒有對應的角色,那麼就會返回一個403的錯誤。 同理@Secured("IS_AUTHENTICATED_ANONYMOUSLY")對應的便是AuthenticatedVoter。 利用這種特性,我們也可以通過自定義相關表示式與客製化對應的AccessDecisionVoter來完成特有的訪問控制邏輯。

1.3 PostInvocationExpressionAttribute

最後則是利用切面進行訪問控制邏輯的@Pre與@Post註解。同樣的如果需要使用這種方法級的配置,也需要啟用對應的配置。

@EnableGlobalMethodSecurity(prePostEnabled = true) // <--開啟Pre與Post註解配置
public class MethodSecurityConfig {
// ...
}
複製程式碼

與@Secured註解不同,如果啟用了改配置,則可以使用以下四個註解對Java方法級進行訪問控制和處理:

  • @PreAuthorize
  • @PreFilter
  • @PostAuthorize
  • @PostFilter

@PreAuthorize

其中比較常用的可能是@PreAuthorize註解,@PreAuthrize在功能上與@Secured基本是一致的,便是在執行被註解的方法事前先對規則進行投票,如果通過之後則授權進行訪問。相比@Secured而言@PreAuthrize並不是依靠不同的AccessDecisionVoter來完成,而是依靠其編寫的各種表示式的值進行判斷。 比如我們對方法入參的使用者名稱進行判斷,則可使用

@PreAuthorize("#n == authentication.name")
Contact findContactByName(@Param("n") String name);
複製程式碼

或者

@PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);
複製程式碼

@PreAuthorize的表示式是非常強大工具,畢竟注入Authentication物件的方法在寫測試用例的時候就非常的痛苦了……

public void doSomething(Contact contact,@Autowired Authentication authentication){
    if (contact.name == authentication.name){   
    ///
   }
};
複製程式碼

相對先驗的@PreAuthorize來說後驗的@PostAuthorize註解的使用場景就基本很罕見了。是一個幾乎可以忽略的註解。

@PreFilter & @PostFilter

接下來說兩個其他教程中都不太提到的Spring Security中的兩個Filter註解。這兩個註解的本質是通過在方法級編寫了一個Spring-EL表示式對方法使用和返回的集合進行過濾。 比如我們最常見的場景便是不同使用者可能生成的選單可能不同,我們可能會給每一個選單都賦予一個Permission許可權。但是如果在這裡展開怕是3000個字也說不清楚,有機會單獨在ACL部分做一個實戰型的說明。 這邊舉個簡單的例子,假設我們現在對使用者使用的一個集合進行過濾,如果規則是隻可訪問奇數ID的物件。換言之,過濾的規則則是“當ID為偶數”。

   @PreFilter(filterTarget="ids", value="filterObject%2==0")
 
   public void doSomething(List<Integer> ids) {
 
   }
複製程式碼

如果是對方法返回值的集合進行過濾,則需要使用@PostFilter

   @PostFilter("filterObject.id%2==0")
  public List<User> findAll() {

      List<User> userList = new ArrayList<User>();

      User user;

      for (int i=0; i<10; i++) {

         user = new User();

         user.setId(i);

         userList.add(user);

      }

      return userList;
   }
複製程式碼

在SpringSecurity中也內建了一部分表示式規則,如基於Permission對相應物件做許可權驗證過濾:

@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
public List<Contact> getAll();
複製程式碼

而這一部分便設計了PermissionEvaluator許可權評估器還完成相應進行目標領域物件操作所需要的許可權邏輯。而這一部分則是在ACL客製化的重點。

PermissionEvaluator介面

結尾

這一期稍微詳細的帶大家走馬觀花般的瞭解了下Spring Security提供訪問控制各種配置方法和其使用場景。 因為針對這一部分如果過度展開脫離實戰場景也非常難掌握,所以這一期的真的就只是讓大家瞭解Spring Security針對不同的訪問控制顆粒細度應該怎麼配置,比如URL級、程式碼方法級、領域集合級別的許可權過濾或者是客製化對應的控制邏輯。 下一期我們將先對訪問控制另外一個核心,即如何針對配置進行處理的AccessDecisionVoter介面元件進行說明。 我們下期再見。

相關文章