一、数据权限设计初衷
近期在做数据中台的项目,主要包括元数据管理,质量规则,数据服务,数据导出等多个业务模块,目前该中台只是供内部使用,但随着业务的正式上线,必然会在公网进行访问。因此,如何控制每个用户只能访问自己能够看到的数据内容至关重要,这就存在用户数据权限的设计问题。
二、前提条件
(1)满足不同的业务授权方式统一化,通用且方便扩展;
(2)与业务模块完全解耦,减少代码的侵入,提高权限系统的复用性;
(3)业务方不需要关心授权流程及授权资源回显问题。要保证不同的资源类型,在授权时,能够自动从后台获取需要授权的资源列表信息进行授权。
(4)授权系统易用,可维护性高。
三、数据权限设计整体架构
在这里插入图片描述
本次设计是基于用户-角色-资源体系构建的通用数据权限系统。通过将角色与资源绑定,再将用户和角色绑定,从而达到用户与资源的关联关系。
在资源模块中,提供统一资源授权接口和统一资源鉴权接口。统一资源授权接口主要是用来提供不同资源在授权时web页面回显的问题,后台主要是通过反射的方式来实现的。统一资源鉴权接口主要是提供gaia用户的鉴权操作,通过对用户所拥有的资源类型和角色的判断,从而给中台返回该用户能够访问的资源id集合,中台返回具体的资源内容信息。
四、数据权限核心代码实现
4.1依赖springboot对bean的管理,达到接口实现的自动扫描操作。

public abstract class ClassUtil<T> implements InitializingBean, ApplicationContextAware {

    protected ApplicationContext applicationContext;

    private final Map<String, T> map = new HashMap<>();

    /**
     * 设置spring上下文
     * @param applicationContext spring上下文
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        setImplementedBeanMap(map, Objects.nonNull(getTypeClass()) ? getTypeClass() : getTClass());
    }

    /**
     * 设置接口实现类Map
     * @param map 存放接口实现类Map
     * @param type 接口类
     */
    protected <E> void setImplementedBeanMap(Map<String, E> map, Class<E> type) {
        Map<String, E> beanMap = applicationContext.getBeansOfType(type);
        for (E storageType : beanMap.values()) {
            map.put(storageType.getClass().getSimpleName(), storageType);
        }
    }

    /**
     * 获取实现类集合
     */
    protected Map<String, T> getUseMap() {
        return map;
    }

    @SuppressWarnings("unchecked")
    private Class<T> getTClass() {
        return (Class<T>)((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }

    /**
     * 获取接口类Class
     */
    protected abstract Class<T> getTypeClass();

}

4.2 统一授权接口实现类

public interface PrivilegeResourceTemplate {

    Pagination<PrivilegeResourceEntity> showResourceList(String className, String keyword,  Integer pageNum, Integer pageSize);
}

前端调用统一接口

@Service
public class PrivilegeBaseServiceImpl extends ClassUtil<PrivilegeResourceTemplate> implements PrivilegeBaseService {
    @Override
    protected Class<PrivilegeResourceTemplate> getTypeClass() {
        return PrivilegeResourceTemplate.class;
    }


    /**
     * 授权资源列表
     *   统一授权资源展示接口
     * @param className 具体实现类
     * @param keyword   关键字
     * @param pageNum   分页参数
     * @param pageSize  分页参数
     * @return
     */
    @Override
    public Pagination<PrivilegeResourceEntity> resourceLists(String className, String keyword, Integer pageNum, Integer pageSize) {
        Map<String, PrivilegeResourceTemplate> map = getUseMap();
        return map.get(className).showResourceList(className, keyword, pageNum, pageSize);
    }


    /**
     * 获取实现类列表
     *   通过该方法,可以获取接口的具体实现类有哪些
     * @return
     */
    @Override
    public Set<String> getClassImpl() {
        Map<String, PrivilegeResourceTemplate> map = getUseMap();
        Set<String> classImpls = map.keySet();
        return classImpls;
    }

}

4.3统一鉴权接口,用到了redis,如果不需要,可以去掉。

public interface PrivilegeCommonAuthorize {

    List<Object> getUserRefResourceIds(String resourceType);

}
@Service
public class PrivilegeCommonAuthorizeImpl implements PrivilegeCommonAuthorize {

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private PrivilegeRoleRefUserMapper privilegeRoleRefUserMapper;

    @Override
    public List<Object> getUserRefResourceIds(String resourceType) {
        if (StringUtils.isEmpty(resourceType)) {
            return null;
        }
        String employeeNo = "涉及业务,已去掉,有需要自己添加逻辑";
        String key = CommonConstants.USER_KEY + employeeNo;

        Set<Object> roleIds = redisUtil.sGet(key);

        /**
         * 如果redis中没有,则去数据库查询,并存入redis
         */
        if (roleIds.isEmpty()) {
            roleIds = privilegeRoleRefUserMapper.queryRoleRefUserByUserId(employeeNo);
        }

        if (!roleIds.isEmpty()) {
            redisUtil.sSet(key, roleIds.toArray());
        } else {
            throw new CustomException("该用户未被授权,请先授权后再进行操作");
        }

        Set<String> roleIdsSet = new HashSet<>();
        roleIds.forEach(roleId -> {
            String str = resourceType + CommonConstants.SPLITOR + roleId;
            String roleKey = CommonConstants.ROLE_KEY + Base64.getEncoder().encodeToString(str.getBytes(StandardCharsets.UTF_8));
            roleIdsSet.add(roleKey);
        });
        return redisUtil.multiGet(roleIdsSet);
    }
}

五、数据权限实现流程
新增资源时的配置页面:
在这里插入图片描述
5.1 开发人员在开发完成之后,需要在该页面配置资源类型和该资源的具体实现类(后台实现PrivilegeResourceTemplate 接口的具体实现类)。
5.2 在授权时,前端选择资源类型,后台接收到资源类型的值,从数据库查询该资源类型具体的实现类,然后通过反射,获取该资源类型的授权资源内容回显在web页面进行授权。
5.3 鉴权时,用户登录中台,通过网关获取到用户的信息,然后从redis(mysql)中查询该用户(对应的资源类型所拥有)的角色信息,然后通过角色信息,查询该角色下挂载的资源内容(资源id),返回给中台具体接口进行回显拦截,从而达到权限控制的目的。
六、数据库表结构设计
1、用户表
在这里插入图片描述

CREATE TABLE `privilege_user` (
  `privilege_user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_name` varchar(16) DEFAULT NULL COMMENT '用户名',
  `employ_id` varchar(10) DEFAULT NULL COMMENT '员工编码',
  `account` varchar(12) DEFAULT NULL COMMENT 'gaia账号',
  `business_descr` varchar(32) DEFAULT NULL COMMENT '所属公司',
  `creator` varchar(16) DEFAULT NULL COMMENT '创建人姓名',
  `user_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '用户状态,0标识正常,1表示关闭',
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`privilege_user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4

2、角色表
在这里插入图片描述

CREATE TABLE `privilege_role` (
  `privilege_role_id` varchar(18) NOT NULL COMMENT '主键,角色id',
  `role_name` varchar(32) DEFAULT NULL COMMENT '角色名',
  `role_descr` varchar(256) DEFAULT NULL COMMENT '角色描述',
  `added_by_name` varchar(16) DEFAULT NULL COMMENT '创建人姓名',
  `role_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '角色状态,0表示正常,1表示关闭',
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`privilege_role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

3、角色用户关联表
在这里插入图片描述

CREATE TABLE `privilege_role_ref_user` (
  `privilege_role_ref_user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `role_id` varchar(18) DEFAULT NULL COMMENT '角色id',
  `user_id` varchar(10) DEFAULT NULL COMMENT '用户id',
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`privilege_role_ref_user_id`),
  KEY `index_name` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4

4、角色和资源关联表
在这里插入图片描述

CREATE TABLE `privilege_role_ref_resource` (
  `privilege_role_ref_resource_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `role_id` varchar(18) DEFAULT NULL COMMENT '角色id',
  `resource_id` bigint(20) DEFAULT NULL COMMENT '资源模型id',
  `resource_type` varchar(32) DEFAULT NULL COMMENT '资源类型',
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`privilege_role_ref_resource_id`),
  KEY `index_name` (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4

5、资源配置表
在这里插入图片描述

CREATE TABLE `privilege_resource_class` (
  `resource_reflex_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `resource_type` varchar(32) DEFAULT NULL COMMENT '资源类型',
  `resource_name` varchar(255) DEFAULT NULL COMMENT '资源名',
  `class_name` varchar(128) DEFAULT NULL COMMENT '类名',
  `creator` varchar(16) DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`resource_reflex_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4

七、总结
1、该数据权限设计系统,完全解耦业务代码,通用性强,可扩展。
2、表结构清晰,模块明朗,容易维护;
3、设计简单,方便使用。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐