SpringBoot之SpringSecurity權限注解在方法上進行權限認證多種方式


前言

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM