大家好,在上一篇文章中我们详细介绍了在RBAC模型中如何集成数据权限,本篇文章我们将通过实际案例,从代码实战的角度来实现这样的一个数据权限。
在开始阅读本文之前,建议先把上篇文章 读一遍,读一遍,读一遍!
[数据权限就该这么设计,yyds!]
上篇文章的数据模型是基于传统的RBAC模型来设计的,由于我们这里的应用场景不一样,所以这里的数据权限模型并没有严格按照上篇文章的方案来设计,但是万变不离其宗,核心原理还是相同的。
首先我来介绍一下我们最终实现的效果
一个组件(可以理解成菜单)可以绑定多个授权维度,当给角色授权组件时可以给这个授权组件赋予不同维度的权限。
关于数据权限的授权维度有以下几个关键点需要仔细体会:
如果一个角色勾选了客户群的全部 + A产品线,那么最终生成的sql 会是 where 产品线 in ('A产品线')
3. 如果一个角色勾选了多个维度,维度之间用 AND 拼接
如果一个角色勾选了A客户群 + B产品线,那么最终生成的sql 会是 where 客户群 in('A客户群')AND 产品线 in ('B产品线')
4. 一个用户可能有多个角色,角色之间用 OR 拼接
一个用户有两个角色:客户群总监,产品线经理。其中客户群总监角色拥有A客户群和B客户群的权限,产品线经理角色拥有A产品线权限,那么最终生成的sql会是 where 客户群 in ('A客户群','B客户群') OR 产品线 in ('A产品线')
由于我们业务场景中数据规则比较单一,全部使用
in
作为sql条件连接符,你们可以根据实际业务场景进行补充。
最终的数据模型如下所示:
这里的组件大家完全可以理解成RBAC模型中的资源、菜单,只不过叫法不同而已。
下面是具体的表结构设计
CREATE TABLE `wb_dimension` (
`ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键',
`DIMENSION_CODE` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '维度编码',
`DIMENSION_NAME` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '维度名称',
PRIMARY KEY (`ID`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='授权维度'
CREATE TABLE `wb_dimension_proc_line` (
`ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键',
`DIMENSION_CODE` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '维度编码',
`PROC_LINE_CODE` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '产品线编码',
`PROC_LINE_NAME` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '产品线名称',
PRIMARY KEY (`ID`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='授权维度-产品线'
跟授权维度表实际是一个表继承的关系,由于每个授权维度的属性不一样,展现形式也不一样,所以分表存储。
CREATE TABLE `wb_route` (
`ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键ID',
`COMPONENT_ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '组件ID',
`ROUTE_URL` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '路由地址',
`AUTHORIZATION_TYPE` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '授权方式:1 自定义,2 上下级授权, 3 范围授权',
`AUTHORIZATION_DIMENSION` json DEFAULT NULL COMMENT '授权维度',
PRIMARY KEY (`ID`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='组件路由'
当组件属性授权方式为范围授权时在应用侧会强制要求选择具体的授权维度,如 产品线、客户群。
CREATE TABLE `wb_role` (
`ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键ID',
`ROLE_CODE` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '角色CODE',
`ROLE_NAME` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '角色名称',
`IDENTITY_ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '身份ID'
PRIMARY KEY (`ID`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='角色表'
角色上有一个身份属性,多个角色可以归属同一个身份,方便对角色进行分类管理。
CREATE TABLE `role_component_relation` (
`ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键ID',
`ROLE_ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色ID',
`COMPONENT_ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '组件ID',
PRIMARY KEY (`ID`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='角色授权组件'
CREATE TABLE `wb_role_component_rule` (
`ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键',
`ROLE_ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '角色ID',
`COMPONENT_ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '组件ID',
`RULE_CODE` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '规则编码',
`RULE_NAME` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '规则名称',
`RULE_CONDITION` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '规则条件',
`RULE_VALUE` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '规则值',
PRIMARY KEY (`ID`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='角色组件维度规则表'
数据权限的核心表,规则条件的取值为IN,规则值存储具体的维度编码,当在数据维度中选择 全部 时我们将规则值存储为ALL这个特殊值,方便后续生成SQL语句。
上篇文章中提到过数据权限的实现过程,这里再回顾一下
DataPermission
@DataPermission
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
@Documented
public @interface DataPermission {
/**
* 数据权限类型
* 1 上下级授权 2 数据范围授权
*/
String permissionType() default "2";
/**
* 配置菜单的组件路径,用于数据权限
*/
String componentRoute() default "";
}
@Aspect
@Slf4j
public class DataPermissionAspect {
@Autowired
private RoleComponentRuleService roleComponentRuleService;
@Pointcut("@annotation(com.ifly.workbench.security.annotation.DataPermission)")
public void pointCut() {
}
@Around("pointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable{
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
//获取请求token
String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
String userName = JwtUtil.getUsername(token);
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DataPermission permissionData = method.getAnnotation(DataPermission.class);
//获取授权方式
String permissionType = permissionData.permissionType();
//获取组件路由
String componentRoute = permissionData.componentRoute();
if (StringUtils.isNotEmpty(componentRoute)){
// 查找当前用户此组件下的所有规则
List<RoleComponentRuleDTO> componentRules = roleComponentRuleService.getRoleComponentRule(userName, componentRoute);
if(CollectionUtils.isNotEmpty(componentRules)){
DataPermissionUtils.installDataSearchConditon(request, componentRules);
SysUserCacheInfo userInfo = buildCacheUser(userName);
DataPermissionUtils.installUserInfo(request, userInfo);
}
}
return point.proceed();
}
private SysUserCacheInfo buildCacheUser(String userName) {
SysUserCacheInfo info = new SysUserCacheInfo();
info.setSysUserName(userName);
info.setOneDepart(true);
return info;
}
}
在AOP中获取当前用户、需要访问的组件中所有的数据规则,参考wb_role_component_rule
表设计,并将其放到Request作用域中。
public class DataPermissionUtils {
public static final String COMPONENT_DATA_RULES = "COMPONENT_DATA_RULES";
public static final String SYS_USER_INFO = "SYS_USER_INFO";
/**
* 往链接请求里面,传入数据查询条件
* @param request
* @param componentRules
*/
public static void installDataSearchConditon(HttpServletRequest request, List<RoleComponentRuleDTO> componentRules) {
// 1.先从request获取MENU_DATA_AUTHOR_RULES,如果存则获取到LIST
List<RoleComponentRuleDTO> list = loadDataSearchCondition();
if (list==null) {
// 2.如果不存在,则new一个list
list = Lists.newArrayList();
}
list.addAll(componentRules);
// 3.往list里面增量存指
request.setAttribute(COMPONENT_DATA_RULES, list);
}
/**
* 获取请求对应的数据权限规则
*
*/
@SuppressWarnings("unchecked")
public synchronized List<RoleComponentRuleDTO> loadDataSearchCondition() {
return (List<RoleComponentRuleDTO>) SpringContextUtils.getHttpServletRequest().getAttribute(COMPONENT_DATA_RULES);
}
public synchronized void installUserInfo(HttpServletRequest request, SysUserCacheInfo userinfo) {
request.setAttribute(SYS_USER_INFO, userinfo);
}
}
在Request中存储数据规则。
public interface RoleComponentRuleService extends IService<RoleComponentRule> {
/**
* 根据 用户域账户和组件编码 获取组件对应的关系
*
* @param userName 域账号
* @param componentCode 组件编码
* @return 用户的所有规则
*/
List<RoleComponentRuleDTO> getRoleComponentRule(String userName, String componentCode);
}
@Service
public class RoleComponentRuleServiceImpl extends ServiceImpl<RoleComponentRuleMapper, RoleComponentRule> implements RoleComponentRuleService {
@Resource
private RoleComponentRuleMapper roleComponentRuleMapper;
/**
* 根据 用户域账户和组件编码 获取组件对应的关系
* @param userName 域账号
* @param componentCode 组件编码
* @return 用户的所有规则
*/
@Override
public List<RoleComponentRuleDTO> getRoleComponentRule(String userName, String componentCode) {
return roleComponentRuleMapper.getRoleComponentRule(userName,componentCode);
}
}
@Service
public class RoleComponentRuleServiceImpl extends ServiceImpl<RoleComponentRuleMapper, RoleComponentRule> implements RoleComponentRuleService {
@Resource
private RoleComponentRuleMapper roleComponentRuleMapper;
/**
* 根据 用户域账户和组件编码 获取组件对应的关系
* @param userName 域账号
* @param componentCode 组件编码
* @return 用户的所有规则
*/
@Override
public List<RoleComponentRuleDTO> getRoleComponentRule(String userName, String componentCode) {
return roleComponentRuleMapper.getRoleComponentRule(userName,componentCode);
}
}
@ApiOperation(value = "服务BU-领导-总览")
@GetMapping("opp/getLeaderOverviewSve")
@DataPermission(componentRoute = "020202")
public Result<SalesProjOverviewSve> getLeaderOverviewSve(@RequestParam(name = "identityId") String identityId) {
String permissionSql = RuleQueryGenerator.getPermissionSql(identityId);
log.info("查服务BU-领导-总览-permissionSQL==" + permissionSql);
return Result.OK(overviewSveService.getLeaderOverviewSve(permissionSql));
}
在controller的请求方法上加上自定义注解@DataPermission
并指定组件编码,然后通过工具类生成SQL条件,最后将SQL条件传入service层进行处理。
@Slf4j
@UtilityClass
public class RuleQueryGenerator {
private static final String SQL_AND = " and ";
private static final String SQL_OR = " or ";
private static final String SQL_JOINT = " (%s) ";
/**
* 获取带有数据权限的SQL
* @param identityId 身份ID
*/
public String getPermissionSql(String identityId) {
//------------------------获取当前身份的数据规则------------------------------------
List<RoleComponentRuleDTO> conditionList = getCurrentIdentyPermission(identityId);
if (CollectionUtils.isEmpty(conditionList)) {
//没有权限
return "1 = 0";
}
//存在权限
//对当前身份根据规则编码分组-去除不同角色中相同编码且规则值为ALL的规则 并根据角色id分组
Map<String, List<RoleComponentRuleDTO>> ruleMap = getRuleMapByRoleId(conditionList);
StringBuilder sb = new StringBuilder();
String roleSql;
if (MapUtils.isNotEmpty(ruleMap)) {
//按角色拼接SQL
for (Map.Entry<String, List<RoleComponentRuleDTO>> entry : ruleMap.entrySet()) {
List<RoleComponentRuleDTO> lists = entry.getValue();
// 同角色之间使用 AND
roleSql = buildRoleSql(lists);
//角色之间使用 OR
if (StringUtils.isNotEmpty(roleSql)) {
jointSqlByRoles(sb, roleSql);
}
}
}
return sb.toString();
}
private static List<RoleComponentRuleDTO> getCurrentIdentyPermission(String identityId) {
//----------------------------获取所有数据规则-----------------------------
List<RoleComponentRuleDTO> roleRuleList = DataPermissionUtils.loadDataSearchCondition();
if(CollectionUtils.isEmpty(roleRuleList)){
return null;
}
//-----------------------------过滤掉不属于当前身份的规则-----------------------------------
return roleRuleList.stream()
.filter(item -> item.getIdentityId().equals(identityId))
.collect(Collectors.toList());
}
/**
* 构建单角色SQL
*/
private static String buildRoleSql(List<RoleComponentRuleDTO> lists) {
StringBuilder roleSql = new StringBuilder();
for (RoleComponentRuleDTO item : lists) {
//如果出现全选 则 代表全部,不需要限定范围
if ("ALL".equals(item.getRuleValue())) {
continue;
}
//将规则转换成SQL
String filedSql = convertRuleToSql(item);
roleSql.append(SQL_AND).append(filedSql);
}
return roleSql.toString();
}
/**
* 将单一规则转化成SQL,默认全部使用 In
* ruleCode : area_test
* ruleValue : 区域1,区域2,区域3
* @param rule 规则值
*/
private static String convertRuleToSql(RoleComponentRuleDTO rule) {
String whereCondition = " in ";
String ruleValueConvert = getInConditionValue(rule.getRuleValue());
return rule.getRuleCode() + whereCondition + ruleValueConvert;
}
/**
* IN字符串转换
* 区域1, 区域2, 区域3 --> ("区域1","区域2","区域3")
* 江西大区 --> ("江西大区")
*/
private static String getInConditionValue(String ruleValue) {
String[] temp = ruleValue.split(",");
StringBuilder res = new StringBuilder();
for (String string : temp) {
res.append(",'").append(string).append("'");
}
return "(" + res.substring(1) + ")";
}
/**
* 拼接单角色的SQL
* @param sqlBuilder 总的SQL
* @param roleSql 单角色SQL
*/
private static void jointSqlByRoles(StringBuilder sqlBuilder, String roleSql) {
roleSql = roleSql.replaceFirst(SQL_AND, "");
if (StringUtils.isEmpty(sqlBuilder.toString())) {
sqlBuilder.append(String.format(SQL_JOINT, roleSql));
} else {
sqlBuilder.append(SQL_OR).append(String.format(SQL_JOINT, roleSql));
}
}
/**
*
* 1. 对当前身份根据规则编码分组-去除不同角色中相同编码且规则值为ALL的规则
* 2. 对角色进行分组
* @param conditionList 数据规则
* @return 分组后的规则list
*/
private static Map<String, List<RoleComponentRuleDTO>> getRuleMapByRoleId(List<RoleComponentRuleDTO> conditionList) {
//--------过滤掉不属于当前身份的规则,并对条件编码进行分组-----------------------------------
Map<String, List<RoleComponentRuleDTO>> conditionMap = conditionList.stream().collect(Collectors.groupingBy(RoleComponentRuleDTO::getRuleCode));
//--------相同编码分组中存在ALL的排除掉-----------------------------------------------
List<RoleComponentRuleDTO> newRoleRuleList = new ArrayList<>();
if (MapUtils.isNotEmpty(conditionMap)) {
for (Map.Entry<String, List<RoleComponentRuleDTO>> entry : conditionMap.entrySet()) {
boolean flag = true;
List<RoleComponentRuleDTO> lists = entry.getValue();
for (RoleComponentRuleDTO item : lists) {
if ("ALL".equals(item.getRuleValue())) {
flag = false;
break;
}
}
if (flag) {
newRoleRuleList.addAll(lists);
}
}
}
if (CollectionUtils.isNotEmpty(newRoleRuleList)) {
return newRoleRuleList.stream().collect(Collectors.groupingBy(RoleComponentRuleDTO::getRoleId));
}
return Maps.newHashMap();
}
}
核心类,用于生成数据权限查询的SQL脚本。
<select id="getLeaderOverviewSve" resultType="com.ifly.center.entity.SalesProjOverviewSve">
SELECT <include refid="column_list"/> FROM U_STD_ADS.LTC_SALES_PROJ_OVERVIEW_SVE
<where>
<if test="permissionSql != null and permissionSql != ''">
${permissionSql}
</if>
</where>
</select>
Dao层接受service层传入已经生成好的sql语句,作为查询条件直接拼接在业务语句之后。
以上,就是数据权限的实现过程,其实代码实现并不复杂,主要还是得理解其中的实现原理。如果你也有数据权限的需求,不妨参考一下。当然如果你有更好的实现方案,也可以留言告诉我。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/jFceD2K-ccIiA1wvxZNUyw
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。