Spring Security:授權

授權

所謂的授權,就是用戶如果要訪問某一個資源,我們要去檢查用戶是否具備這樣的權限,如果具備就允許訪問,如果不具備,則不允許訪問。

準備測試用戶

因爲我們現在還沒有連接數據庫,所以測試用戶還是基於內存來配置。

基於內存配置測試用戶,我們有兩種方式,第一種就是我們本系列前面幾篇文章用的配置方式,如下:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
            .withUser("javakf")
            .password("123")
            .roles("admin")
            .and()
            .withUser("test")
            .password("456")
            .roles("user");
}

這是一種配置方式。

由於 Spring Security 支持多種數據源,例如內存、數據庫、LDAP 等,這些不同來源的數據被共同封裝成了一個 UserDetailService 接口,任何實現了該接口的對象都可以作爲認證數據源。

因此我們還可以通過重寫 WebSecurityConfigurerAdapter 中的 userDetailsService 方法來提供一個 UserDetailService 實例進而配置多個用戶:

@Bean
protected UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("javakf").password("123").roles("admin").build());
    manager.createUser(User.withUsername("test").password("456").roles("user").build());
    return manager;
}

兩種基於內存定義用戶的方法,大家任選一個。

準備測試接口

測試用戶準備好了,接下來我們準備三個測試接口。如下:

@RestController
public class HelloController {

	@GetMapping("/hello")
	public String hello() {
		return "hello";
	}

	@GetMapping("/admin/hello")
	public String admin() {
		return "admin";
	}

	@GetMapping("/user/hello")
	public String user() {
		return "user";
	}

}

這三個測試接口,我們的規劃是這樣的:

  1. /hello 是任何人都可以訪問的接口
  2. /admin/hello 是具有 admin 身份的人才能訪問的接口
  3. /user/hello 是具有 user 身份的人才能訪問的接口
  4. 所有 user 能夠訪問的資源,admin 都能夠訪問

「注意第四條規範意味着所有具備 admin 身份的人自動具備 user 身份。」

配置

接下來我們來配置權限的攔截規則,在 Spring Security 的 configure(HttpSecurity http) 方法中,代碼如下:

http.authorizeRequests()
        .antMatchers("/admin/**").hasRole("admin")
        .antMatchers("/user/**").hasRole("user")
        .anyRequest().authenticated()
        .and()
        ...
        ...

這裏的匹配規則我們採用了 Ant 風格的路徑匹配符,Ant 風格的路徑匹配符在 Spring 家族中使用非常廣泛,它的匹配規則也非常簡單:

通配符 含義
** 匹配多層路徑
* 匹配一層路徑
? 匹配任意單個字符

上面配置的含義是:

  1. 如果請求路徑滿足 /admin/** 格式,則用戶需要具備 admin 角色。
  2. 如果請求路徑滿足 /user/** 格式,則用戶需要具備 user 角色。
  3. 剩餘的其他格式的請求路徑,只需要認證(登錄)後就可以訪問。

注意代碼中配置的三條規則的順序非常重要,和 Shiro 類似,Spring Security 在匹配的時候也是按照從上往下的順序來匹配,一旦匹配到了就不繼續匹配了,「所以攔截規則的順序不能寫錯」

另一方面,如果你強制將 anyRequest 配置在 antMatchers 前面,像下面這樣:

http.authorizeRequests()
        .anyRequest().authenticated()
        .antMatchers("/admin/**").hasRole("admin")
        .antMatchers("/user/**").hasRole("user")
        .and()

此時項目在啓動的時候,就會報錯,會提示不能在 anyRequest 之後添加 antMatchers:

Caused by: java.lang.IllegalStateException: Can't configure antMatchers after anyRequest
	at org.springframework.util.Assert.state(Assert.java:73) ~[spring-core-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry.antMatchers(AbstractRequestMatcherRegistry.java:122) ~[spring-security-config-5.3.2.RELEASE.jar:5.3.2.RELEASE]
	at cn.com.javakf.springsecurity.config.SecurityConfig.configure(SecurityConfig.java:40) ~[classes/:na]

這從語義上很好理解,anyRequest 已經包含了其他請求了,在它之後如果還配置其他請求也沒有任何意義。

從語義上理解,anyRequest 應該放在最後,表示除了前面攔截規則之外,剩下的請求要如何處理。

在攔截規則的配置類 AbstractRequestMatcherRegistry 中,我們可以看到如下一些代碼(部分源碼):

public abstract class AbstractRequestMatcherRegistry<C> {
 private boolean anyRequestConfigured = false;
 public C anyRequest() {
  Assert.state(!this.anyRequestConfigured, "Can't configure anyRequest after itself");
  this.anyRequestConfigured = true;
  return configurer;
 }
 public C antMatchers(HttpMethod method, String... antPatterns) {
  Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
  return chainRequestMatchers(RequestMatchers.antMatchers(method, antPatterns));
 }
 public C antMatchers(String... antPatterns) {
  Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
  return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns));
 }
 protected final List<MvcRequestMatcher> createMvcMatchers(HttpMethod method,
   String... mvcPatterns) {
  Assert.state(!this.anyRequestConfigured, "Can't configure mvcMatchers after anyRequest");
  return matchers;
 }
 public C regexMatchers(HttpMethod method, String... regexPatterns) {
  Assert.state(!this.anyRequestConfigured, "Can't configure regexMatchers after anyRequest");
  return chainRequestMatchers(RequestMatchers.regexMatchers(method, regexPatterns));
 }
 public C regexMatchers(String... regexPatterns) {
  Assert.state(!this.anyRequestConfigured, "Can't configure regexMatchers after anyRequest");
  return chainRequestMatchers(RequestMatchers.regexMatchers(regexPatterns));
 }
 public C requestMatchers(RequestMatcher... requestMatchers) {
  Assert.state(!this.anyRequestConfigured, "Can't configure requestMatchers after anyRequest");
  return chainRequestMatchers(Arrays.asList(requestMatchers));
 }
}

從這段源碼中,我們可以看到,在任何攔截規則之前(包括 anyRequest 自身),都會先判斷 anyRequest 是否已經配置,如果已經配置,則會拋出異常,系統啓動失敗。

這樣大家就理解了爲什麼 anyRequest 一定要放在最後。

測試

接下來,我們啓動項目進行測試。

項目啓動成功後,我們首先以 test 的身份進行登錄:
在這裏插入圖片描述
登錄成功後,分別訪問 /hello,/admin/hello 以及 /user/hello 三個接口,其中:

  1. /hello 因爲登錄後就可以訪問,這個接口訪問成功。
  2. /admin/hello 需要 admin 身份,所以訪問失敗。
  3. /user/hello 需要 user 身份,所以訪問成功。

按照相同的方式,大家也可以測試 javakf 用戶。

角色繼承

在前面提到過一點,所有 user 能夠訪問的資源,admin 都能夠訪問,很明顯我們目前的代碼還不具備這樣的功能。

要實現所有 user 能夠訪問的資源,admin 都能夠訪問,這涉及到另外一個知識點,叫做角色繼承。

這在實際開發中非常有用。

上級可能具備下級的所有權限,如果使用角色繼承,這個功能就很好實現,我們只需要在 SecurityConfig 中添加如下代碼來配置角色繼承關係即可:

@Bean
RoleHierarchy roleHierarchy() {
    RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
    hierarchy.setHierarchy("ROLE_admin > ROLE_user");
    return hierarchy;
}

注意,在配置時,需要給角色手動加上 ROLE_ 前綴。上面的配置表示 ROLE_admin 自動具備 ROLE_user 的權限。

配置完成後,重啓項目,此時我們發現 javakf也能訪問 /user/hello 這個接口了。

代碼託管:springsecurity_example_4

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章