天天看點

@PreAuthorize,@PostAuthorize, @Secured注解+EL表達式

說明

(1)JDK版本:1.8

(2)Spring Boot 2.0.6

(3)Spring Security 5.0.9

(4)Spring Data JPA 2.0.11.RELEASE

(5)hibernate5.2.17.Final

(6)MySQLDriver 5.1.47

(7)MySQL 8.0.12

一、注解式方法級安全開啟

       需要在WebSecuirtyConfig添加配置:

@Configuration
@EnableWebSecurity //啟用Spring Security.

//會攔截注解了@PreAuthrize注解的配置.
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

}      

二、允許的注解

       這裡主要@PreAuthorize, @PostAuthorize, @Secured這三個注解可以使用。

2.1 @Secured

       當@EnableGlobalMethodSecurity(securedEnabled=true)的時候,@Secured可以使用:

@GetMapping("/helloUser")
@Secured({"ROLE_normal","ROLE_admin"})
public String helloUser() {
    return "hello,user";
}      
說明:擁有normal或者admin角色的使用者都可以方法helloUser()方法。另外需要注意的是這裡比對的字元串需要添加字首“ROLE_“。      

 如果我們要求,隻有同時擁有admin & noremal的使用者(上面是兩者擇一就行,這裡兩者都要滿足)才能使用方法helloUser()方法,這時候@Secured就無能為力了。

2.2 @PreAuthorize

       Spring的 @PreAuthorize/@PostAuthorize 注解更适合方法級的安全,也支援Spring 表達式語言,提供了基于表達式的通路控制。

       當@EnableGlobalMethodSecurity(prePostEnabled=true)的時候,@PreAuthorize可以使用:

@GetMapping("/helloUser")
@PreAuthorize("hasAnyRole('normal','admin')")
public String helloUser() {
    return "hello,user";
}      

說明:擁有normal或者admin角色的使用者都可以方法helloUser()方法。

       此時如果我們要求使用者必須同時擁有normal和admin的話,那麼可以這麼編碼:

@GetMapping("/helloUser")
@PreAuthorize("hasRole('normal') AND hasRole('admin')") 
public String helloUser() {
    return "hello,user";
}      

2.3 @PostAuthorize

       @PostAuthorize 注解使用并不多,在方法執行後再進行權限驗證,适合驗證帶有傳回值的權限,Spring EL 提供 傳回對象能夠在表達式語言中擷取傳回的對象returnObject。

當@EnableGlobalMethodSecurity(prePostEnabled=true)的時候,@PostAuthorize可以使用:

@GetMapping("/helloUser")
@PostAuthorize(" returnObject!=null &&  returnObject.username == authentication.name")
public User helloUser() {
        Object pricipal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        User user;
        if("anonymousUser".equals(pricipal)) {
            user = null;
        }else {
            user = (User) pricipal;
        }
        return user;
}      

這三個最常用也就是@PreAuthorize這個注解了,在使用中主要是配合Spring EL表達式。

三、EL表達式

什麼是SpringEL?

Spring3中引入了Spring表達式語言—SpringEL,SpEL是一種強大,簡潔的裝配Bean的方式,他可以通過運作期間執行的表達式将值裝配到我們的屬性或構造函數當中

更可以調用JDK中提供的靜态常量,擷取外部Properties檔案中的的配置      

為什麼要使用SpringEL?

平常通過配置檔案或Annotaton注入的Bean,其實都可以稱為靜态性注入,試想一下,若然我Bean A中有變量A,它的值需要根據Bean B的B變量為參考,在這場景下靜态注入就對這樣的處理顯得非常無力,

而Spring3增加的SpringEL就可以完全滿足這種需求,而且還可以對不同Bean的字段進行計算再進行指派,功能非常強大      

如何使用SpringEL?

SpringEL從名字來看就能看出,和EL是有點關系的,SpringEL的使用和EL表達式的使用非常相似,EL表達式在JSP頁面更友善的擷取背景中的值,而SpringEL就是為了更友善擷取Spring容器中的Bean的值,

EL使用${},而SpringEL使用#{}進行表達式的聲明。      

使用SpringEL注入簡單值

public class TestSpringEL {
    
    /*
     * @Value注解等同于XML配置中的<property/>标簽, 
     * SpringEL同樣支援在XML<property/>中編寫
    */
    
    // 注入簡單值,輸出num為5
    @Value("#{5}")
    
    private Integer num;
    
    // 注入ID為testConstant的Bean
    @Value("#{testConstant}")
    
    private TestConstant Constant;
    
    // 注入ID為testConstant Bean中的STR常量/變量
    @Value("#{testConstant.STR}")
    private String str;

}      

使用SpringEL調用方法

public class TestSpringEL {
    
    /*
     * TestConstant類中有兩個方法重載,
     * 傳回值為String類型
    */
    
    // 調用無參方法
    @Value("#{testConstant.showProperty}")
    private String method1;
    
    // 有參接收字元串的方法
    @Value("#{testConstant.showProperty('Hello')}")
    private String method2;
    
    /*
     * 若然希望方法傳回的String為大寫
    */
    @Value("#{testConstant.showProperty().toUpperCase()}")
    private String method3;
    
    /*
     * 若使用method3這種方式,若然showProperty傳回為null, 
     * 将會抛出NullPointerException,可以使用以下方式避免
    */
    @Value("#{testConstant.showProperty()?.toUpperCase}")
    private String method4;
    
    /*
     * 使用?.符号代表若然左邊的值為null,将不執行右邊方法, 
     * 讀者可以靈活運用在其他場景,隻要左邊可能傳回null,
     * 即可使用上面示例中的?.
    */
}      

SpringEL調用靜态類或常量

public class TestSpringEL {

    /*
     * 注入JDK中的工具類常量或調用工具類的方法
    */
    
    // 擷取Math的PI常量
    @Value("#{T(java.lang.Math).PI")
    private double pi;
    
    // 調用random方法擷取傳回值
    @Value("#{T(java.lang.Math).random()}")
    private double ramdom;
    
    // 擷取檔案路徑符号
    @Value("#{T(java.io.File).separator}")
    private String separator;
}      

SpringEL運算

public class TestSpringEL {

    /*
     * 使用SpringEL進行運算及邏輯操作
    */
    
    // 拼接字元串
    @Value("#{testConstant.nickname + ' ' + testConstant.name}")
    private String concatString;
    
    // 對數字類型進行運算,testConstant擁有num屬性
    @Value("#{ 3 * T(java.lang.Math).PI + testConstant.num}")
    private double operation;
    
    // 進行邏輯運算
    @Value("#{testConstant.num > 100 and testConstant.num <= 200}")
    private boolean logicOperation;
    
    // 進行或非邏輯操作
    @Value("#{ not testConstant.num == 100 or testConstant.num <= 200}")
    private boolean logicOperation2;
    
    // 使用三元運算符
    @Value("#{testConstant.num > 100 ? testConstant.num : testConstant.num + 100}")
    private Integer logicOperation3;
}      

SpringEL使用正規表達式

public class TestSpringEL {

    // 驗證是否郵箱位址正規表達式
    @Value("#{testConstant.STR match '\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+'}")
    private boolean regularExpression;
}      

SpringEL操作集合

public class TestSpringEL {

    /*
     * TestConstant類中擁有名為testList的List變量, 和名為testMap的Map
    */
    
    // 擷取下标為0的元素
    @Value("#{testConstant.testList[0]}")
    private String str;
    
    // 擷取下标為0元素的大寫形式
    @Value("#{testConstant.testList[0]?.toUpperCase()}")
    private String upperStr;
    
    // 擷取map中key為hello的value
    @Value("#{testConstant.testMap['hello']}")
    private String mapValue;
    
    // 根據testList下标為0元素作為key擷取testMap的value
    @Value("#{testConstant.testMap[testConstant.testList[0]]}")
    private String mapStrByTestList;
}      
<!-- 首先通過applicaContext.xml中<util:properties>增加properties檔案 -->
<!-- 注意需要引入Spring的util schemea命名空間和注意id屬性,id屬性将在SpringEL中使用 -->

<util:properties id="test" location="classpath:application.properties"/>      
public class TestSpringEL {

    // 注意test為xml檔案中聲明的id
    @Value("#{test['jdbc.url']}")
    private String propertiesValue;
}      
public class TestSpringEL {

    /*
     * 聲明City類,有population屬性 testContants擁有名叫cityList的City類List集合
    */
    
    // 過濾testConstant中cityList集合population屬性大于1000的全部資料注入到本屬性
    @Value("#{testConstant.cityList.?[population > 1000]}")
    private List<City> cityList;
    
    // 過濾testConstant中cityList集合population屬性等于1000的第一條資料注入到本屬性
    @Value("#{testConstant.cityList.^[population == 1000]}")
    private City city;
    
    // 過濾testConstant中cityList集合population屬性小于1000的最後一條資料注入到本屬性
    @Value("#{testConstant.cityList.$[population < 1000]}")
    private City city2;
    
    /*
     * 首先為city增加name屬性,代表城市的名稱
    */
    
    /*
     * 假如我們在過濾城市集合後隻想保留城市的名稱,
     * 可以使用如下方式進行投影
    */
    @Value("#{testConstant.cityList.?[population > 1000].![name]}")
    private List<String> cityName;
}