概念
功能权限和数据权限。
- 功能权限:用户是否能打开某一个网页,是否能点击编辑按钮等。
- 数据权限:用户可以使用的数据范围。
用户
应用系统的具体操作者,用户可以自己拥有权限信息,可以归属于0~n个角色,可属于0~n个组。他的权限集是自身具有的权限、所属的各角色具有的权限、所属的各组具有的权限的合集。它与权限、角色、组之间的关系都是n对n的关系。
角色
为了对许多拥有相似权限的用户进行分类管理,定义了角色的概念,例如系统管理员、管理员、用户、访客等角色。角色具有上下级关系,可以形成树状视图,父级角色的权限是自身及它的所有子角色的权限的综合。父级角色的用户、父级角色的组同理可推。
组
为了更好地管理用户,对用户进行分组归类,简称为用户分组。组也具有上下级关系,可以形成树状视图。在实际情况中,我们知道,组也可以具有自己的角色信息、权限信息。比如:用户群,一个群可以有多个用户,一个用户也可以加入多个群。每个群具有自己的权限信息。例如查看群共享。QQ群也可以具有自己的角色信息,例如普通群、高级群等。
权限
页面权限:用户可以看到那些页面;
操作权限:用户可以在页面内进行那些操作,增删改查等;
数据权限:用户可以看到那些数据或内容如:本部门的数据库,全部数据,本身数据;
权限模型
1.ACL:访问控制列表
说明
用户表 权限表 用户权限关系表。
2.RBAC: 基于角色的权限控制
RBAC0、RBAC1、RBAC2、RBAC3 四个阶段,一般公司使用 RBAC0 的模型就可以。另外,RBAC0 相当于底层逻辑,后三者都是在 RBAC0 模型上的拔高。
3.ABAC:基于属性的权限控制
通过动态计算一个或一组属性来是否满足某种条件来进行授权判断(可以编写简单的逻辑)。属性通常来说分为四类:用户属性(如用户年龄),环境属性(如当前时间),操作属性(如读取)和对象属性(如一篇文章,又称资源属性),理论上能够实现非常灵活的权限控制,几乎能满足所有类型的需求。
典型的 ABAC 场景描述如下图,当 subject 需要去读取某一条记录时,我们的访问控制机制在请求发起后遍开始运作,该机制需要计算,来自 policy 中记录的规则,subject 的 attribute,object 的 attribute 以及 environment conditions,而最后会产生一个是否允许读取的结果:
细粒度权限控制,实现起来非常负责,维护成本高。
4.PBAC:基于策略的权限控制
PBAC支持运行时授权,因此它是动态的,并具有允许实时进行更改的能力,不能直观看出用户和资源的访问关系,需要实时计算,较多规则会有性能问题。
PBAC是一种将角色和属性与逻辑结合以创建灵活的动态控制策略的方法。与ABAC一样,它使用许多属性来确定访问权限,因此它还提供了“细粒度”访问控制。PBAC旨在支持各种方式的访问设备,通常被认为是最灵活的授权解决方案。
文献
数据权限
用户表 角色表 权限表 机构表 。
思想:
角色表新增数据范围字段: 0:全部数据 1:本部门及子部门数据 2:本部门数据 3:本人数据 4:自定义数据
用户登录缓存数据用户信息和机构信息。查询时候根据用户信息和机构信息获取用户数据权限范围。
如下仅供思维参考。
第一种:基于mybatis-plus插件
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.2</version>
</dependency>
@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();// 数据权限mybatisPlusInterceptor.addInnerInterceptor(new DataScopeInnerInterceptor());return mybatisPlusInterceptor;}
/*** 数据范围*/
@Data
@AllArgsConstructor
public class DataScope {private String sqlFilter;
}
数据拦截器
package com.lean.auth.interceptor;import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;import java.util.Map;/*** 数据权限*/
public class DataScopeInnerInterceptor implements InnerInterceptor {@Overridepublic void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {DataScope scope = getDataScope(parameter);// 不进行数据过滤if (scope == null || StrUtil.isBlank(scope.getSqlFilter())) {return;}// 拼接新SQLString buildSql = getSelect(boundSql.getSql(), scope);// 重写SQLPluginUtils.mpBoundSql(boundSql).sql(buildSql);}private DataScope getDataScope(Object parameter) {if (parameter == null) {return null;}// 判断参数里是否有DataScope对象if (parameter instanceof Map) {Map<?, ?> parameterMap = (Map<?, ?>) parameter;for (Map.Entry entry : parameterMap.entrySet()) {if (entry.getValue() != null && entry.getValue() instanceof DataScope) {return (DataScope) entry.getValue();}}} else if (parameter instanceof DataScope) {return (DataScope) parameter;}return null;}private String getSelect(String buildSql, DataScope scope) {try {//解析sqlSelect select = (Select) CCJSqlParserUtil.parse(buildSql);PlainSelect plainSelect = (PlainSelect) select.getSelectBody();Expression expression = plainSelect.getWhere();if (expression == null) {plainSelect.setWhere(new StringValue(scope.getSqlFilter()));} else {AndExpression andExpression = new AndExpression(expression, new StringValue(scope.getSqlFilter()));plainSelect.setWhere(andExpression);}return select.toString().replaceAll("'", "");} catch (JSQLParserException e) {return buildSql;}}
}
业务组装数据权限sql:
/*** 原生SQL 数据权限* @param tableAlias 表别名,多表关联时,需要填写表别名* @param orgIdAlias 机构ID别名,null:表示org_id* @return 返回数据权限*/protected DataScope getDataScope(String tableAlias, String orgIdAlias) {UserDetail user = SecurityUser.getUser();// 如果是超级管理员,则不进行数据过滤if(user.getSuperAdmin().equals("1")) {return null;}// 如果为null,则设置成空字符串if(tableAlias == null){tableAlias = "";}// 获取表的别名if(StringUtils.isNotBlank(tableAlias)){tableAlias += ".";}StringBuilder sqlFilter = new StringBuilder();sqlFilter.append(" (");// 数据权限范围List<Long> dataScopeList = getDataScopeList(user.getId(),user.getOrgId());// 全部数据权限if (dataScopeList == null){return null;}// 数据过滤if(dataScopeList.size() > 0){if(StringUtils.isBlank(orgIdAlias)){orgIdAlias = "org_id";}sqlFilter.append(tableAlias).append(orgIdAlias);sqlFilter.append(" in(").append(StrUtil.join(",", dataScopeList)).append(")");sqlFilter.append(" or ");}// 查询本人数据sqlFilter.append(tableAlias).append("create_id").append("=").append(user.getId());sqlFilter.append(")");return new DataScope(sqlFilter.toString());}/*** 获取数据权限部门id* @param userId 用户id* @param orgId 部门id* @return*/private List<Long> getDataScopeList(Long userId,Long orgId) {//获取用户最大的数据范围//select max(t1.data_scope) from sys_role t1, sys_user_role t2 where t1.id = t2.role_id and t2.user_id = #{userId} and t1.is_del = 0Integer dataScope = sysRoleDao.getDataScopeByUserId(userId);if (dataScope == null) {return new ArrayList<>();}//数据范围 0:全部数据 1:本部门及子部门数据 2:本部门数据 3:本人数据 4:自定义数据if (dataScope.equals("0")) {// 全部数据权限,则返回nullreturn null;} else if (dataScope.equals("1")) {// 本部门及子部门数据List<Long> dataScopeList = sysOrgService.getSubOrgIdList(orgId);return dataScopeList;} else if (dataScope.equals("2")) {// 本部门数据List<Long> dataScopeList = new ArrayList<>();dataScopeList.add(orgId);return dataScopeList;} else if (dataScope.equals("4")) {// 自定义数据权限范围//select t2.org_id from sys_user_role t1, sys_role_data_scope t2 where t1.user_id = #{userId} and t1.role_id = t2.role_id and t1.is_del = 0return sysRoleDataScopeDao.getDataScopeList(orgId);}return new ArrayList<>();}/*** MyBatis-Plus 数据权限请求参数填充sql*/protected void dataScopeWrapper(LambdaQueryWrapper<T> queryWrapper) {DataScope dataScope = getDataScope(null, null);if (dataScope != null){queryWrapper.apply(dataScope.getSqlFilter());}}
案例:
@GetMapping("page")
@Operation(summary = "分页")
public Result<PageResult> page(@Valid SysRoleQuery query){PageResult page = sysRoleService.page(query);return Result.ok(page);
}@Override
public PageResult page(SysRoleQuery query) {IPage page = baseMapper.selectPage(getPage(query), getWrapper(query));return new PageResult<>(page);
}private Wrapper getWrapper(SysRoleQuery query){LambdaQueryWrapper<SysRoleEntity> wrapper = new LambdaQueryWrapper<>();wrapper.like(StrUtil.isNotBlank(query.getName()), SysRoleEntity::getName, query.getName());// 数据权限 请求参数填充权限sql dataScope,供拦截器拦截处理dataScopeWrapper(wrapper);return wrapper;}
第二种:基于mybatis插件
mybatis插件原理
<dependency><groupId>com.github.jsqlparser</groupId><artifactId>jsqlparser</artifactId><version>4.4</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.29</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.7.22</version><scope>compile</scope></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.1</version></dependency>
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 用来判断是否需要进行数据权限*/
@Target(value = {ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataAuthSelect {
}
import lombok.AllArgsConstructor;
import lombok.Data;/*** 数据范围***/
@Data
@AllArgsConstructor
public class DataScope {private String sqlFilter;
}
import cn.hutool.extra.spring.SpringUtil;
import com.lean.mybatis.service.MenuService;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Properties;@Intercepts({ @Signature(method = "prepare", type = StatementHandler.class, args = { Connection.class,Integer.class }) })
public class DataScopeInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {StatementHandler handler = (StatementHandler)invocation.getTarget();//由于mappedStatement中有我们需要的方法id,但却是protected的,所以要通过反射获取MetaObject statementHandler = SystemMetaObject.forObject(handler);MappedStatement mappedStatement = (MappedStatement) statementHandler.getValue("delegate.mappedStatement");//没自定义注解直接按通过算DataAuthSelect dataAuth = getDataAuth(mappedStatement);if (dataAuth == null) {return invocation.proceed();}//判断是否登录 TODO //获取sql SELECT * FROM t_role WHERE id = ? and is_valid = 0BoundSql boundSql = handler.getBoundSql();String sql = boundSql.getSql();//获得方法类型 (如select,update)SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();if ("SELECT".equalsIgnoreCase(sqlCommandType.toString())) {//增强sql代码块 这里可通过判断用户权限为不通类型的用户拼接不同的sql//根据用户id和机构id 获取数据权限范围MenuService menuService = SpringUtil.getBean(MenuService.class);DataScope dataScope = menuService.getDataScope(null, null);// 拼接新SQL// SELECT * FROM t_role WHERE id = ? AND is_valid = 0 AND (create_id=10)// SELECT * FROM t_role WHERE id = ? AND is_valid = 0 AND (org_id in(2,21) or create_id=10)sql=getSelect(sql,dataScope);//将增强后的sql放回statementHandler.setValue("delegate.boundSql.sql",sql);}return invocation.proceed();}@Overridepublic Object plugin(Object o) {//生成代理对象return Plugin.wrap(o, this);}@Overridepublic void setProperties(Properties properties) {}/*** 通过反射获取mapper方法是否加了自定义注解*/private DataAuthSelect getDataAuth(MappedStatement mappedStatement) throws ClassNotFoundException {DataAuthSelect dataAuth = null;String id = mappedStatement.getId();String className = id.substring(0, id.lastIndexOf("."));String methodName = id.substring(id.lastIndexOf(".") + 1);final Class<?> cls = Class.forName(className);final Method[] methods = cls.getMethods();for (Method method : methods) {if (method.getName().equals(methodName) && method.isAnnotationPresent(DataAuthSelect.class)) {dataAuth = method.getAnnotation(DataAuthSelect.class);break;}}return dataAuth;}private String getSelect(String buildSql, DataScope scope){try {Select select = (Select) CCJSqlParserUtil.parse(buildSql);PlainSelect plainSelect = (PlainSelect) select.getSelectBody();Expression expression = plainSelect.getWhere();if(expression == null){plainSelect.setWhere(new StringValue(scope.getSqlFilter()));}else{AndExpression andExpression = new AndExpression(expression, new StringValue(scope.getSqlFilter()));plainSelect.setWhere(andExpression);}return select.toString().replaceAll("'", "");}catch (JSQLParserException e){return buildSql;}}
}
import java.util.Date;
@Data
public class Role {private Long id;private String roleName;private String roleRemark;private Date createDate;private Date updateDate;private Integer isValid;
}
import lombok.AllArgsConstructor;
import lombok.Data;@Data
@AllArgsConstructor
public class UserDetail {private Long id;private Long orgId;private String superAdmin;
}
import cn.hutool.core.util.StrUtil;
import com.lean.mybatis.datascope.DataScope;
import com.lean.mybatis.entity.UserDetail;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;import java.util.ArrayList;
import java.util.List;@Service
public class DataScopService {/*** 获取数据权限部门id** @param userId 用户id* @param orgId 部门id* @return*/private List<Long> getDataScopeList(Long userId, Long orgId) {//根据用户ID,获取用户最大的数据范围//select max(t1.data_scope) from sys_role t1, sys_user_role t2 where t1.id = t2.role_id and t2.user_id = #{userId} and t1.is_del = 0Integer dataScope = 1;if (dataScope == null) {return new ArrayList<>();}//数据范围 0:全部数据 1:本部门及子部门数据 2:本部门数据 3:本人数据 4:自定义数据if (dataScope == 0) {// 全部数据权限,则返回nullreturn null;} else if (dataScope == 1) {// 本部门及子部门数据List<Long> dataScopeList = new ArrayList<>();dataScopeList.add(2L);dataScopeList.add(21L);return dataScopeList;} else if (dataScope == 2) {// 本部门数据List<Long> dataScopeList = new ArrayList<>();dataScopeList.add(orgId);return dataScopeList;} else if (dataScope == 4) {// 自定义数据权限范围//select t2.org_id from sys_user_role t1, sys_role_data_scope t2 where t1.user_id = #{userId} and t1.role_id = t2.role_id and t1.is_del = 0List<Long> dataScopeList = new ArrayList<>();dataScopeList.add(23L);return dataScopeList;}return new ArrayList<>();}/*** 原生SQL 数据权限** @param tableAlias 表别名,多表关联时,需要填写表别名* @param orgIdAlias 机构ID别名,null:表示org_id* @return 返回数据权限*/public DataScope getDataScope(String tableAlias, String orgIdAlias) {//模拟当前用户 TODOUserDetail user = new UserDetail(10L, 2L, "2");// 如果是超级管理员,则不进行数据过滤if (user.getSuperAdmin().equals("1")) {return null;}// 如果为null,则设置成空字符串if (tableAlias == null) {tableAlias = "";}// 获取表的别名if (!StringUtils.isEmpty(tableAlias)) {tableAlias += ".";}StringBuilder sqlFilter = new StringBuilder();sqlFilter.append(" (");// 数据权限范围List<Long> dataScopeList = getDataScopeList(user.getId(), user.getOrgId());// 全部数据权限if (dataScopeList == null) {return null;}// 数据过滤if (dataScopeList.size() > 0) {if (StringUtils.isEmpty(orgIdAlias)) {orgIdAlias = "org_id";}sqlFilter.append(tableAlias).append(orgIdAlias);sqlFilter.append(" in(").append(StrUtil.join(",", dataScopeList)).append(")");sqlFilter.append(" or ");}// 查询本人数据sqlFilter.append(tableAlias).append("create_id").append("=").append(user.getId());sqlFilter.append(")");return new DataScope(sqlFilter.toString());}}
xml配置
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><plugins><plugin interceptor="com.lean.mybatis.datascope.DataScopeInterceptor"></plugin></plugins>
</configuration>
import com.lean.mybatis.datascope.DataAuthSelect;
import com.lean.mybatis.entity.Role;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;import java.io.Serializable;@Mapper
public interface RoleMapper {@Select(value = "SELECT * FROM t_role WHERE id = #{id} and is_valid = 0")@DataAuthSelectRole selectByIdAuth(Serializable id);
}
测试
import com.lean.mybatis.dao.RoleMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest
class SpringLeanMybatisApplicationTests {@Autowiredprivate RoleMapper roleMapper;@Testvoid contextLoads() {}@Testvoid testAuth() {roleMapper.selectByIdAuth(1);}
}