A fan added me yesterday and asked me how to implement shiro-like access control for resource permission expressions. I used to have a small framework that used shiro, and the permission control used resource permission expressions, so this thing is not unfamiliar to me, but I have not used it in Spring Security, but I think Spring Security can achieve this . Yes, I found a way to implement it.
Resource permission expressions
Having said so much, I think I should explain what a resource permission expression is. The core of permission control is to clearly express a certain operation of a specific resource. A well-formed permission statement can clearly express the user's operation permission for the resource.
Usually, the identifier of a resource in the system is unique, for example, User is used to identify users, and ORDER is used to identify orders. Regardless of the resources, the following operations can be summarized
In the shiro permission declaration, the above resource operation relationship is usually expressed in a colon-separated manner. For example, the operation of reading user information is expressed as USER:READ
, or even more detailed, with USER:READ:123
to indicate that the user permission to read the ID is 123
.
After the resource operation is defined, isn't it the RBAC-based permission resource control to associate it with the role? Like the following:
In this way, the relationship between resources and roles can be dynamically bound by CRUD operations.
Implementation in Spring Security
Dynamic permission control of resource permission expressions is also possible in Spring Security. First, enable method-level annotation security controls.
/**
* 开启方法安全注解
*
* @author felord.cn
*/
@EnableGlobalMethodSecurity(prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true)
public class MethodSecurityConfig {
}
MethodSecurityExpressionHandler
MethodSecurityExpressionHandler
provides a facade extension for secure access to methods. Its implementation class DefaultMethodSecurityExpressionHandler
also provides a series of extended interfaces for methods. Here I summarize:
Here PermissionEvaluator
just meets the needs.
PermissionEvaluator
PermissionEvaluator
interface abstracts the process of evaluating whether a user has permission to access a particular domain object.
public interface PermissionEvaluator extends AopInfrastructureBean {
boolean hasPermission(Authentication authentication,
Object targetDomainObject, Object permission);
boolean hasPermission(Authentication authentication,
Serializable targetId, String targetType, Object permission);
}
The only difference between the two methods is the parameter list. The meaning of these parameters is:
-
authentication
Authentication information of the current user, holding the role permissions of the current user. -
targetDomainObject
The target domain object the user wants to access, such as the aboveUSER
. -
permission
The permission of the target domain object set by the current method, such as the aboveREAD
. -
targetId
This is the embodiment of the abovetargetDomainObject
, for example, the ID is123
USER
of. -
targetType
is to cooperate withtargetId
.
The first method is used to implementUSER:READ
; the second method is used to implementUSER:READ:123
.
ideas and implementation
targetDomainObject:permission
n't USER:READ
an abstraction of ---fba258224a5a600b44d734dcb53fbdf2---? Just find out the corresponding role set USER:READ
and compare it with the roles held by the current user. The intersection of them proves that the user has permission to access. With this idea, Brother Fat realized a 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;
}
}
The second method is not implemented, because the two are similar, the second you can think about specific usage scenarios.
Configure and use
PermissionEvaluator
needs to be injected into Spring IoC , and Spring IoC can only have one bean of this type:
@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<>();
});
}
Next, write an interface, mark it with @PreAuthorize
annotation, and then directly use hasPermission('USER','READ')
to statically bind the access permission expression of the interface:
@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;
}
Then define a user:
@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);
}
The next step is to be able to access the interface normally. When you change the value of the expression in @PreAuthorize
or remove the user's ROLE_ADMIN
permissions, or USER:READ
are associated with other roles, etc., it will return 403
.
I leave it to you to test
You can see how the annotation changes to this:
@PreAuthorize("hasPermission('1234','USER','READ')")
and this:
@PreAuthorize("hasPermission('USER','READ') or hasRole('ADMIN')")
Or make targetId
dynamic:
@PreAuthorize("hasPermission(#id,'USER','READ')")
public Collection<String> postfilter(String id){
}
关注公众号:Felordcn 获取更多资讯
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。