訪問控制表達式
1. 前言
在實際的開發(fā)過程中,權(quán)限的分配規(guī)則往往非常復(fù)雜,如果才能實現(xiàn)快速實現(xiàn)細粒度的權(quán)限策略呢?Spring Security 已為我們找到了解決辦法。
自從 Spring Security 3.0 支持了使用 Spring 語法表達式來配置安全規(guī)則,便大大降低了安全規(guī)則實現(xiàn)的復(fù)雜度。
本節(jié),我們主要討論如何通過 Spring Security 訪問控制表達式實現(xiàn)安全規(guī)則的配置。
2.Spring 內(nèi)置表達式
Spring Security 使用 Spring EL (Spring 表達式語法)用來支持表達式配置。表達式是作為運算上下文中的根級對象被執(zhí)行的。
SecurityExpressionRoot
是支持表達式的基礎(chǔ)實現(xiàn)類,它提供了一些支持 Web 或者方法層面的安全表達式。
以下為其支持的表達式:
hasRole(String role)
如果當(dāng)前的用戶身份信息中,包含 role 值的角色時,該表達式返回 true。
例如判斷是否具有 admin 角色:hasRole('admin')
。
需要注意的是,角色名稱在 Spring Security 內(nèi)會自動增加 ROLE_
前綴,如果需要修改該前綴,可通過 DefaultWebSecurityExpressionHandler
對象中的 defaultRolePrefix
屬性實現(xiàn)。
hasAnyRole(String… roles)
和 hasRole
類似,可以同時判斷多個角色,只要包含其中一種即可,多個角色用逗號隔開。
hasAuthority(String authority)
如果當(dāng)前的身份信息中包含參數(shù)中指定權(quán)限,則返回 true
。
例如:hasAuthority('read')
hasAnyAuthority(String… authorities)
如果當(dāng)前的身份信息中包含參數(shù)中指定權(quán)限之一,則返回 true
。多個權(quán)限之間用逗號 ,
分隔。
例如:hasAnyAuthority('read', 'write')
principal
允許當(dāng)前登錄用戶直接訪問其身份信息 principal
對象。
authentication
允許直接訪問當(dāng)前安全上下文中的認(rèn)證信息 Authentication
對象。
permitAll
永遠返回 true
。
denyAll
永遠返回 false
。
isAnonymous()
如果當(dāng)前用戶的身份信息為匿名用戶,則返回 true
。
isRememberMe()
如果當(dāng)前用戶的身份信息是來自于「記住我」認(rèn)證用戶,則返回 true
。
isAuthenticated()
如果當(dāng)前用戶的身份信息不是匿名用戶,則返回 true
。
isFullyAuthenticated()
如果當(dāng)前用戶的身份信息既不是匿名用戶又不是記住我自動登錄用戶,則返回 true
。
hasPermission(Object target, Object permission)
如果當(dāng)前用戶包含對指定對象的訪問權(quán)限,則返回 true
。
例如:hasPermission(domainObject, 'read')
。
hasPermission(Object targetId, String targetType, Object permission)
如果當(dāng)前用戶包含對指定對象的訪問權(quán)限,則返回 true
。
例如:hasPermission(1, 'com.example.domain.Message', 'read')
。
3. 內(nèi)置表達式在 Web 系統(tǒng)中的使用
3.1 表達式的應(yīng)用配置
要針對 URL 應(yīng)用表達式規(guī)則,我們需要在 <http>
對象上將 use-expressions
屬性值置為 true
。Spring Security 則會將 <intercept-url>
元素中 access
屬性值解釋為表達式,并根據(jù)表達式規(guī)則返回 true
或者 false
的判定結(jié)果。
例如:
<http>
<intercept-url pattern="/admin*"
access="hasRole('admin') and hasIpAddress('192.168.1.0/24')"/>
...
</http>
本例中,我們將 /admin
開頭的資源定義為只有包含 admin
角色,并且來源 IP 為 192.168.1.x 網(wǎng)段的用戶才可以訪問。這里出現(xiàn)的 hasIpAddress
方法在前面內(nèi)容中沒有提到,因為它是只有 Web 系統(tǒng)才包含的,被定義在 WebSecurityExpressionRoot
類中。
3.2 使用自定義方法作為表達式
如果我們需要擴展現(xiàn)有的表達式,我們可以使用 Spring Bean 里的方法。
例如,使用自定義 Bean WebSecurity
中的 check
方法:
public class WebSecurity {
public boolean check(Authentication authentication, HttpServletRequest request) {
...
}
}
我們可以在配置文件中這樣寫:
<http>
<intercept-url pattern="/user/**"
access="@webSecurity.check(authentication,request)"/>
...
</http>
或者在 Java 中直接加入配置:
http
.authorizeRequests(authorize -> authorize
.antMatchers("/user/**").access("@webSecurity.check(authentication,request)")
...
)
3.3 路徑參數(shù)的使用
很多時候我們的路徑是包含參數(shù)信息的,而規(guī)則的應(yīng)用往往也和參數(shù)信息想匹配。例如針對用戶信息的 RESTful 資源地址 /user/{userId}
。我們可以在表達式中使用這些參數(shù)。
例如,假設(shè)我們的擴展表達式方法如下:
public class WebSecurity {
public boolean checkUserId(Authentication authentication, int id) {
...
}
}
此方法的 id 參數(shù)需要傳入路徑中的 userId
的值,那我們在配置表達式時可用如下方式:
<http>
<intercept-url pattern="/user/{userId}/**"
access="@webSecurity.checkUserId(authentication,#userId)"/>
...
</http>
或者用 Java 的方式配置
http
.authorizeRequests(authorize -> authorize
.antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
...
);
上述寫法的結(jié)果都是將 userId
作為參數(shù)傳入了方法之中,加入用戶訪問的 URL 地址為 /user/123/resource
,則傳入的 id 參數(shù)為 123
。
4. 方法安全表達式
方法安全表達式通常通過注解方式控制方法的訪問。這里主要使用了 @Pre 和 @Post 注解。
@Pre 代表執(zhí)行前,@Post 代表執(zhí)行后。在方法的安全表達式中,有四個相關(guān)的表達式注解,分別是:@PreAuthorize
,@PreFilter
,@PostAuthorize
和 @PostFilter
。要啟用這四個標(biāo)簽要,首先要增加全局配置:
<global-method-security pre-post-annotations="enabled"/>
4.1 使用 @PreAuthorize 和 @PostAuthorize 進行訪問控制
@PreAuthorize 最主要的作用是決定一個方法能否被執(zhí)行。例如:
@PreAuthorize("hasRole('USER')")
public void create(Contact contact);
這段代碼的含義是,只有包含了 USER
角色的用戶,才被允許調(diào)用 create
方法。
對于權(quán)限,還有另一種更為具體的寫法:
@PreAuthorize("hasPermission(#contact, 'admin')")
public void deletePermission(Contact contact, Sid recipient, Permission permission);
這里我們指定了調(diào)用方法中的參數(shù),用來判斷當(dāng)前用戶是否對將被刪除的 Contact
對象具有 admin
角色。通過這種寫法,我們可以將目標(biāo)對象中的任意方法作為表達式的變量參數(shù)。
Spring Security 要在表達式中訪問方法參數(shù)有幾種方式:
- Spring Security 可以使用
@P
注解,為變量設(shè)置別名。例如:
import org.springframework.security.access.method.P;
...
@PreAuthorize("#c.name == authentication.name")
public void doSomething(@P("c") Contact contact);
- 使用 Spring 的
@Param
注解,為變量設(shè)置別名。例如:
import org.springframework.data.repository.query.Param;
...
@PreAuthorize("#n == authentication.name")
Contact findContactByName(@Param("n") String name);
- 如果使用的是 JDK 8 + Spring 4+ ,不需要額外的注解,程序會自動發(fā)現(xiàn)參數(shù)名稱。
Spring Security 的表達式注解支持對象的屬性。例如,當(dāng)我們指定方法入?yún)⒅械膶傩悦Q和我們身份信息中的名稱一致時允許訪問:
@PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);
這里我們用到了內(nèi)置參數(shù) authentication
,該對象保存在 Spring Security 的安全上下文中。我們也可以直接訪問到該對象的身份信息屬性。
@PostAuthorize 使用場景相對較少,它針對方法的返回進行權(quán)限過濾。
4.2 使用 @PreFilter 和 @PostFilter
@PreFilter
過濾器用于處理參數(shù)中的集合或者數(shù)組對象;@PostFilter
用于過濾返回值中集合或者數(shù)組對象。
例如:
@PreAuthorize("hasRole('USER')")
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
public List<Contact> getAll();
5. 小結(jié)
本節(jié)討論了 Spring Security 中如何通過表達式的方式簡化權(quán)限規(guī)則配置,主要內(nèi)容有:
- 表達式方式簡化了鑒權(quán)配置難度,增強鑒權(quán)規(guī)則的可讀性;
- Spring 內(nèi)置了非常多的表達式,可以滿足大多數(shù)場景;
- 表達式是對實例方法的引用,我們可以擴展自己的表達式鑒權(quán)方法;
- @PreAuthorize 和 @PostAuthorize 分別代表在方法調(diào)用前或方法返回前執(zhí)行表達式鑒權(quán);
- @PreFilter 和 @PostFilter 是對入?yún)⒒蛘叻祷刂档臈l件過濾。
下節(jié)我們討論一個新的概念「安全對象」。