后台管理系统 – 权限管理

不管是开发手机APP,网站还是小程序等项目,基本上都需要一个后台管理系统的支撑。而每个后台管理系统都有一个通用的功能就是用户权限管理。最近基于Antd pro+React.js做了一个后台管理系统。


一、概要

权限管理是一个几乎所有网站的都会涉及的一个重要组成部分,主要目的是对整个系统进行权限的控制,包括API权限和功能权限。避免因权限控制缺失或操作不当引发的系统风险问题,如操作错误,数据泄露等问题。

就目前来说使用最广泛的是一个账号对应多个角色,每个角色对应相应的权限集(RBAC模型)这种模型基本可以应对所有的问题,且通过角色可以实现灵活且多样的的权限操作需求。

权限管理中涉及到三个重要的名词:账号、角色、权限。

  • 账号任何用户想要进入系统需要分配一个账号,而这个账号就是一把钥匙。我们通过控制账号所具备的权限,进而控制这个用户的授权范围
  • 角色角色管理是确定角色具备哪些权限的一个过程,他是一个集合的概念,是众多最小权限颗粒的组成。我们通过把权限给这个角色,再把角色给账号,从而实现账号的权限,因此它承担了一个桥梁的作用。
  • 权限权限包括页面权限,操作权限,数据权限。页面权限控制你可以看到哪个页面,看不到哪个页面;操作权限则控制你可以在页面上操作哪些按钮;数据权限则是控制你可以看到哪些数据。

二、思考

了解过很多网上给出的权限管理方案,绝大部分解决方案都是事先定义好角色和各个页面权限的对应关系,这样做有一个缺点就是无法灵活配置。比如要调整一个角色的权限,那系统需要从代码层面做相应的修改。所以在想有没有一种解决方案可以把权限的颗粒度划分的更细,通过洁面实现角色和权限动态配置。

带着这些疑问整理了自己对权限管理的理解和解决方案,本人非专业WEB端开发,只是提出个人见解,有不对的地方希望大家指正。上面也说了权限主要分为页面权限、操作权限和数据权限,我想从这三个方面细化并做成可自由配置。

页面权限控制:页面权限主要通过菜单配置和页面跳转操作来控制。一般进入后台管理系统会呈现出主菜单,我们可以在角色管理中为每一个角色配置不同的菜单以及前端页面的功能标识。用户使用账号登录时,后台通过判断该账号的角色,返回对应的菜单和功能标识,这样就可以控制用户的一级页面的权限和跳转到二级页面的入口,从而实现控制页面权限。

操作权限控制:操作权限主要控制用户哪些按钮可以点击,哪些按钮不可以点击。我们可以在管理后台动态为每个页面定义功能标识,在获取用户菜单时把该角色的功能标识一并返回,通过检查当前用户是有有这个标识来判断是否有相应的操作权限。

数据权限控制:我理解的数据权限控制是角色有哪些接口调用的权限,防止用户跨权限调用接口,我们需要把接口和功能点关联起来,调用接口时后台检测当前用户的角色是否有调用该接口的权限,如果没有权限则返回失败。

三、详细介绍

以下介绍以下我根据我的想法实现的权限管理功能(项目可能还有需要改进的地方,欢迎各位大神指点O(∩_∩)O~)

1.、工作台预览

以下是目前正在开发系统的整体布局,基于React.js+Antd pro5.0开发,左侧菜单列表可以通过角色控制,不同角色返回的菜单不一样。

2、菜单管理

菜单管理是管理系统中菜单以及每个菜单中都有哪些功能权限和API权限等。

这里需要配置当前菜单(一级菜单可以不配置)下的所有功能标识(定义前端的功能点,比如列表,编辑,查看,删除等权限)以及该功能对应API权限标识(我后台使用的是Spring Security框架,API标识对应@PreAuthorize注解中的标识)。配置API标识时无需手动一个个输入,有后端接口返回所有API表示列表,从过年下拉框中选择即可。

为每个功能点指定API标识后,后端可以控制只有拥有该功能权限的角色才可以调用该API,从而做到数据权限控制。

后端可通过以下代码获取所有已注册的API权限标识,然后返回给前端,

Set<String> authSet = new HashSet<>();

Map<String,Object> beans = applicationContext.getBeansWithAnnotation(Controller.class);
for(Object bean : beans.values()){
    Method[] methods = bean.getClass().getMethods();

    for (Method method : methods) {
        PreAuthorize anno = AnnotationUtils.findAnnotation(method, PreAuthorize.class);

        if (anno != null) {
            String val = anno.value();
            if (val.indexOf("hasAuthority") >= 0 || val.indexOf("hasAnyAuthority") >= 0) {
                val = val.substring(val.indexOf("(") + 1, val.indexOf(")"));
                String[] authArray = val.replace("'", "").split(",");
                for (String auth : authArray) {
                    authSet.add(auth);
                }
            }
        }
    }
}

3.权限管理

权限管理是配置菜单中有哪些功能点以及各个功能点有哪些API权限,跟上面菜单管理中的功能一样,只是提供2个入口配置,通过菜单管理中的权限管理配置入口可以为做到为菜单批量配置权限,这里需要一个一个的添加。




4. 部门管理

管理公司部门,支持分级管理。

5、用户管理

这个没有什么好说的,就是管理用户信息的地方。这里其实有2个可以优化的地方,一个用户应该可以对应多个角色,以及一个用户可以对应多个部门,这一块我简化了一下,每一个用户只有一个角色,每个用户只能对应一个部门。

6.角色管理

角色可根据自身系统业务需求,随意增删改查,无需事先定死角色类型,每个角色后面有一个权限管理入口,在那个页面给么给角色动态分配权限。

这里会列出当前菜单拥有的所有功能权限,可以给当前角色赋予指定权限

由此后端的权限配置就完成了

7.前端菜单展示和权限控制

用户登录以后,后端验证用户,根据用户的角色获取对应菜单信息和功能权限列表返回给前端,

菜单列表格式如下:

功能权限列表格式如下:

修改前端的routes.ts文件,设置菜单的access值为上图对应的功能标识。

修改app.tsx和access.ts文件,配置后端返回的功能权限列表以及用户菜单。

app.tsx:

export async function getInitialState(): Promise<{
    settings?: Partial<LayoutSettings>;
    currentUser?: CurrentUserInfo;
    config?: ConfigInfo;
    access?:any;
    menuData?:MenuNavItem[];
    fetchUserInfo?: () => Promise<BizInfo<CurrentUserInfo>>;
    fetchUserPerms?: () => Promise<BizInfo<any>>;
    fetchUserNav?: () => Promise<BizInfo<MenuNavItem[]>>;
    getAccess?:(perms: any) => any;
  }> {
    const fetchUserInfo = async () => {
      //调用接口获取用户信息,代码省略。。。
    };
  
    const fetchUserPerms = async () => {
      //调用接口获取用户功能权限,代码省略。。。
    };
  
    const fetchUserNav = async () => {
      //调用接口获取用户菜单,代码省略。。。
    };
  
    const getAccess = (perms: any) => {
      let defaultData = Object.create(null);
      initDefaultAccess(defaultData, routes);
    
      let access = {...defaultData, ...perms};
  
      if (!access) {
        access = Object.create(null);
      }
      return access;
     }
  
  
    let config = undefined;
    const configData = await getConfigRequest();
    if (configData.state === ErrorCode.SUCCESS) {
      config = configData.data;
    }
    
    // 如果是登录页面,不执行
    if (history.location.pathname !== loginPath) {
      const currentUser = await fetchUserInfo();
      const perms = await fetchUserPerms();
      const menus = await fetchUserNav();
      return {
        fetchUserInfo:getCurrentUserRequest,
        fetchUserPerms:getSelfPermRequest,
        fetchUserNav: getSelfNavRequest,
        getAccess: getAccess,
        currentUser,
        config:config,
        settings: {},
        access:getAccess(perms),
        menuData:menus,
      };
    }
    return {
      fetchUserInfo:getCurrentUserRequest,
      fetchUserPerms:getSelfPermRequest,
      fetchUserNav: getSelfNavRequest,
      getAccess:getAccess,
      config:config,
      settings: {},
    };
  }

access.ts:

export default function access(initialState: { access?: any }) {
  const { access } = initialState || {};
  
  if (!access) {
    let defaultData = Object.create(null);
    return defaultData;
  }
  
  return access;
}

在页面中可以通过以下代码获取该角色是否有指定功能权限

canAccess('SYS_MENU_ADD', access);

如上面的代码,如果权限接口返回的功能权限标识中SYS_MENU_ADD=true,则该canAccess函数放回true,由此可以控制页面中是否显示指定按钮或内容块。

四、总结

以上是个人总结的一套权限管理解决方案,优点是可以通过管理后台界面对角色权限动态配置,无需修改代码。大家如果有什么其他更好的解决方案,希望能多多交流,学习学习。

五、其他

以上哪里写的不对或者有待改进,欢迎大家提意见,谢谢!
转载请注明出处:http://www.luoxudong.com/?p=442

发表评论

您的电子邮箱地址不会被公开。

评论(3)

  • 匿名 2019-09-17 20:12

    功能接口那个感觉可以直接并到添加权限哪里

    • 匿名 @ 匿名 2019-09-19 10:42

      不清楚菜单怎么细节到和按钮关联,然后每个角色都分配一套菜单吗?

  • hao 2019-09-19 10:43

    大佬还在吗?
    不清楚菜单怎么细节到和按钮关联,然后每个角色都分配一套菜单吗?