Java利用SpEL表達(dá)式實(shí)現(xiàn)權(quán)限校驗(yàn)
對于在Springboot中,利用自定義注解+切面來實(shí)現(xiàn)接口權(quán)限的控制這個(gè)大家應(yīng)該都很熟悉,也有大量的博客來介紹整個(gè)的實(shí)現(xiàn)過程,整體來說思路如下:
- 自定義一個(gè)權(quán)限校驗(yàn)的注解,包含參數(shù)value
- 配置在對應(yīng)的接口上
- 定義一個(gè)切面類,指定切點(diǎn)
- 在切入的方法體里寫上權(quán)限判斷的邏輯
乍一看,沒毛病,學(xué)到了,學(xué)到了~,收藏起來。但是呢,等到實(shí)際用到的時(shí)候就傻眼了,為什么呢?在實(shí)際的開發(fā)中,你會發(fā)現(xiàn),對于權(quán)限校驗(yàn)的需求場景是很多的,比如:
- 只要配置了任何角色,就可以訪問
- 有某個(gè)權(quán)限就可以訪問
- 放行所有請求
- 只有超級管理員角色才可以訪問
- 只有登錄后才可以訪問
- 在指定時(shí)間段內(nèi)可以訪問
- 有某個(gè)角色的情況下才可以訪問
- 同時(shí)具有指定的多個(gè)角色情況下才可以訪問
- 等
傻眼了不,按照上面的實(shí)現(xiàn)邏輯的話怎么搞?加注解?寫各種判斷?這時(shí)候,其實(shí)我們就可以通過SpEL表達(dá)式來幫我們處理這個(gè)問題。
Spring Security實(shí)現(xiàn)
Spring Security已經(jīng)幫我們實(shí)現(xiàn)了如何通過注解,直接上例子:
@PreAuthorize("@pms.hasPermission('1-6')")
@ApiOperation("查 詢")
@GetMapping
public PageResponse<RoleCO> pageRole(RolePageQry qry){
return roleService.listPage(qry);
}
通過@PreAuthorize("@pms.hasPermission('1-6')")實(shí)現(xiàn)了訪問該接口,需要有1-6這個(gè)權(quán)限。我們需要實(shí)現(xiàn)pms
public interface Pms {
/**
* 判斷接口是否有任意xxx,xxx權(quán)限
*
* @param permission 權(quán)限
* @return {boolean}
*/
default boolean hasPermission(String permission) {
if (StrUtil.isBlank(permission)) {
return false;
}
return listPermissions().contains(permission);
}
//
default boolean hasAnyPermission(String... permissions) {
if (CollectionUtil.isEmpty(Arrays.asList(permissions))) {
return false;
}
return listPermissions().stream().filter(StringUtils::hasText)
.anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x));
}
List<String> listPermissions();
}
@Data
public class Pms implements com.jjb.saas.framework.api.system.Pms {
/**
* 菜單下有哪些按鈕權(quán)限
*
* @param
* @return {boolean}
*/
@Override
public List<String> listPermissions() {
MultiResponse multiResponse = FacadeServiceFactory.getInstance().genericInvoke("com.jjb.saas.system.client.permsgroup.facade.PermsGroupFacade", "listPermssionsByAppKey", MultiResponse.class);
if (multiResponse.isSuccess()) {
List datas = multiResponse.getData();
if (ObjectUtil.isNotEmpty(datas)) {
return datas;
}
return Collections.emptyList();
}
return Collections.emptyList();
}
}
注入到spring中
@ComponentScan
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
@AllArgsConstructor
public class SecurityConfiguration {
@Bean("pms")
@ConditionalOnMissingBean
public Pms pms() {
Pms pms = new Pms();
return pms;
}
}
接下來,我們就來看看如何自己造輪子
SpEL表達(dá)式
提到SpEL,那么到底SpEL是啥呢?
SpEL的全稱為Spring Expression Language,即Spring表達(dá)式語言。是Spring3.0提供的。他最強(qiáng)大的功能是可以通過運(yùn)行期間執(zhí)行的表達(dá)式將值裝配到我們的屬性或構(gòu)造函數(shù)之中。
如果有小伙伴之前沒有接觸過,不太理解這句話的含義,那么不要緊,繼續(xù)往下看,通過后續(xù)的實(shí)踐你就能明白他的作用了。
造輪子
自定義注解
當(dāng)然,萬變不離其宗,自定義注解我們還是需要滴。這里呢,我們僅需要定義一個(gè)value屬性用于接收表達(dá)式即可。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuth {
/**
*
*
* permissionAll()-----只要配置了角色就可以訪問
* hasPermission("MENU.QUERY")-----有MENU.QUERY操作權(quán)限的角色可以訪問
* permitAll()-----放行所有請求
* denyAll()-----只有超級管理員角色才可訪問
* hasAuth()-----只有登錄后才可訪問
* hasTimeAuth(1,,10)-----只有在1-10點(diǎn)間訪問
* hasRole(‘管理員')-----具有管理員角色的人才能訪問
* hasAllRole(‘管理員','總工程師')-----同時(shí)具有管理員、總工程師角色的人才能訪問
*
* Spring el
* 文檔地址:https://docs.spring.io/spring/docs/5.1.6.RELEASE/spring-framework-reference/core.html#expressions
*/
String value();
}
定義切面
注解定義好了,我們就需要定義切面了。這里要考慮一個(gè)點(diǎn)。我們希望的是如果方法上有注解,則對方法進(jìn)行限制,若方法上無注解,單是類上有注解,那么類上的權(quán)限注解對該類下所有的接口生效。因此,我們切點(diǎn)的話要用@within注解。代碼如下:
@Around(
"@annotation(PreAuth注解路徑) || " +
"@within(PreAuth注解路徑)"
)
public Object preAuth(ProceedingJoinPoint point) throws Throwable {
if (handleAuth(point)) {
return point.proceed();
}
throw new SecureException(ResultCode.REQ_REJECT);
}
private boolean handleAuth(ProceedingJoinPoint point) {
//TODO 邏輯判斷,返回true or false
}
權(quán)限校驗(yàn)
關(guān)鍵點(diǎn)來了。這里我們要引入SpEL。
首先,引入SpEL:
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
然后,從注解上獲取我們需要的表達(dá)式:
MethodSignature ms = point.getSignature() instanceof MethodSignature? (MethodSignature) point.getSignature():null;
Method method = ms.getMethod();
// 讀取權(quán)限注解,優(yōu)先方法上,沒有則讀取類
PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class);
// 判斷表達(dá)式
String condition = preAuth.value();
if (StringUtil.isNotBlank(condition)) {
//TODU 表達(dá)式解析
}
表達(dá)式解析
private boolean handleAuth(ProceedingJoinPoint point) {
MethodSignature ms = point.getSignature() instanceof MethodSignature? (MethodSignature) point.getSignature():null;
Method method = ms.getMethod();
// 讀取權(quán)限注解,優(yōu)先方法上,沒有則讀取類
PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class);
// 判斷表達(dá)式
String condition = preAuth.value();
if (StringUtil.isNotBlank(condition)) {
Expression expression = EXPRESSION_PARSER.parseExpression(condition);
// 方法參數(shù)值
Object[] args = point.getArgs();
StandardEvaluationContext context = getEvaluationContext(method, args);
//獲取解析計(jì)算的結(jié)果
return expression.getValue(context, Boolean.class);
}
return false;
}
/**
* 獲取方法上的參數(shù)
*
* @param method 方法
* @param args 變量
* @return {SimpleEvaluationContext}
*/
private StandardEvaluationContext getEvaluationContext(Method method, Object[] args) {
// 初始化Sp el表達(dá)式上下文,并設(shè)置 AuthFun
StandardEvaluationContext context = new StandardEvaluationContext(new AuthFun());
// 設(shè)置表達(dá)式支持spring bean
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
for (int i = 0; i < args.length; i++) {
// 讀取方法參數(shù)
MethodParameter methodParam = ClassUtil.getMethodParameter(method, i);
// 設(shè)置方法 參數(shù)名和值 為spel變量
context.setVariable(methodParam.getParameterName(), args[i]);
}
return context;
}
自定義解析方法
看完上面的解析處理是不是很蒙蔽,只看到了獲取表達(dá)式,獲取參數(shù),設(shè)置參數(shù),然后expression.getValue就完事了。有的同學(xué)會問,你權(quán)限校驗(yàn)的邏輯呢?
別急,關(guān)鍵點(diǎn)在這:StandardEvaluationContext context = new StandardEvaluationContext(new AuthFun());在上文代碼中找到了吧。這個(gè)AuthFun就是我們進(jìn)行權(quán)限校驗(yàn)的對象。
所以呢,我們還得在定義一下這個(gè)對象。進(jìn)行具體的權(quán)限校驗(yàn)邏輯處理,這里定的每一個(gè)方法都可以作為表達(dá)式在權(quán)限注解中使用。代碼如下:
public class AuthFun {
/**
* 判斷角色是否具有接口權(quán)限
*
* @return {boolean}
*/
public boolean permissionAll() {
//TODO
}
/**
* 判斷角色是否具有接口權(quán)限
*
* @param permission 權(quán)限編號,對應(yīng)菜單的MENU_CODE
* @return {boolean}
*/
public boolean hasPermission(String permission) {
//TODO
}
/**
* 放行所有請求
*
* @return {boolean}
*/
public boolean permitAll() {
return true;
}
/**
* 只有超管角色才可訪問
*
* @return {boolean}
*/
public boolean denyAll() {
return hasRole(RoleConstant.ADMIN);
}
/**
* 是否已授權(quán)
*
* @return {boolean}
*/
public boolean hasAuth() {
if(Func.isEmpty(AuthUtil.getUser())){
// TODO 返回異常提醒
}else{
return true;
}
}
/**
* 是否有時(shí)間授權(quán)
*
* @param start 開始時(shí)間
* @param end 結(jié)束時(shí)間
* @return {boolean}
*/
public boolean hasTimeAuth(Integer start, Integer end) {
Integer hour = DateUtil.hour();
return hour >= start && hour <= end;
}
/**
* 判斷是否有該角色權(quán)限
*
* @param role 單角色
* @return {boolean}
*/
public boolean hasRole(String role) {
return hasAnyRole(role);
}
/**
* 判斷是否具有所有角色權(quán)限
*
* @param role 角色集合
* @return {boolean}
*/
public boolean hasAllRole(String... role) {
for (String r : role) {
if (!hasRole(r)) {
return false;
}
}
return true;
}
/**
* 判斷是否有該角色權(quán)限
*
* @param role 角色集合
* @return {boolean}
*/
public boolean hasAnyRole(String... role) {
//獲取當(dāng)前登錄用戶
BladeUser user = AuthUtil.getUser();
if (user == null) {
return false;
}
String userRole = user.getRoleName();
if (StringUtil.isBlank(userRole)) {
return false;
}
String[] roles = Func.toStrArray(userRole);
for (String r : role) {
if (CollectionUtil.contains(roles, r)) {
return true;
}
}
return false;
}
}
實(shí)際使用
在使用的時(shí)候,我們只需要在類上或者接口上,加上@PreAuth的直接,value值寫的時(shí)候要注意一下,value應(yīng)該是我們在AuthFun類中定義的方法和參數(shù),如我們定義了解析方法hasAllRole(String... role),那么在注解中,我們就可以這樣寫@PreAuth("hasAllRole('角色1','角色2')"),需要注意的是,參數(shù)要用單引號包括。
@PreAuth("hasPermission('LM_QUERY,LM_QUERY_ALL')")
public T 接口名稱....
原理
根據(jù)上面的實(shí)際使用,可以看到。SpEL表達(dá)式解析將我們注解中的"hasAllRole('角色1','角色2')"這樣的字符串,給動(dòng)態(tài)解析為了hasAllRole(參數(shù)1,參數(shù)1),并調(diào)用我們注冊類中同名的方法。
總結(jié)
通過SpEL的使用,讓我們的權(quán)限配置校驗(yàn)更加靈活。當(dāng)出現(xiàn)新的場景時(shí),我們僅需要在自定的表達(dá)式解析類中增加對應(yīng)場景的解析方法即可。相對于之前的實(shí)現(xiàn)方式,這不得不說是更好的一個(gè)選擇。
以上就是Java利用SpEL表達(dá)式實(shí)現(xiàn)權(quán)限校驗(yàn)的詳細(xì)內(nèi)容,更多關(guān)于Java權(quán)限校驗(yàn)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java異常 Exception類及其子類(實(shí)例講解)
下面小編就為大家?guī)硪黄狫ava異常 Exception類及其子類(實(shí)例講解)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-11-11
java中分組統(tǒng)計(jì)的三種實(shí)現(xiàn)方式
這篇文章主要介紹了java中分組統(tǒng)計(jì)的三種實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
Java實(shí)戰(zhàn)之仿天貓商城系統(tǒng)的實(shí)現(xiàn)
這篇文章主要介紹了如何利用Java制作一個(gè)基于SSM框架的迷你天貓商城系統(tǒng),文中采用的技術(shù)有JSP、Springboot、SpringMVC、Spring等,需要的可以參考一下2022-03-03
SpringBoot封裝響應(yīng)數(shù)據(jù)實(shí)現(xiàn)過程詳解
這篇文章主要介紹了SpringBoot封裝響應(yīng)數(shù)據(jù)實(shí)現(xiàn)過程,SpringBoot響應(yīng)數(shù)據(jù)封裝是指在SpringBoot應(yīng)用程序中,將返回的數(shù)據(jù)進(jìn)行封裝,以便于前端頁面或其他客戶端使用,感興趣想要詳細(xì)了解可以參考下文2023-05-05
合并有序數(shù)組的實(shí)現(xiàn)(java與C語言)
這篇文章主要介紹了合并有序數(shù)組的實(shí)現(xiàn)(java與C語言)的相關(guān)資料,這里對有序數(shù)組的合并分享了java版本和C語言版本的示例,需要的朋友可以參考下2017-08-08
Java JDBC的底層實(shí)現(xiàn)原理深度解析
JDBC是Java和數(shù)據(jù)庫之間的一個(gè)橋梁,是一個(gè)「規(guī)范」而不是一個(gè)實(shí)現(xiàn),能夠執(zhí)行SQL語句,JDBC由一組用Java語言編寫的類和接口組成,各種不同類型的數(shù)據(jù)庫都有相應(yīng)的實(shí)現(xiàn),這篇文章給大家介紹Java JDBC的底層實(shí)現(xiàn)原理,感興趣的朋友跟隨小編一起看看吧2025-11-11
遠(yuǎn)程調(diào)用@FeignClient注解屬性使用詳解
這篇文章主要為大家介紹了遠(yuǎn)程調(diào)用@FeignClient注解屬性使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
Java實(shí)現(xiàn)FTP服務(wù)器功能實(shí)例代碼
FTP(File Transfer Protocol 文件傳輸協(xié)議)是Internet 上用來傳送文件的協(xié)議,本文給大家分享Java實(shí)現(xiàn)FTP服務(wù)器功能實(shí)例代碼,對java實(shí)現(xiàn)ftp服務(wù)器相關(guān)知識感興趣的朋友一起學(xué)習(xí)吧2015-12-12

