天天看點

TienChin 項目中的 RBAC 是怎麼玩的?

松哥最近正在錄制 TienChin 項目視訊~采用 Spring Boot+Vue3 技術棧,裡邊會涉及到各種好玩的技術,小夥伴們來和松哥一起做一個完成率超 90% 的項目,戳戳戳這裡-->TienChin 項目配套視訊來啦

昨天的文章和小夥伴們簡單分享了權限細化到按鈕的思路,傳送門:

  • 權限想要細化到按鈕,怎麼做?

這篇文章最後留下來一個問題,就是使用者的權限該如何設定?今天我們就來聊聊這個話題。

1. 角色與權限

首先我們先來看看角色與權限,該如何設計角色與權限,其實有很多非常成熟的理論,最為常見的莫過于 RBAC 了。

1.1 RBAC 簡介

RBAC(Role-based access control)是一種以角色為基礎的通路控制(Role-based access control,RBAC),它是一種較新且廣為使用的權限控制機制,這種機制不是直接給使用者賦予權限,而是将權限賦予角色。

RBAC 權限模型将使用者按角色進行歸類,通過使用者的角色來确定使用者對某項資源是否具備操作權限。RBAC 簡化了使用者與權限的管理,它将使用者與角色關聯、角色與權限關聯、權限與資源關聯,這種模式使得使用者的授權管理變得非常簡單和易于維護。

1.2 RBAC 的提出

權限、角色這些東西,在早期 1970 年代的商業計算機程式中就可以找到相關的應用,但是早期的程式相對簡單,而且并不存在一個明确的、通用的、公認的權限管理模型。

Ferraiolo 和 Kuhn 兩位大佬于 1992 年提出了一種基于通用角色的通路控制模型(看來這個模型比松哥年齡還大),首次提出了 RBAC 權限模型用來代替傳統的 MAC 和 DAC 兩種權限控制方案,并且就 RBAC 中的相關概念給出了解釋。

Ferraiolo,Cugini 和 Kuhn 于 1995 年擴充了 1992 年提出的權限模型。該模型的主要功能是所有通路都是通過角色進行的,而角色本質上是權限的集合,并且所有使用者隻能通過角色獲得權限。在組織内,角色相對穩定,而使用者和權限都很多,并且可能會迅速變化。是以,通過角色控制權限可以簡化通路控制的管理和檢查。

到了 1996 年,Sandhu,Coyne,Feinstein 和 Youman 正式提出了 RBAC 模型,該模型以子產品化方式細化了 RBAC,并提出了基于該理論的 RBAC0-RBAC3 四種不同模型。

今天,大多數資訊技術供應商已将 RBAC 納入其産品線,除了正常的企業級應用,RBAC 也廣泛應用在醫療、國防等領域。

目前網上關于 RBAC 理論性的東西松哥隻找到英文的,感興趣的小夥伴可以看下,位址是:

  • https://csrc.nist.gov/projects/Role-Based-Access-Control

如果小夥伴們有中文的資料連結,歡迎留言說明。

1.3 RBAC 三原則

  1. 最小權限:給角色配置的權限是其完成任務所需要的最小權限集合。
  2. 責任分離:通過互相獨立互斥的角色來共同完成任務。
  3. 資料抽象:通過權限的抽象來展現,RBAC 支援的資料抽象程度與 RBAC 的實作細節有關。

1.4 RBAC 模型分類

說到 RBAC,我們就得從它的模型分類開始看起。

1.4.1 RBAC0

RBAC0 是最簡單的使用者、角色、權限模型。RBAC0 是 RBAC 權限模型中最核心的一部分,後面其他模型都是在此基礎上建立。

TienChin 項目中的 RBAC 是怎麼玩的?

圖檔源自網絡

在 RBAC0 中,一個使用者可以具備多個角色,一個角色可以具備多個權限,最終使用者所具備的權限是使用者所具備的角色的權限并集。

1.4.2 RBAC1

RBAC1 則是在 RABC0 的基礎上引入了角色繼承,讓角色有了上下級關系。

TienChin 項目中的 RBAC 是怎麼玩的?

圖檔源自網絡

在本系列前面的文章中,松哥也曾多次向大家介紹過 Spring Security 中的角色繼承。

1.4.3 RBAC2

RBAC2 也是在 RBAC0 的基礎上進行擴充,引入了靜态職責分離和動态職責分離。

TienChin 項目中的 RBAC 是怎麼玩的?

圖檔源自網絡

要了解職責分離,我們得先明白角色互斥。

在實際項目中,有一些角色是互斥的,對立的,例如财務這個角色一般是不能和其他角色兼任的,否則自己報賬自己審批,豈不是爽歪歪!

通過職責分離可以解決這個問題:

靜态職責分離

在設定階段就做好了限制。比如同一使用者不能授予互斥的角色,使用者隻能有有限個角色,使用者獲得進階權限之前要有低級權限等等。

動态職責分離

在運作階段進行限制。比如運作時同一使用者下5個角色中隻能同時有2個角色激活等等。

1.4.4 RBAC3

将 RBAC1 和 RBAC2 結合起來,就形成了 RBAC3。

TienChin 項目中的 RBAC 是怎麼玩的?

圖檔源自網絡

1.5 擴充

我們日常見到的很多權限模型都是在 RBAC 的基礎上擴充出來的。

例如在有的系統中我們可以見到使用者組的概念,就是将使用者分組,使用者同時具備自身的角色以及分組的角色。

我們 TienChin 項目所用的腳手架中的權限,就基本上是按照 RBAC 這套權限模型來的。

2. 表設計

我們來看下 RuoYi-Vue 腳手架中跟使用者、角色以及權限相關的表。

這裡主要涉及到如下幾張表:

  • sys_user

    :這個是使用者表。
  • sys_role

    :這個是角色表。
  • sys_user_role

    :這個是使用者角色關聯表。
  • sys_menu

    :這個是菜單表,也可以了解為是資源表。
  • sys_role_menu

    :這個是資源角色關聯表。

通過使用者的 id,可以去

sys_user_role

表中查詢到這個使用者具備的角色 id,再根據角色 id,去

sys_role_menu

表中查詢到這個角色可以操作的資源 id,再根據資源 id,去

sys_menu

表中查詢到對應的資源,基本上就是這個樣一個流程。

那麼 Java 代碼中該怎麼做呢?

3. 代碼實作

首先定義了一個 Java 類 SysUser,這個跟資料庫中的

sys_user

表是對應的,我們來看

UserDetailsService

的具體實作:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysPermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser user = userService.selectUserByUserName(username);
        if (StringUtils.isNull(user)) {
            log.info("登入使用者:{} 不存在.", username);
            throw new ServiceException("登入使用者:" + username + " 不存在");
        } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
            log.info("登入使用者:{} 已被删除.", username);
            throw new ServiceException("對不起,您的賬号:" + username + " 已被删除");
        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
            log.info("登入使用者:{} 已被停用.", username);
            throw new ServiceException("對不起,您的賬号:" + username + " 已停用");
        }

        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user) {
        return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
    }
}
           

複制

從資料庫中查詢到的就是 SysUser 對象,然後對該對象稍作改造,将之改造成為一個 LoginUser 對象,這個 LoginUser 則是 UserDetails 接口的實作類,裡邊儲存了目前登入使用者的關鍵資訊。

在建立 LoginUser 對象的時候,有一個 permissionService.getMenuPermission 方法用來查詢使用者的權限,根據目前使用者的 id,查詢到使用者的角色,再根據使用者角色,查詢到使用者的權限,另外,如果目前使用者的角色是 admin,那麼就設定使用者角色為

*:*:*

,這是一段寫死。

我們再來看看 LoginUser 的設計:

public class LoginUser implements UserDetails {
    /**
     * 權限清單
     */
    private Set<String> permissions;

    /**
     * 使用者資訊
     */
    private SysUser user;
    public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions) {
        this.userId = userId;
        this.deptId = deptId;
        this.user = user;
        this.permissions = permissions;
    }

    @JSONField(serialize = false)
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }
    /**
     * 賬戶是否未過期,過期無法驗證
     */
    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    /**
     * 指定使用者是否解鎖,鎖定的使用者無法進行身份驗證
     *
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    /**
     * 訓示是否已過期的使用者的憑據(密碼),過期的憑據防止認證
     *
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    /**
     * 是否可用 ,禁用的使用者不能身份驗證
     *
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isEnabled() {
        return true;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }
}
           

複制

有一些屬性我省略掉了,大家可以文末下載下傳源碼檢視。

小夥伴們看到,這個 LoginUser 實作了 UserDetails 接口,但是和 vhr 中有一個很大的不同,就是這裡沒有處理 getAuthorities 方法,也就是說當系統想要去擷取使用者權限的時候,二話不說直接傳回一個 null。這是咋回事呢?

因為在這個腳手架中,将來進行權限校驗的時候,是按照下面這樣來的:

@PreAuthorize("@ss.hasPermi('system:menu:add')")
@PostMapping
public AjaxResult add(@Validated @RequestBody SysMenu menu) {
    //省略
}
           

複制

@PreAuthorize 注解中的

@ss.hasPermi('system:menu:add')

表達式,表示調用 Spring 容器中一個名為 ss 的 Bean 的 hasPermi 方法,去判斷目前使用者是否具備一個名為

system:menu:add

的權限。一個名為 ss 的 Bean 的 hasPermi 方法如下:

@Service("ss")
public class PermissionService {
    /**
     * 所有權限辨別
     */
    private static final String ALL_PERMISSION = "*:*:*";

    /**
     * 管理者角色權限辨別
     */
    private static final String SUPER_ADMIN = "admin";

    private static final String ROLE_DELIMETER = ",";

    private static final String PERMISSION_DELIMETER = ",";

    /**
     * 驗證使用者是否具備某權限
     *
     * @param permission 權限字元串
     * @return 使用者是否具備某權限
     */
    public boolean hasPermi(String permission) {
        if (StringUtils.isEmpty(permission)) {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {
            return false;
        }
        return hasPermissions(loginUser.getPermissions(), permission);
    }
    /**
     * 判斷是否包含權限
     *
     * @param permissions 權限清單
     * @param permission  權限字元串
     * @return 使用者是否具備某權限
     */
    private boolean hasPermissions(Set<String> permissions, String permission) {
        return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
    }
}
           

複制

由于這裡是純手工操作,在比較的時候,直接擷取到目前登入使用者對象 LoginUser,再手動調用他的 hasPermissions 方法去判斷權限是否滿足,由于都是自定義操作,是以是否實作 UserDetails#getAuthorities 方法已經不重要了,不過按照這裡的比對方案,是不支援通配符的比對的。

例如使用者具備針對字典表的所有操作權限,表示為

system:dict:*

,但是當和

system:dict:list

進行比較的時候,發現比較結果為 false,這塊想要比對成功也是可以的,例如可以通過正規表達式或者其他方式來操作,反正都是字元串比較,相信大家都能自己搞得定。

現在,前端提供操作頁面,也可以配置每一個使用者的角色,也可以配置每一個角色可以操作的權限就行了,這個就比較簡單了,不多說。

好啦,這就是 TienChin 項目中的 RBAC 權限實作方案,後面松哥也會錄制相關的視訊教程,對視訊感興趣的小夥伴戳這裡:TienChin 項目配套視訊來啦。