权限开发手册,数据权限和接口权限配置

简介: 权限开发手册,数据权限和接口权限配置

权限开发手册

一般来说,权限有许多种,我们经常用到的一般有操作权限和数据权限两种。

功能权限

所谓操作权限就是有或者没有做某种操作的权限,具体表现形式就是你看不到某个菜单或按钮,当然也有的是把菜单或按钮灰掉的形式。操作权限一般都在显示界面做一次控制,过滤没有权限的操作菜单或按钮,另外在真正执行操作时,还要进行一次权限检查,确保控制非授权访问。操作权限都是围绕角色来开展的,目的就是要实现操作的角色化。

功能权限控制

在上图所示的ER图关系中,角色(blade_role)菜单表(blade_menu)通过 角色菜单关联表(blade_role_menu)建立多对多联系。

操作权限授权的过程即为给角色授予某一菜单的操作权限,用户与角色通过用户平台扩展表(blade_user_web)建立联系,若用户拥有多个角色,则在平台扩展表的角色字段role_id中多个id用英文逗号分隔。基本关系如图所示:

系统一级菜单代表系统,二极菜单代表功能管理,三级菜单代表操作按钮,如增删改查权限

数据权限

1)、什么是数据权限?

所谓数据权限,就是有或者没有对某些数据的访问权限,具体表现形式就是当某用户有操作权限的时候,但不代表其对所有的数据都有查看或者管理权限。

数据权限有两种表现形式:一种是行权限、另外一种是列权限。


所谓行权限,就是限制用户对某些行的访问权限,比如:只能对本人、本部门、本组织的数据进行访问;也可以是根据数据的范围进行限制,比如:合同额大小来限制用户对数据的访问。

所谓列权限,就是限制用户对某些列的访问权限,比如:某些内容的摘要可以被查阅,但是详细内容就只有VIP用户才能查看。通过数据权限,可以从物理层级限制用户对数据的行或列进行获取。

再比如:同样一个部门经理的角色,看到的数据是不一样的,所以,牵扯到数据二字,就应该不和操作二字等同起来。所以我们是通过职位来解决数据权限的,职位也可以叫岗位,是和数据查看范围有关系的,也就是组织结构里的关系。


所以在设计数据结构的时候,每个有数据范围的数据实体,都需要具备数据拥有人的字段,比如A部门的小b同学,他创建的数据,只能由A部门的部门经理看到,而同样在角色里具有部门经理的B部门的部门经理是不能看到的。所以延伸出来了一个设计思路,根据数据拥有人,圈定查看范围。数据范围的维度有:全部、本集团、本公司、本部门、自己,五个维度,可以满足大部分业务场景。

还有一个维度是自定义维度,可以自定义机构进行设置。这样的设计就达到了数据权限的操作灵活性。


● 系统都离不开权限模块,它是支撑整个系统运行的基础模块。而根据项目类型和需求的不同,权限模块的设计更是大相径庭。但不管怎么变,权限模块从大的方面来说,可以分为三种大的类型:功能权限、接口权限、数据权限。

● 功能权限:也就是我们最熟悉的菜单、按钮权限。可以配置各个角色能看到的菜单、按钮从而从最表层分配好权限

● 接口权限:顾名思义,配置不通角色调用接口的权限。有些敏感接口,是只能有固定的一些角色才能调用,普通角色是不能调用的。这种情况需要有一个明确的系统来控制对应的访问权限

● 数据权限:是大家最为需求也是最广为谈资的一个设计理念。我们需要控制不通的角色、机构人员有查看不通数据范围的权限。如果你动手去设计数据权限,当你去各大平台、百度、谷歌查找设计思路的时候,你会发现很难找到有用的资料,很多设计思路局限性非常大。


详情请参考:https://www.jianshu.com/p/0ab125cf8258

2)、行级别权限

给某一角色授权的数据仅能看到自己所管理的部分数据

3)、列级别权限

某一角色用户只能看到所拥有菜单权限下的部分列字段数据

实现不同人看不同数据,不同人对同一个页面操作不同字段。系统按钮权限和表单权限原来是正控制,只有授权的人才有权限,未授权看不到对应按钮;

数据权限的配置

为解决这一类疑难问题,提供三种方式来实现数据权限。

1.提供代码层配置@DataAuth注解达到脱离数据库的全固定配置方式

● 如果是纯注解配置,那么是不通过数据库的,相当于是离线配置。

● 我们只需要关注column、type、value这三个字段。

● column:需要过滤的数据库字段

● type:数据权限过滤的类型

● value:当数据权限类型为自定义的时候,配置的sql条件语句

1)、所在部门可见

配置DataAuth注解,因为默认字段就是create_dept,所以无需配置column

2)、所在机构及其子集可见

配置DataAuth注解,因为默认字段就是create_dept,所以无需配置column

3)、个人可见

配置DataAuth注解,由于创建人字段为create_user,不是默认,所以需要指定

4)、自定义配置

● 配置DataAuth注解,配置自定义sql

● 在这个配置的sql里我使用里占位符${userId},没错,这么写在底层就可以直接获取到当前登录用户的deptId字段,除此之外我们还可以用更多的参数,比如${deptId}、${roleId}、${tenantId}、${account}、${userName}等等

● 这些参数可以参考BladeUser类,他的所有字段我们都是可以根据占位符来获得的。

2.提供代码层配置@DataAuth注解配置数据权限资源编码来达到依赖数据库的半自动配置方式

这个就需要关联数据库,根据数据权限code码来设置数据权限定义规则。

3.Web可视化全自动动态配置

● 数据权限动态配置需要依赖数据库,所以我们需要前往web端进行配置

● 配置逻辑与纯注解配置一致,其实就是把注解配置拓展,并做成了web可视化

可视化页面开发中,

注解说明

数据权限的核心注解为@DataAuth,它的定义代码如下:

package com.springblade.core.datascope.annotation;
import com.springblade.core.datascope.enums.DataScopeEnum;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface DataAuth {
  /**
   * 资源编号
   */
  String code() default "";
  /**
   * 数据权限对应字段
   */
  String column() default DataScopeConstant.DEFAULT_COLUMN;
  /**
   * 数据权限规则
   */
  DataScopeEnum type() default DataScopeEnum.ALL;
  /**
   * 可见字段
   */
  String field() default "*";
  /**
   * 数据权限规则值域
   */
  String value() default "";
}

● 可以看到,目前的数据权限类型一共有五种,前面四种都是不需要自定义写sql的,只有选择了CUSTOM类型,才需要定义注解的value属性

● 注解默认过滤的字段名为create_dept,如果有修改,则需要定义对应的字段名。

数据权限拦截器配置

/**
 * mybatis 数据权限拦截器
 * @author L.cm, Chill
 */
@Slf4j
@RequiredArgsConstructor
@SuppressWarnings({"rawtypes"})
public class DataScopeInterceptor implements QueryInterceptor {
  private final ConcurrentMap<String, DataAuth> dataAuthMap = new ConcurrentHashMap<>(8);
  private final DataScopeHandler dataScopeHandler;
  private final DataScopeProperties dataScopeProperties;
  @Override
  public void intercept(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    //未启用则放行
    if (!dataScopeProperties.getEnabled()) {
      return;
    }
    //未取到用户则放行
    BladeUser bladeUser = AuthUtil.getUser();
    if (bladeUser == null) {
      return;
    }
    if (SqlCommandType.SELECT != ms.getSqlCommandType() || StatementType.CALLABLE == ms.getStatementType()) {
      return;
    }
    String originalSql = boundSql.getSql();
    //查找注解中包含DataAuth类型的参数
    DataAuth dataAuth = findDataAuthAnnotation(ms);
    //注解为空并且数据权限方法名未匹配到,则放行
    String mapperId = ms.getId();
    String className = mapperId.substring(0, mapperId.lastIndexOf(StringPool.DOT));
    String mapperName = ClassUtil.getShortName(className);
    String methodName = mapperId.substring(mapperId.lastIndexOf(StringPool.DOT) + 1);
    boolean mapperSkip = dataScopeProperties.getMapperKey().stream().noneMatch(methodName::contains)
      || dataScopeProperties.getMapperExclude().stream().anyMatch(mapperName::contains);
    if (dataAuth == null && mapperSkip) {
      return;
    }
    //创建数据权限模型
    DataScopeModel dataScope = new DataScopeModel();
    //若注解不为空,则配置注解项
    if (dataAuth != null) {
      dataScope.setResourceCode(dataAuth.code());
      dataScope.setScopeColumn(dataAuth.column());
      dataScope.setScopeType(dataAuth.type().getType());
      dataScope.setScopeField(dataAuth.field());
      dataScope.setScopeValue(dataAuth.value());
    }
    //获取数据权限规则对应的筛选Sql
    String sqlCondition = dataScopeHandler.sqlCondition(mapperId, dataScope, bladeUser, originalSql);
    if (!StringUtil.isBlank(sqlCondition)) {
      PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
      mpBoundSql.sql(sqlCondition);
    }
  }
  /**
   * 获取数据权限注解信息
   *
   * @param mappedStatement mappedStatement
   * @return DataAuth
   */
  private DataAuth findDataAuthAnnotation(MappedStatement mappedStatement) {
    String id = mappedStatement.getId();
    return dataAuthMap.computeIfAbsent(id, (key) -> {
      String className = key.substring(0, key.lastIndexOf(StringPool.DOT));
      String mapperBean = StringUtil.firstCharToLower(ClassUtil.getShortName(className));
      Object mapper = SpringUtil.getBean(mapperBean);
      String methodName = key.substring(key.lastIndexOf(StringPool.DOT) + 1);
      Class<?>[] interfaces = ClassUtil.getAllInterfaces(mapper);
      for (Class<?> mapperInterface : interfaces) {
        for (Method method : mapperInterface.getDeclaredMethods()) {
          if (methodName.equals(method.getName()) && method.isAnnotationPresent(DataAuth.class)) {
            return method.getAnnotation(DataAuth.class);
          }
        }
      }
      return null;
    });
  }
}

数据权限处理规则

需要注意的是,下面的DataScopeEnum这个判断,如果角色是ADMINISTRATOR的话也是不执行直接返回null的,我就是在这里掉坑,所以还是要看源码了解执行过程找问题。

/**
 * 默认数据权限规则
 * 获取过滤sql
 * @param mapperId    数据查询类
 * @param dataScope   数据权限类
 * @param bladeUser   当前用户信息
 * @param originalSql 原始Sql
 * @author Chill
 */
@RequiredArgsConstructor
public class BladeDataScopeHandler implements DataScopeHandler {
  private final ScopeModelHandler scopeModelHandler;
  @Override
  public String sqlCondition(String mapperId, DataScopeModel dataScope, BladeUser bladeUser, String originalSql) {
    //数据权限资源编号
    String code = dataScope.getResourceCode();
    //根据mapperId从数据库中获取对应模型
    DataScopeModel dataScopeDb = scopeModelHandler.getDataScopeByMapper(mapperId, bladeUser.getRoleId());
    //mapperId配置未取到则从数据库中根据资源编号获取
    if (dataScopeDb == null && StringUtil.isNotBlank(code)) {
      dataScopeDb = scopeModelHandler.getDataScopeByCode(code);
    }
    //未从数据库找到对应配置则采用默认
    dataScope = (dataScopeDb != null) ? dataScopeDb : dataScope;
    //判断数据权限类型并组装对应Sql
    Integer scopeRule = Objects.requireNonNull(dataScope).getScopeType();
    DataScopeEnum scopeTypeEnum = DataScopeEnum.of(scopeRule);
    List<Long> ids = new ArrayList<>();
    String whereSql = "where scope.{} in ({})";
        //需要注意的是,下面的这个判断,如果角色是ADMINISTRATOR的话也是不执行直接返回null的,我就是在这里掉坑
    if (DataScopeEnum.ALL == scopeTypeEnum || StringUtil.containsAny(bladeUser.getRoleName(), RoleConstant.ADMINISTRATOR)) {
      return null;
    } else if (DataScopeEnum.CUSTOM == scopeTypeEnum) {
      whereSql = PlaceholderUtil.getDefaultResolver().resolveByMap(dataScope.getScopeValue(), BeanUtil.toMap(bladeUser));
    } else if (DataScopeEnum.OWN == scopeTypeEnum) {
      ids.add(bladeUser.getUserId());
    } else if (DataScopeEnum.OWN_DEPT == scopeTypeEnum) {
      ids.addAll(Func.toLongList(bladeUser.getDeptId()));
    } else if (DataScopeEnum.OWN_DEPT_CHILD == scopeTypeEnum) {
      List<Long> deptIds = Func.toLongList(bladeUser.getDeptId());
      ids.addAll(deptIds);
      deptIds.forEach(deptId -> {
        List<Long> deptIdList = scopeModelHandler.getDeptAncestors(deptId);
        ids.addAll(deptIdList);
      });
    }
    return StringUtil.format(" select {} from ({}) scope " + whereSql, Func.toStr(dataScope.getScopeField(), "*"), originalSql, dataScope.getScopeColumn(), StringUtil.join(ids));
  }
}

纯注解我们只需要关注下面三个字段即可,当中的数据权限规则枚举类我们来看下构成:

● 可以看到,目前的数据权限类型一共有五种,前面四种都是不需要自定义写sql的,只有选择了CUSTOM类型,才需要定义注解的value属性

● 注解默认过滤的字段名为create_dept,如果有修改,则需要定义对应的字段名

接口权限配置

1)、功能介绍

接口权限:顾名思义,配置不通角色调用接口的权限。有些敏感接口,是只能有固定的一些角色才能调用,普通角色是不能调用的。这种情况需要有一个明确的系统来控制对应的访问权限


接口权限系统,可以控制某些接口只能由固定的角色调用,可以动态控制不同的角色对不同接口的访问权限


通过接口配置实现,对接口的访问权限控制和数据权限控制,


接口是REST接口,接口权限认证机制使用Json web token (JWT)

接口权限调用流程:

(1)通过接口用户的用户名密码,调用鉴权token接口获取接口用户的token

该token,2个小时内有效

(2)把获取的token作为参数,调用接口的时候,会根据token去鉴权

(3)鉴权通过,接口会根据接口定义的编码,检验是否有访问权限

有则可以继续访问,无则提示访问受限

(4)有访问权限,则获取接口的数据权限规则,根据授权的数据权限规则返回需要的数据

实现一个新的接口,无需关注token的鉴权机制,

接口权限判断方式

使用AOP实现接口拦截:@PreAuth

鉴权配置注解名称为 @PreAuth ,在需要进行鉴权配置的方法加上 @PreAuth 注解,并在注解内写 入相关的鉴权方法。

@PreAuth的注解定义:

package com.springblade.core.secure.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuth {
    String value();
}

具体实现方法:

@Aspect
public class AuthAspect implements ApplicationContextAware {
  /**
   * 表达式处理
   */
  private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
  /**
   * 切 方法 和 类上的 @PreAuth 注解
   *
   * @param point 切点
   * @return Object
   * @throws Throwable 没有权限的异常
   */
  @Around(
    "@annotation(com.springblade.core.secure.annotation.PreAuth) || " +
      "@within(com.springblade.core.secure.annotation.PreAuth)"
  )
  public Object preAuth(ProceedingJoinPoint point) throws Throwable {
    if (handleAuth(point)) {
      return point.proceed();
    }
    throw new SecureException(ResultCode.UN_AUTHORIZED);
  }
  /**
   * 处理权限
   *
   * @param point 切点
   */
  private boolean handleAuth(ProceedingJoinPoint point) {
    MethodSignature ms = (MethodSignature) point.getSignature();
    Method method = ms.getMethod();
    // 读取权限注解,优先方法上,没有则读取类
    PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class);
    // 判断表达式
    String condition = preAuth.value();
    if (StringUtil.isNotBlank(condition)) {
      Expression expression = EXPRESSION_PARSER.parseExpression(condition);
      // 方法参数值
      Object[] args = point.getArgs();
      StandardEvaluationContext context = getEvaluationContext(method, args);
      return expression.getValue(context, Boolean.class);
    }
    return false;
  }
  /**
   * 获取方法上的参数
   *
   * @param method 方法
   * @param args   变量
   * @return {SimpleEvaluationContext}
   */
  private StandardEvaluationContext getEvaluationContext(Method method, Object[] args) {
    // 初始化Sp el表达式上下文,并设置 AuthFun
    StandardEvaluationContext context = new StandardEvaluationContext(new AuthFun());
    // 设置表达式支持spring bean
    context.setBeanResolver(new BeanFactoryResolver(applicationContext));
    for (int i = 0; i < args.length; i++) {
      // 读取方法参数
      MethodParameter methodParam = ClassUtil.getMethodParameter(method, i);
      // 设置方法 参数名和值 为sp el变量
      context.setVariable(methodParam.getParameterName(), args[i]);
    }
    return context;
  }
  private ApplicationContext applicationContext;
  @Override
  public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
    this.applicationContext = applicationContext;
  }
}
public class AuthFun {
  /**
   * 权限校验处理器
   */
  private static IPermissionHandler permissionHandler;
  private static IPermissionHandler getPermissionHandler() {
    if (permissionHandler == null) {
      permissionHandler = SpringUtil.getBean(IPermissionHandler.class);
    }
    return permissionHandler;
  }
  /**
   * 判断角色是否具有接口权限
   *
   * @return {boolean}
   */
  public boolean permissionAll() {
    return getPermissionHandler().permissionAll();
  }
  /**
   * 判断角色是否具有接口权限
   *
   * @param permission 权限编号
   * @return {boolean}
   */
  public boolean hasPermission(String permission) {
    return getPermissionHandler().hasPermission(permission);
  }
  /**
   * 放行所有请求
   *
   * @return {boolean}
   */
  public boolean permitAll() {
    return true;
  }
  /**
   * 只有超管角色才可访问
   *
   * @return {boolean}
   */
  public boolean denyAll() {
    return hasRole(RoleConstant.ADMIN);
  }
  /**
   * 是否已授权
   *
   * @return {boolean}
   */
  public boolean hasAuth() {
    return Func.isNotEmpty(AuthUtil.getUser());
  }
  /**
   * 是否有时间授权
   *
   * @param start 开始时间
   * @param end   结束时间
   * @return {boolean}
   */
  public boolean hasTimeAuth(Integer start, Integer end) {
    Integer hour = DateUtil.hour();
    return hour >= start && hour <= end;
  }
  /**
   * 判断是否有该角色权限
   *
   * @param role 单角色
   * @return {boolean}
   */
  public boolean hasRole(String role) {
    return hasAnyRole(role);
  }
  /**
   * 判断是否具有所有角色权限
   *
   * @param role 角色集合
   * @return {boolean}
   */
  public boolean hasAllRole(String... role) {
    for (String r : role) {
      if (!hasRole(r)) {
        return false;
      }
    }
    return true;
  }
  /**
   * 判断是否有该角色权限
   *
   * @param role 角色集合
   * @return {boolean}
   */
  public boolean hasAnyRole(String... role) {
    BladeUser user = AuthUtil.getUser();
    if (user == null) {
      return false;
    }
    String userRole = user.getRoleName();
    if (StringUtil.isBlank(userRole)) {
      return false;
    }
    String[] roles = Func.toStrArray(userRole);
    for (String r : role) {
      if (CollectionUtil.contains(roles, r)) {
        return true;
      }
    }
    return false;
  }
}

使用方法:可以注释到类上、方法上

@PreAuth("hasPermission(#test) and @PreAuth.hasPermission(#test)")

例如:下图表示只能有test接口权限的才能访问

用户角色关系

代表了菜单权限和资源权限的一种组合方式,比如我设置了多个用户需要相同的菜单权限和资源权限, 就可以将这些权限组合起来,设置为角色,再将角色分配给用户简化操作。

功能关系

用户需要分配角色,角色需要分配菜单权限和资源权限

在项目中,不会直接对某个用户进行菜单权限或者资源权限的分配,而是提前根据岗位设定不同的角色,再将角色分配给用户就可以了。

目录
相关文章
|
5月前
|
存储 数据安全/隐私保护 索引
设计一个完美的用户角色权限表
设计一个完美的用户角色权限表
424 1
|
7月前
|
存储 安全 Android开发
Android系统 AppOps默认授予应用相应的权限
Android系统 AppOps默认授予应用相应的权限
228 0
|
7月前
|
关系型数据库 MySQL 数据库
MySQL技能完整学习列表9、用户管理和权限控制——1、创建和管理用户——2、权限授予和撤销
MySQL技能完整学习列表9、用户管理和权限控制——1、创建和管理用户——2、权限授予和撤销
97 0
|
资源调度 前端开发 数据库
权限 | 前端控制权限
啊,我们经常做权限控制,控制菜单,控制按钮功能等,但是在一些特殊情况下不能够由后端来做权限控制,那就只能前端来做啦。
101 0
|
UED
路由权限登录后还保留上一个登录角色的权限,刷新一下就好了的解决方案
路由权限登录后还保留上一个登录角色的权限,刷新一下就好了的解决方案
103 0
|
编译器 数据安全/隐私保护 C语言
C++ 权限控制,权限的继承(上)
C++ 权限控制,权限的继承
|
SQL XML 缓存
修改若依的数据权限功能
修改若依的数据权限功能
1169 0
|
安全 Java 数据安全/隐私保护
权限控制之动态权限注解使用说明|学习笔记
快速学习权限控制之动态权限注解使用说明
权限控制之动态权限注解使用说明|学习笔记
|
存储 JSON 监控
在日志服务中使用权限助手进行权限配置
在使用 SLS 日志服务的时候,很多情况下,我们需要给不同子账号,角色赋予不同的权限。由于日志服务中涉及的功能模块很多,而且功能模块互相之间有依赖关系,所以手工编写 RAM 的配置文件会显得很繁琐。现在,日志服务中新上线了权限助手功能,使用这个工具可以在很大程度上简化权限配置的操作。
1644 0
在日志服务中使用权限助手进行权限配置