昨天有個粉絲加了我,問我如何實現類似shiro的資源許可權表達式的訪問控制。我以前有一個小框架用的就是shiro,許可權控制就用了資源許可權表達式,所以這個東西對我不陌生,但是在Spring Security中我並沒有使用過它,不過我認為Spring Security可以實現這一點。是的,我找到了實現它的 ...
昨天有個粉絲加了我,問我如何實現類似shiro的資源許可權表達式的訪問控制。我以前有一個小框架用的就是shiro,許可權控制就用了資源許可權表達式,所以這個東西對我不陌生,但是在Spring Security中我並沒有使用過它,不過我認為Spring Security可以實現這一點。是的,我找到了實現它的方法。
資源許可權表達式
說了這麼多,我覺得應該解釋一下什麼叫資源許可權表達式。許可權控制的核心就是清晰地表達出特定資源的某種操作,一個格式良好好的許可權聲明可以清晰表達出用戶對該資源擁有的操作許可權。
通常一個資源在系統中的標識是唯一的,比如User用來標識用戶,ORDER標識訂單。不管什麼資源大都可以歸納出以下這幾種操作
在 shiro許可權聲明通常對上面的這種資源操作關係用冒號分隔的方式進行表示。例如讀取用戶信息的操作表示為USER:READ
,甚至還可以更加細一些,用USER:READ:123
表示讀取ID為123
的用戶許可權。
資源操作定義好了,再把它和角色關聯起來不就是基於RBAC的許可權資源控制了嗎?就像下麵這樣:
這樣資源和角色的關係可以進行CRUD操作進行動態綁定。
Spring Security中的實現
資源許可權表達式動態許可權控制在Spring Security也是可以實現的。首先開啟方法級別的註解安全控制。
/**
* 開啟方法安全註解
*
* @author felord.cn
*/
@EnableGlobalMethodSecurity(prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true)
public class MethodSecurityConfig {
}
MethodSecurityExpressionHandler
MethodSecurityExpressionHandler
提供了一個對方法進行安全訪問的門面擴展。它的實現類DefaultMethodSecurityExpressionHandler
更是提供了針對方法的一系列擴展介面,這裡我總結了一下:
這裡的PermissionEvaluator
正好可以滿足需要。
PermissionEvaluator
PermissionEvaluator
介面抽象了對一個用戶是否有許可權訪問一個特定的領域對象的評估過程。
public interface PermissionEvaluator extends AopInfrastructureBean {
boolean hasPermission(Authentication authentication,
Object targetDomainObject, Object permission);
boolean hasPermission(Authentication authentication,
Serializable targetId, String targetType, Object permission);
}
這兩個方法僅僅參數列表不同,這些參數的含義為:
authentication
當前用戶的認證信息,持有當前用戶的角色許可權。targetDomainObject
用戶想要訪問的目標領域對象,例如上面的USER
。permission
這個當前方法設定的目標領域對象的許可權,例如上面的READ
。targetId
這種是對上面targetDomainObject
的具體化,比如ID為123
的USER
,我覺得還可以搞成租戶什麼的。targetType
是為了配合targetId
。
第一個方法是用來實現
USER:READ
的;第二個方法是用來實現USER:READ:123
的。
思路以及實現
targetDomainObject:permission
不就是USER:READ
的抽象嗎?只要找出USER:READ
對應的角色集合,和當前用戶持有的角色進行比對,它們存在交集就證明用戶有許可權訪問。藉著這個思路胖哥實現了一個PermissionEvaluator
:
/**
* 資源許可權評估
*
* @author felord.cn
*/
public class ResourcePermissionEvaluator implements PermissionEvaluator {
private final BiFunction<String, String, Collection<? extends GrantedAuthority>> permissionFunction;
public ResourcePermissionEvaluator(BiFunction<String, String, Collection<? extends GrantedAuthority>> permissionFunction) {
this.permissionFunction = permissionFunction;
}
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
//查詢方法標註對應的角色
Collection<? extends GrantedAuthority> resourceAuthorities = permissionFunction.apply((String) targetDomainObject, (String) permission);
// 用戶對應的角色
Collection<? extends GrantedAuthority> userAuthorities = authentication.getAuthorities();
// 對比 true 就能訪問 false 就不能訪問
return userAuthorities.stream().anyMatch(resourceAuthorities::contains);
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
//todo
System.out.println("targetId = " + targetId);
return true;
}
}
第二個方法沒有實現,因為兩個差不多,第二個你可以想想具體的使用場景。
配置和使用
PermissionEvaluator
需要註入到Spring IoC,並且Spring IoC只能有一個該類型的Bean:
@Bean
PermissionEvaluator resourcePermissionEvaluator() {
return new ResourcePermissionEvaluator((targetDomainObject, permission) -> {
//TODO 這裡形式其實可以不固定
String key = targetDomainObject + ":" + permission;
//TODO 查詢 key 和 authority 的關聯關係
// 模擬 permission 關聯角色 根據key 去查 grantedAuthorities
Set<SimpleGrantedAuthority> grantedAuthorities = new HashSet<>();
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
return "USER:READ".equals(key) ? grantedAuthorities : new HashSet<>();
});
}
接下來寫個介面,用@PreAuthorize
註解標記,然後直接用hasPermission('USER','READ')
來靜態綁定該介面的訪問許可權表達式:
@GetMapping("/postfilter")
@PreAuthorize("hasPermission('USER','READ')")
public Collection<String> postfilter(){
List<String> list = new ArrayList<>();
list.add("felord.cn");
list.add("碼農小胖哥");
list.add("請關註一下");
return list;
}
然後定義一個用戶:
@Bean
UserDetailsService users() {
UserDetails user = User.builder()
.username("felord")
.password("123456")
.passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()::encode)
.roles("USER")
.authorities("ROLE_ADMIN","ROLE_USER")
.build();
return new InMemoryUserDetailsManager(user);
}
接下來肯定是正常能夠訪問介面的。當你改變了@PreAuthorize
中表達式的值或者移除了用戶的ROLE_ADMIN
許可權,再或者USER:READ
關聯到了其它角色等等,都會返回403
。
留給你去測試的
你可以看看註解改成這樣會是什麼效果:
@PreAuthorize("hasPermission('1234','USER','READ')")
還有這個:
@PreAuthorize("hasPermission('USER','READ') or hasRole('ADMIN')")
或者讓targetId
動態化:
@PreAuthorize("hasPermission(#id,'USER','READ')")
public Collection<String> postfilter(String id){
}
關註公眾號:Felordcn 獲取更多資訊
博主:碼農小胖哥 出處:felord.cn 本文版權歸原作者所有,不可商用,轉載需要聲明出處,否則保留追究法律責任的權利。如果文中有什麼錯誤,歡迎指出。以免更多的人被誤導。 |