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;
}
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,923評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,041評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,475評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,253評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,801評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,023評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,530評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,494評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,639評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,890評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,289評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,552評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,242評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,626評論 2 370

推薦閱讀更多精彩內容