前言

Spring Security支援方法級別的許可權控制。在此機制上,我們可以在任意層的任意方法上加入許可權註解,加入註解的方法將自動被Spring Security保護起來,僅僅允許特定的使用者訪問,從而還到許可權控制的目的, 當然如果現有的許可權註解不滿足我們也可以自定義

快速開始

  1. 首先加入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標準註解

主要註解

  1. @DenyAll
  2. @RolesAllowed
  3. @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

  1. Spring Security的@Secured註解。註解規定了訪問訪方法的角色列表,在列表中最少指定一種角色

  2. @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。

主要註解

  1. @PreAuthorize --適合進入方法之前驗證授權

  2. @PostAuthorize --檢查授權方法之後才被執行並且可以影響執行方法的返回值

3. @PostFilter --在方法執行之後執行,而且這裡可以呼叫方法的返回值,然後對返回值進行過濾或處理或修改並返回

  1. @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的安全策略。

總結

在此結合我們的使用經驗,給出以下兩點提示:

  1. 預設情況下,在方法中使用安全註解是由Spring AOP代理實現的,這意味著:如果我們在方法1中去呼叫同類中的使用安全註解的方法2,則方法2上的安全註解將失效。

  2. Spring Security上下文是執行緒繫結的,這意味著:安全上下文將不會傳遞給子執行緒。

public boolean isValidUsername4(String username) {
// 以下的方法將會跳過安全認證
this.getUsername();
return true;
}