前言
Spring Security
支援方法級別的許可權控制。在此機制上,我們可以在任意層的任意方法上加入許可權註解,加入註解的方法將自動被Spring Security
保護起來,僅僅允許特定的使用者訪問,從而還到許可權控制的目的, 當然如果現有的許可權註解不滿足我們也可以自定義
快速開始
- 首先加入security依賴如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.接著新建安全配置類
Spring Security
預設是禁用註解的,要想開啟註解,要在繼承WebSecurityConfigurerAdapter
的類加@EnableMethodSecurity
註解,並在該類中將AuthenticationManager
定義為Bean。
@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
我們看到@EnableGlobalMethodSecurity
分別有prePostEnabled 、securedEnabled、jsr250Enabled
三個欄位,其中每個欄位程式碼一種註解支援,預設為false,true為開啟
。那麼我們就一一來說一下這三總註解支援。
prePostEnabled = true 的作用的是啟用Spring Security的@PreAuthorize 以及@PostAuthorize 註解。
securedEnabled = true 的作用是啟用Spring Security的@Secured 註解。
jsr250Enabled = true 的作用是啟用@RoleAllowed 註解
更詳細使用整合請參考我這兩篇
輕鬆上手SpringBoot+SpringSecurity+JWT實RESTfulAPI許可權控制實戰
Spring Security核心介面使用者許可權獲取,鑑權流程執行原理
在方法上設定許可權認證
JSR-250註解
遵守了JSR-250標準註解
主要註解
- @DenyAll
- @RolesAllowed
- @PermitAll
這裡面@DenyAll
和 @PermitAll
相信就不用多說了 代表拒絕和通過。
@RolesAllowed
使用示例
@RolesAllowed("ROLE_VIEWER")
public String getUsername2() {
//...
}
@RolesAllowed({ "USER", "ADMIN" })
public boolean isValidUsername2(String username) {
//...
}
代表標註的方法只要具有USER, ADMIN任意一種許可權就可以訪問。這裡可以省略字首ROLE_,實際的許可權可能是ROLE_ADMIN
在功能及使用方法上與 @Secured
完全相同
securedEnabled註解
主要註解
@Secured
Spring Security的
@Secured
註解。註解規定了訪問訪方法的角色列表,在列表中最少指定一種角色@Secured
在方法上指定安全性,要求 角色/許可權等 只有對應 角色/許可權 的使用者才可以呼叫這些方法。 如果有人試圖呼叫一個方法,但是不擁有所需的 角色/許可權,那會將會拒絕訪問將引發異常。
比如:
@Secured("ROLE_VIEWER")
public String getUsername() {
SecurityContext securityContext = SecurityContextHolder.getContext();
return securityContext.getAuthentication().getName();
}
@Secured({ "ROLE_DBA", "ROLE_ADMIN" })
public String getUsername2() {
//...
}
@Secured("ROLE_VIEWER")
表示只有擁有ROLE_VIEWER
角色的使用者,才能夠訪問getUsername()
方法。
@Secured({ "ROLE_DBA", "ROLE_ADMIN" })
表示使用者擁有"ROLE_DBA", "ROLE_ADMIN"
兩個角色中的任意一個角色,均可訪問 getUsername2
方法。
還有一點就是@Secured,不支援Spring EL表示式
prePostEnabled註解
這個開啟後支援Spring EL表示式 算是蠻厲害的。如果沒有訪問方法的許可權,會丟擲AccessDeniedException。
主要註解
@PreAuthorize
--適合進入方法之前驗證授權@PostAuthorize
--檢查授權方法之後才被執行並且可以影響執行方法的返回值
3. @PostFilter
--在方法執行之後執行,而且這裡可以呼叫方法的返回值,然後對返回值進行過濾或處理或修改並返回
@PreFilter
--在方法執行之前執行,而且這裡可以呼叫方法的引數,然後對引數值進行過濾或處理或修改
@PreAuthorize註解使用
@PreAuthorize("hasRole('ROLE_VIEWER')")
public String getUsernameInUpperCase() {
return getUsername().toUpperCase();
}
@PreAuthorize("hasRole('ROLE_VIEWER')") 相當於@Secured(“ROLE_VIEWER”)。
同樣的 @Secured({“ROLE_VIEWER”,”ROLE_EDITOR”})
也可以替換為:@PreAuthorize(“hasRole(‘ROLE_VIEWER') or hasRole(‘ROLE_EDITOR')”)
。
除此以外,我們還可以在方法的引數上使用表示式:
在方法執行之前執行,這裡可以呼叫方法的引數,也可以得到引數值,這裡利用JAVA8的引數名反射特性,如果沒有JAVA8,那麼也可以利用Spring Secuirty的@P標註引數,或利用Spring Data的@Param標註引數。
//無java8
@PreAuthorize("#userId == authentication.principal.userId or hasAuthority(‘ADMIN’)")
void changePassword(@P("userId") long userId ){}
//有java8
@PreAuthorize("#userId == authentication.principal.userId or hasAuthority(‘ADMIN’)")
void changePassword(long userId ){}
這裡表示在changePassword
方法執行之前,判斷方法引數userId的值是否等於principal中儲存的當前使用者的userId,或者當前使用者是否具有ROLE_ADMIN許可權,兩種符合其一,就可以訪問該 方法。
@PostAuthorize註解使用
在方法執行之後執行可,以獲取到方法的返回值,並且可以根據該方法來決定最終的授權結果(是允許訪問還是不允許訪問):
@PostAuthorize
("returnObject.username == authentication.principal.nickName")
public CustomUser loadUserDetail(String username) {
return userRoleRepository.loadUserByUserName(username);
}
上述程式碼中,僅當loadUserDetail
方法的返回值中的username與當前登入使用者的username相同時才被允許訪問
注意如果EL為false,那麼該方法也已經執行完了,可能會回滾。EL變數returnObject表示返回的物件。
@PreFilter以及@PostFilter註解使用
Spring Security提供了一個@PreFilter
註解來對傳入的引數進行過濾:
@PreFilter("filterObject != authentication.principal.username")
public String joinUsernames(List<String> usernames) {
return usernames.stream().collect(Collectors.joining(";"));
}
當usernames中的子項與當前登入使用者的使用者名稱不同時,則保留;當usernames中的子項與當前登入使用者的使用者名稱相同時,則移除。比如當前使用使用者的使用者名稱為zhangsan,此時usernames的值為{"zhangsan", "lisi", "wangwu"}
,則經@PreFilter過濾後,實際傳入的usernames的值為{"lisi", "wangwu"}
如果執行方法中包含有多個型別為Collection的引數,filterObject 就不太清楚是對哪個Collection引數進行過濾了。此時,便需要加入 filterTarget 屬性來指定具體的引數名稱:
@PreFilter
(value = "filterObject != authentication.principal.username",
filterTarget = "usernames")
public String joinUsernamesAndRoles(
List<String> usernames, List<String> roles) {
return usernames.stream().collect(Collectors.joining(";"))
+ ":" + roles.stream().collect(Collectors.joining(";"));
}
同樣的我們還可以使用@PostFilter
註解來過返回
的Collection進行過濾:
@PostFilter("filterObject != authentication.principal.username")
public List<String> getAllUsernamesExceptCurrent() {
return userRoleRepository.getAllUsernames();
}
此時 filterObject 代表返回值。如果按照上述程式碼則實現了:移除掉返回值中與當前登入使用者的使用者名稱相同的子項。
自定義元註解
如果我們需要在多個方法中使用相同的安全註解,則可以通過建立元註解的方式來提升專案的可維護性。
比如建立以下元註解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ROLE_VIEWER')")
public @interface IsViewer {
}
然後可以直接將該註解新增到對應的方法上:
@IsViewer
public String getUsername4() {
//...
}
在生產專案中,由於元註解分離了業務邏輯與安全框架,所以使用元註解是一個非常不錯的選擇。
類上使用安全註解
如果一個類中的所有的方法我們全部都是應用的同一個安全註解,那麼此時則應該把安全註解提升到類的級別上:
@Service
@PreAuthorize("hasRole('ROLE_ADMIN')")
public class SystemService {
public String getSystemYear(){
//...
}
public String getSystemDate(){
//...
}
}
上述程式碼實現了:訪問getSystemYear 以及getSystemDate 方法均需要ROLE_ADMIN許可權。
方法上應用多個安全註解
在一個安全註解無法滿足我們的需求時,還可以應用多個安全註解:
@PreAuthorize("#username == authentication.principal.username")
@PostAuthorize("returnObject.username == authentication.principal.nickName")
public CustomUser securedLoadUserDetail(String username) {
return userRoleRepository.loadUserByUserName(username);
}
此時Spring Security將在執行方法前執行@PreAuthorize的安全策略,在執行方法後執行@PostAuthorize的安全策略。
總結
在此結合我們的使用經驗,給出以下兩點提示:
預設情況下,在方法中使用安全註解是由Spring AOP代理實現的,這意味著:如果我們在方法1中去呼叫同類中的使用安全註解的方法2,則方法2上的安全註解將失效。
Spring Security上下文是執行緒繫結的,這意味著:安全上下文將不會傳遞給子執行緒。
public boolean isValidUsername4(String username) {
// 以下的方法將會跳過安全認證
this.getUsername();
return true;
}