为什么要有数据范围?
首先,大家应该都听说过垂直越权和水平越权?什么没有听过?那么我大致讲下:
- 垂直越权:发生在不同级别的用户之间,即低级用户尝试访问或操作高级用户的功能。例如,一个普通用户通过不当手段获取到管理员账号,然后利用该账号访问管理员才有权访问的敏感数据。
- 水平越权:在同一级别用户之间发生,即用户A尝试访问或操作用户B的数据或功能,即使用户A的权限低于用户B。例如,一个普通用户试图访问另一个普通用户的数据。
通过上面描述相信大家已经了解什么是垂直权限和水平权限。
两者各有不同,但又有共同点。就是越权访问原本没有权限访问的数据。
本文主要讲解并提供数据范围权限的解决方案,希望能给大家带来一些帮助。
解决方案
核心思想就是拦截最终执行的 SQL 并重写,使其满足要求。示例通过整合 mybatis-plus 实现该功能。为方便演示,采用 H2
内存数据库,直接启动项目即可。
1.定义数据范围注解
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {
}
2.数据范围处理逻辑
@Slf4j
public abstract class DataScopeHandler {
/**
* 获取数据范围 SQL 片段
*
* @param plainSelect 查询对象
* @param whereSegment 查询条件片段
* @return JSqlParser 条件表达式
*/
@SneakyThrows(Exception.class)
public Expression getSqlSegment(PlainSelect plainSelect, String whereSegment) {
// 待执行 sql where 条件表达式
Expression where = plainSelect.getWhere();
if (where == null) {
where = new HexValue(" 1 = 1 ");
}
log.info("开始进行权限过滤,where:{},mappedStatementId:{}", where, whereSegment);
// 获取 mapper 名称
String className = whereSegment.substring(0, whereSegment.lastIndexOf("."));
// 获取方法名
String methodName = whereSegment.substring(whereSegment.lastIndexOf(".") + 1);
// 获取当前 mapper 的方法
Method[] methods = Class.forName(className).getMethods();
// 遍历判断 mapper 的所有方法,判断方法上是否有 DataScope 注解
for (Method m : methods) {
if (Objects.equals(m.getName(), methodName)) {
DataScope annotation = m.getAnnotation(DataScope.class);
if (annotation == null) {
return where;
}
// 数据范围封装
return this.setExpression(where, DataScopeUtil.getTableName(plainSelect));
}
}
// 说明无权查看
where = new HexValue(" 1 = 2 ");
return where;
}
public abstract Expression setExpression(Expression where, String tableName);
}
示例中通过组织机构进行数据范围的拦截
public class DataPermissionHandler extends DataScopeHandler {
@Resource
private UserService userService;
// 根据组织机构拦截数据
private static final String SCOPE_FIELD = "org_id";
// 根据操作用户拦截数据
private static final String USER_FIELD = "create_by";
@Override
public Expression setExpression(Expression where, String tableName) {
// 模拟获取当前登录用户信息
LoginUser user = userService.getLoginUser();
if (null != user) {
DataScope dataScope = DataScope.valueOfId(user.getDatascope());
return switch (dataScope) {
case ALL -> DataScopeUtil.all(where);
case CUS, CURRENT_SUB ->
DataScopeUtil.inExpression(where, tableName, SCOPE_FIELD, List.of(user.getRangeList()));
case CURRENT ->
DataScopeUtil.equalsExpression(where, tableName, SCOPE_FIELD, List.of(user.getRangeList()));
case MYSELF ->
DataScopeUtil.equalsExpression(where, tableName, USER_FIELD, List.of(user.getRangeList()));
};
}
// 获取不到用户信息,无权查看任何数据
return DataScopeUtil.noData(where);
}
}
3.实现拦截器插件
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class DataScopeInterceptor extends JsqlParserSupport implements InnerInterceptor {
private DataScopeHandler dataScopeHandler;
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
return;
}
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
mpBs.sql(this.parserSingle(mpBs.sql(), ms.getId()));
}
@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
SelectBody selectBody = select.getSelectBody();
if (selectBody instanceof PlainSelect) {
this.setWhere((PlainSelect) selectBody, (String) obj);
} else if (selectBody instanceof SetOperationList setOperationList) {
List<SelectBody> selectBodyList = setOperationList.getSelects();
selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
}
}
private void setWhere(PlainSelect plainSelect, String whereSegment) {
Expression sqlSegment = this.dataScopeHandler.getSqlSegment(plainSelect, whereSegment);
if (null != sqlSegment) {
plainSelect.setWhere(sqlSegment);
}
}
}
4.插件配置
@Configuration
public class MybatisConfiguration {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 数据范围插件
interceptor.addInnerInterceptor(dataScopeInterceptor());
// 防止入参错误导致全表更新或删除
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
// 分页插件
interceptor.addInnerInterceptor(new PageInterceptor(DbType.H2));
return interceptor;
}
@Bean
public DataScopeInterceptor dataScopeInterceptor() {
// 数据范围插件
DataScopeInterceptor dataScopeInterceptor = new DataScopeInterceptor();
dataScopeInterceptor.setDataScopeHandler(dataScopeHandler());
return dataScopeInterceptor;
}
@Bean
public DataScopeHandler dataScopeHandler() {
// 数据范围处理器
return new DataPermissionHandler();
}
}
至此,基本功能已实现。我们可以通过对 mapper 层方法添加注解进行 SQL 的拦截重写。
@Mapper
public interface ArticleMapper extends BaseMapper<Article> {
@DataScope
List<Article> list();
}
@Mapper
public interface ArticleMapper extends DataScopeMapper<Article> {
}
大家可以像上面一样操作,继承 BaseMapper,针对自定义方法加上注解进行拦截,也可以继承 DataScopeMapper 默认方法全部进行拦截。