Spring Security實現基於RBAC的許可權表示式動態訪問控制

碼農小胖哥發表於2022-04-19

昨天有個粉絲加了我,問我如何實現類似shiro的資源許可權表示式的訪問控制。我以前有一個小框架用的就是shiro,許可權控制就用了資源許可權表示式,所以這個東西對我不陌生,但是在Spring Security中我並沒有使用過它,不過我認為Spring Security可以實現這一點。是的,我找到了實現它的方法。

資源許可權表示式

說了這麼多,我覺得應該解釋一下什麼叫資源許可權表示式。許可權控制的核心就是清晰地表達出特定資源的某種操作,一個格式良好好的許可權宣告可以清晰表達出使用者對該資源擁有的操作許可權。

通常一個資源在系統中的標識是唯一的,比如User用來標識使用者,ORDER標識訂單。不管什麼資源大都可以歸納出以下這幾種操作

在 shiro許可權宣告通常對上面的這種資源操作關係用冒號分隔的方式進行表示。例如讀取使用者資訊的操作表示為USER:READ,甚至還可以更加細一些,用USER:READ:123表示讀取ID123的使用者許可權。

資源操作定義好了,再把它和角色關聯起來不就是基於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為123USER,我覺得還可以搞成租戶什麼的。
  • 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 獲取更多資訊

個人部落格:https://felord.cn

相關文章