授權
所謂的授權,就是用戶如果要訪問某一個資源,我們要去檢查用戶是否具備這樣的權限,如果具備就允許訪問,如果不具備,則不允許訪問。
準備測試用戶
因爲我們現在還沒有連接數據庫,所以測試用戶還是基於內存來配置。
基於內存配置測試用戶,我們有兩種方式,第一種就是我們本系列前面幾篇文章用的配置方式,如下:
@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";
}
}
這三個測試接口,我們的規劃是這樣的:
- /hello 是任何人都可以訪問的接口
- /admin/hello 是具有 admin 身份的人才能訪問的接口
- /user/hello 是具有 user 身份的人才能訪問的接口
- 所有 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 家族中使用非常廣泛,它的匹配規則也非常簡單:
通配符 | 含義 |
---|---|
** | 匹配多層路徑 |
* | 匹配一層路徑 |
? | 匹配任意單個字符 |
上面配置的含義是:
- 如果請求路徑滿足 /admin/** 格式,則用戶需要具備 admin 角色。
- 如果請求路徑滿足 /user/** 格式,則用戶需要具備 user 角色。
- 剩餘的其他格式的請求路徑,只需要認證(登錄)後就可以訪問。
注意代碼中配置的三條規則的順序非常重要,和 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 三個接口,其中:
- /hello 因爲登錄後就可以訪問,這個接口訪問成功。
- /admin/hello 需要 admin 身份,所以訪問失敗。
- /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 這個接口了。