前言
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;
}