昨天有個(gè)粉絲加了我,問我如何實(shí)現(xiàn)類似shiro的資源權(quán)限表達(dá)式的訪問控制。我以前有一個(gè)小框架用的就是shiro,權(quán)限控制就用了資源權(quán)限表達(dá)式,所以這個(gè)東西對(duì)我不陌生,但是在Spring Security中我并沒有使用過它,不過我認(rèn)為Spring Security可以實(shí)現(xiàn)這一點(diǎn)。是的,我找到了實(shí)現(xiàn)它的方法。
?
資源權(quán)限表達(dá)式
說了這么多,我覺得應(yīng)該解釋一下什么叫資源權(quán)限表達(dá)式。權(quán)限控制的核心就是清晰地表達(dá)出特定資源的某種操作,一個(gè)格式良好好的權(quán)限聲明可以清晰表達(dá)出用戶對(duì)該資源擁有的操作權(quán)限。
通常一個(gè)資源在系統(tǒng)中的標(biāo)識(shí)是唯一的,比如User用來標(biāo)識(shí)用戶,ORDER標(biāo)識(shí)訂單。不管什么資源大都可以歸納出以下這幾種操作
在 shiro權(quán)限聲明通常對(duì)上面的這種資源操作關(guān)系用冒號(hào)分隔的方式進(jìn)行表示。例如讀取用戶信息的操作表示為??USER:READ?
??,甚至還可以更加細(xì)一些,用??USER:READ:123?
?表示讀取ID為??123?
?的用戶權(quán)限。
資源操作定義好了,再把它和角色關(guān)聯(lián)起來不就是基于RBAC的權(quán)限資源控制了嗎?就像下面這樣:
這樣資源和角色的關(guān)系可以進(jìn)行CRUD操作進(jìn)行動(dòng)態(tài)綁定。
Spring Security中的實(shí)現(xiàn)
資源權(quán)限表達(dá)式的動(dòng)態(tài)權(quán)限控制在Spring Security也是可以實(shí)現(xiàn)的。首先開啟方法級(jí)別的注解安全控制。
/**
* 開啟方法安全注解
*
* @author felord.cn
*/
@EnableGlobalMethodSecurity(prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true)
public class MethodSecurityConfig {
}
MethodSecurityExpressionHandler
??MethodSecurityExpressionHandler?
?? 提供了一個(gè)對(duì)方法進(jìn)行安全訪問的門面擴(kuò)展。它的實(shí)現(xiàn)類??DefaultMethodSecurityExpressionHandler?
?更是提供了針對(duì)方法的一系列擴(kuò)展接口,這里我總結(jié)了一下:
這里的??PermissionEvaluator?
?正好可以滿足需要。
PermissionEvaluator
??PermissionEvaluator?
? 接口抽象了對(duì)一個(gè)用戶是否有權(quán)限訪問一個(gè)特定的領(lǐng)域?qū)ο蟮脑u(píng)估過程。
public interface PermissionEvaluator extends AopInfrastructureBean {
boolean hasPermission(Authentication authentication,
Object targetDomainObject, Object permission);
boolean hasPermission(Authentication authentication,
Serializable targetId, String targetType, Object permission);
}
這兩個(gè)方法僅僅參數(shù)列表不同,這些參數(shù)的含義為:
- ?
?authentication?
? 當(dāng)前用戶的認(rèn)證信息,持有當(dāng)前用戶的角色權(quán)限。 - ?
?targetDomainObject?
? 用戶想要訪問的目標(biāo)領(lǐng)域?qū)ο?,例如上面??USER?
?。 - ?
?permission?
? 這個(gè)當(dāng)前方法設(shè)定的目標(biāo)領(lǐng)域?qū)ο蟮臋?quán)限,例如上面的??READ?
?。 - ?
?targetId?
? 這種是對(duì)上面??targetDomainObject?
? 的具體化,比如ID為??123?
?的??USER?
?,我覺得還可以搞成租戶什么的。 - ?
?targetType?
? 是為了配合??targetId?
? 。
?
第一個(gè)方法是用來實(shí)現(xiàn)?
?USER:READ?
??的;第二個(gè)方法是用來實(shí)現(xiàn)??USER:READ:123?
?的。
思路以及實(shí)現(xiàn)
??targetDomainObject:permission?
??不就是??USER:READ?
??的抽象嗎?只要找出??USER:READ?
??對(duì)應(yīng)的角色集合,和當(dāng)前用戶持有的角色進(jìn)行比對(duì),它們存在交集就證明用戶有權(quán)限訪問。借著這個(gè)思路胖哥實(shí)現(xiàn)了一個(gè)??PermissionEvaluator?
?:
/**
* 資源權(quán)限評(píng)估
*
* @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) {
//查詢方法標(biāo)注對(duì)應(yīng)的角色
Collection<? extends GrantedAuthority> resourceAuthorities = permissionFunction.apply((String) targetDomainObject, (String) permission);
// 用戶對(duì)應(yīng)的角色
Collection<? extends GrantedAuthority> userAuthorities = authentication.getAuthorities();
// 對(duì)比 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;
}
}
?
第二個(gè)方法沒有實(shí)現(xiàn),因?yàn)閮蓚€(gè)差不多,第二個(gè)你可以想想具體的使用場(chǎng)景。
配置和使用
??PermissionEvaluator?
? 需要注入到Spring IoC,并且Spring IoC只能有一個(gè)該類型的Bean:
@Bean
PermissionEvaluator resourcePermissionEvaluator() {
return new ResourcePermissionEvaluator((targetDomainObject, permission) -> {
//TODO 這里形式其實(shí)可以不固定
String key = targetDomainObject + ":" + permission;
//TODO 查詢 key 和 authority 的關(guān)聯(lián)關(guān)系
// 模擬 permission 關(guān)聯(lián)角色 根據(jù)key 去查 grantedAuthorities
Set<SimpleGrantedAuthority> grantedAuthorities = new HashSet<>();
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
return "USER:READ".equals(key) ? grantedAuthorities : new HashSet<>();
});
}
接下來寫個(gè)接口,用??@PreAuthorize?
??注解標(biāo)記,然后直接用??hasPermission('USER','READ')?
?來靜態(tài)綁定該接口的訪問權(quán)限表達(dá)式:
@GetMapping("/postfilter")
@PreAuthorize("hasPermission('USER','READ')")
public Collection<String> postfilter(){
List<String> list = new ArrayList<>();
list.add("felord.cn");
list.add("碼農(nóng)小胖哥");
list.add("請(qǐng)關(guān)注一下");
return list;
}
然后定義一個(gè)用戶:
@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);
}
接下來肯定是正常能夠訪問接口的。當(dāng)你改變了??@PreAuthorize?
??中表達(dá)式的值或者移除了用戶的??ROLE_ADMIN?
??權(quán)限,再或者??USER:READ?
??關(guān)聯(lián)到了其它角色等等,都會(huì)返回??403?
??。其它關(guān)于SpringSecurity注解的知識(shí)可以看?這一篇。??
留給你去測(cè)試的
你可以看看注解改成這樣會(huì)是什么效果:
@PreAuthorize("hasPermission('USER','READ') or hasRole('ADMIN')")
這樣呢?
@PreAuthorize("hasPermission('1234','USER','READ')")
或者讓??targetId?
?動(dòng)態(tài)化:
@PreAuthorize("hasPermission(#id,'USER','READ')")
public Collection<String> postfilter(String id){
}
可以留言區(qū)回復(fù)測(cè)試結(jié)果
另外最近胖哥有很多成系列的內(nèi)容輸出:
OAuth2?系列教程,已經(jīng)更新了40多篇。
- 開源了一個(gè)登錄組件擴(kuò)展spring-security-login-extension,降低對(duì)接配置成本,歡迎學(xué)習(xí)、star。
- 粉絲福利:正版IntelliJ IDEA?抽獎(jiǎng)
有興趣的可以看看。
本文摘自 :https://blog.51cto.com/u