2

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 above USER .
  • permission The permission of the target domain object set by the current method, such as the above READ .
  • targetId This is the embodiment of the above targetDomainObject , for example, the ID is 123 USER of.
  • targetType is to cooperate with targetId .
The first method is used to implement USER:READ ; the second method is used to implement USER: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 获取更多资讯

Personal blog: https://felord.cn


码农小胖哥
3.8k 声望8k 粉丝