源碼地址:demo-world (spring-sercurity模塊)
目錄
1.簡介
前幾期我們瞭解了Spring Security Authentication (認證),今天我們來說道說道 Spring Security Authorization(授權)
這裏舉一個通俗易懂的例子:你去火車站買票乘車,你需要兩樣東西,身份證和車票,在去月臺之前,你需要刷一下身份證,確認你是本人,這就是認證,當你坐上了火車,這時候列車員回來檢查你的車票,來確定你是否可以做這一班車,這就是授權
首先我們來回顧一下 認證的結果:
也就是Authentication中的內容,我們來逐一分析一下:
- Principal:這是登錄用戶的信息,一般是指 UserDetails (它的實現類,在前幾期我們有定義)
- Credentials:通常是密碼。在許多情況下,將在驗證用戶身份後清除此內容(設置爲null),以確保它不會泄漏。
- Authorities:這裏存放的是一個 GrantedAuthority 集合,也就是授予用戶的權限, GrantedAuthority對象由AuthenticationManager插入到Authentication對象中,並在以後做出授權決策時由AccessDecisionManager讀取。這是本節一個重點信息
2.GrantedAuthority
GrantedAuthority
s are high level permissions the user is granted. A few examples are roles or scopes.
可以從Authentication.getAuthorities()方法獲得GrantedAuthoritys。此方法提供了GrantedAuthority對象的集合。GrantedAuthority是授予用戶的權限。GrantedAuthority通常由UserDetailsService加載。 此類權限通常有兩種:
- 角色:例如Admin或Common_user。稍後這些角色將作爲配置 web authorization(web授權), method authorization(方法授權) domain object authorization(領域模型授權)的依據。 Spring Security的其他部分能夠解釋這些權限。使用基於用戶名/密碼的身份驗證時。
- 範圍:它們不特定於給定的域對象。因此,您不太可能就某個用戶可以訪問編號爲18的部門信息而單獨設置一個權限,因爲如果有成千上萬個這樣的權限,您很快就會用光內存(或者至少導致應用程序花費很長時間)時間來認證用戶。當然,Spring Security是專門爲滿足這一通用要求而設計的,但您可以爲此目的使用項目的域對象安全功能。例如所有的用戶都可以訪問部門信息這個域對象,但每個人可以訪問的信息範圍不同,公司高管可以訪問全部部門信息,而部門高管只能訪問本部門的信息;
Spring Security包含一個具體的GrantedAuthority實現,即SimpleGrantedAuthority。它允許將任何用戶指定的String轉換爲GrantedAuthority。安全體系結構中包含的所有AuthenticationProvider都使用SimpleGrantedAuthority來填充Authentication對象。
3.AccessDecisionManager
AccessDecisionManager 由 AbstractSecurityInterceptor 調用,並負責做出最終的訪問控制決策。 AccessDecisionManager接口包含三種方法:
void decide(Authentication authentication, Object secureObject,
Collection<ConfigAttribute> attrs) throws AccessDeniedException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
decide方法的參數包括它進行授權決策所需的所有相關信息。secureObject是安全對象,也就是我要保護的訪問資源,例如 method authorization(方法授權)中方法就是受保護的資源,然後在AccessDecisionManager中實現某種安全性邏輯以確保安全訪問。如果訪問被拒絕,則預期實現將引發AccessDeniedException。
在啓動時,AbstractSecurityInterceptor將調用support(ConfigAttribute)方法,以確定AccessDecisionManager是否可以處理傳遞的ConfigAttribute。安全攔截器實現調用support(Class)方法,以確保配置的AccessDecisionManager支持安全攔截器將顯示的安全對象的類型。
4.角色(role)
首先,GrantedAuthority通常由UserDetailsService加載的,因此,我們使用SimpleGrantedAuthority來簡單爲用戶分配一個角色
首先我們來改造一下我們的UserDO,增加一個權限集合字段:
/**
* 權限集合
*/
Set<GrantedAuthority> authorities;
然後我們將回到UserDetails的配置中來,實現該接口的方法:
/**
* 返回授予用戶的權限
*
* @return 權限集合
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return userDO.getAuthorities();
}
我們還需要修改一下僞數據源
public class DataSource {
public static UserDO getUserByUsername(String username) {
SimpleGrantedAuthority roleIntern = new SimpleGrantedAuthority("ROLE_INTERN");
SimpleGrantedAuthority roleDba = new SimpleGrantedAuthority("ROLE_DBA");
SimpleGrantedAuthority roleAdmin = new SimpleGrantedAuthority("ROLE_ADMIN");
List<UserDO> userList = new ArrayList<>();
//初始化三個用戶
userList.add(new UserDO(1L, "swing", new BCryptPasswordEncoder().encode("123456"), 20, Arrays.asList(roleDba, roleIntern)));
userList.add(new UserDO(2L, "sky", new BCryptPasswordEncoder().encode("123456"), 20, Collections.singletonList(roleIntern)));
userList.add(new UserDO(3L, "admin", new BCryptPasswordEncoder().encode("123456"), 20, Arrays.asList(roleDba, roleIntern, roleAdmin)));
return userList.stream().distinct().filter(userDO -> userDO.getUsername().equals(username)).collect(Collectors.toList()).get(0);
}
}
驗證我們配置的結果:
OK!成功給用戶分配了角色,角色也是權限的集合,一個角色可以擁有很多權限,例如實習生可以查看文件和修改文件(兩個權限),但不可以增加文件和刪除文件,那麼Spring是如何做到這一點的呢?答案就在 AccessDecisionManager 中,顧名思義,這是訪問決策中心,一個訪問是否被允許,就是由此類決定的
Spring Security提供了攔截器,用於控制對安全對象的訪問,例如方法調用或Web請求。 AccessDecisionManager會做出關於是否允許進行調用的調用前決定。
5.FilterSecurityInterceptor
FilterSecurityInterceptor爲HttpServletRequests提供授權。它作爲安全篩選器之一插入到FilterChainProxy中,要注意不要被名字迷惑,它不是攔截器,是過濾器
-
First, the
FilterSecurityInterceptor
obtains an Authentication from the SecurityContextHolder. -
Second,
FilterSecurityInterceptor
creates aFilterInvocation
from theHttpServletRequest
,HttpServletResponse
, andFilterChain
that are passed into theFilterSecurityInterceptor
. -
Next, it passes the
FilterInvocation
toSecurityMetadataSource
to get theConfigAttribute
s. -
Finally, it passes the
Authentication
,FilterInvocation
, andConfigAttribute
s to theAccessDecisionManager
.-
If authorization is denied, an
AccessDeniedException
is thrown. In this case theExceptionTranslationFilter
handles theAccessDeniedException
. -
If access is granted,
FilterSecurityInterceptor
continues with the FilterChain which allows the application to process normally.
-
默認情況下,Spring Security 將會對所有的請求進行身份認證,如下:
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
);
}
也可以自定義認證規則,如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
//禁用csrf (跨站請求僞造)
http.csrf().disable();
http.authorizeRequests(authorize -> authorize
//允許直接訪問
.mvcMatchers("/login").permitAll()
.mvcMatchers("/file/1/**").hasRole("INTERN")
.mvcMatchers("/file/4/**").hasRole("ADMIN")
//同時具有兩種角色纔可訪問的api
.mvcMatchers("/login/page").access("hasRole('INTERN') and hasRole('DBA')")
//剩下的都拒絕
.anyRequest().denyAll()
);
//將認證設置在UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
這裏有幾個很重要的點要注意一下:
- 分配角色的時候我們使用 ROLE_角色名 的標準格式,這樣在讀取角色時候,我們只用表示爲 hasRole(角色名)即可
- anyRequest().denyAll 和 anyRequest.authenticatied 是不同的概念 前者表示對於前面沒有配置訪問策略的URL ,一律拒絕,而後者表示,對所有的url進行認證,認證通過後即可訪問。
這裏由一張由官方提供的內置表達式表格:
Expression | Description |
---|---|
|
Returns For example, By default if the supplied role does not start with 'ROLE_' it will be added. This can be customized by modifying the |
|
Returns For example, By default if the supplied role does not start with 'ROLE_' it will be added. This can be customized by modifying the |
|
Returns For example, |
|
Returns For example, |
|
Allows direct access to the principal object representing the current user |
|
Allows direct access to the current |
|
Always evaluates to |
|
Always evaluates to |
|
Returns |
|
Returns |
|
Returns |
|
Returns |
|
Returns |
|
Returns |
6.基於Handler方法的權限控制
上面我們使用了Role來控制某個用戶是否可以訪問一個接口(或handler 方法),但很明顯,在實際開發中,這樣使用會有很多侷限性,例如對用戶是否有權訪問某一個接口的判斷過程很複雜,那麼,這種直接在HttpSecurity中配置的方法,顯然就不太實用了,因此需要使用基於 Handler 方法的權限控制,Spring爲我們提供了幾個很有用的註解
要使用註解,首先在SecurityConfig中開啓它
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
6.1.@PreAuthorize
這是一個很實用的註解,它可以決定方法是否可以被調用,如下:
/**
* 獲取登錄頁面
*
* @return 登錄頁面
*/
@PreAuthorize("hasRole('INTERN') and hasRole('DBA')")
@GetMapping("/page")
String login() {
return "login";
}
這種寫法和之前我們配置的作用相同,當然此註解還有更強大的地方,例如還可以這麼寫(表示只有id =2纔可以訪問):
@PreAuthorize("hasRole('INTERN') and #id==2")
@GetMapping("/1/{id}")
@ResponseBody
public FileDO getFileNameById(@PathVariable Long id) {
return new FileDO(id, "這個殺手不太冷.mp4", 1231232423L);
}
不過,大部分時候這些功能還是不能滿足我們的需求,所以該註解還支持注入Bean,和自定義權限認證,我們來寫一個簡單的例子:
/**
* 自定義認證方法
*
* @author swing
*/
@Service("as")
public class AuthorizeService {
/**
* 是否授權
*
* @return 是否可以訪問呢
*/
public boolean hasPermission(String username) {
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return userDetails.getUsername().equals(username);
}
}
/**
* 獲取文件
*
* @param id 文件Id
* @return 文件信息
*/
@PreAuthorize("@as.hasPermission('swing')")
@GetMapping("/1/{id}")
@ResponseBody
public FileDO getFileNameById(@PathVariable Long id) {
return new FileDO(id, "這個殺手不太冷.mp4", 1231232423L);
}
以上代碼表示,只有用戶名爲swing的用戶才能訪問此接口