组织结构(部门) 数据权限 JPA拦截及SQL解析实现

1. 应用场景

在各业务操作系统中,组织结构是很常见而且重要的配置。
组织结构是一棵树,创建用户时必须为用户选择一个所属的组织。在部分业务场景中需要根据组织ID筛选该用户能看到的业务数据。
举个栗子:

  • 董事长A可以看到所有部门的数据
  • 销售部经理可以看到销售部及其子部门所有数据,但无法查看非销售部其他数据


    image.png

2. 解决思路

  • 用户登录成功,查询该用户组织结构IDorgId及其子孙部门IDorgIds,存储这些用户信息到集中缓存redis中
  • 用户访问接口时,根据token从redis取出用户信息设置到线程变量ThreadLocal中
  • 编写JPA拦截器(Mybatis同理),为符合条件的SQL进行解析并修改,筛选组织数据

关于该思路的具体介绍,请参考我之前的文章
JPA 表租户 SQL解析实现
Mybatis-Plus租户解析的应用

在基于MybatisPlus租户解析器、jsqlphaserSQL解析工具、JPA拦截器的理解上编写如下代码

3. 代码略长,可以略过看看效果后再研究

import com.kichun.sc.common.config.OrganizationProperties;
import com.kichun.sc.common.context.UserContext;
import com.kichun.sc.common.user.CurrentUser;
import com.kichun.sc.common.util.SpringContextUtil;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.*;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.Statements;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.update.Update;
import org.hibernate.resource.jdbc.spi.StatementInspector;

import java.util.ArrayList;
import java.util.List;

/**
 * 参考Mybatis-Plus插件中的TenantSqlParser进行组织ID解析处理,其实现为使用jsqlparser对sql进行解析,拼装SQL语句
 *
 * @author wangqichang
 * @since 2020/1/12
 */
@Slf4j
@Data
@Accessors(chain = true)
@SuppressWarnings("ALL")
public class OrganizationInterceptor implements StatementInspector {


    private String orgId;

    private List<String> orgIds;

    private List<String> orgTables;

    private String orgIdColumn = "org_id";


    /**
     * 重写StatementInspector的inspect接口,参数为hibernate处理后的原始SQL,返回值为我们修改后的SQL
     *
     * @param sql
     * @return
     */
    @Override
    public String inspect(String sql) {
        try {
            /**
             * 未登录用户,系统用户不做解析
             */
            CurrentUser current = UserContext.current();
            if (UserContext.current() == null || UserContext.current().getAdministrator()) {
                return null;
            }
            /**
             * 初始化需要进行解析的组织表,
             */
            if (orgTables == null) {
                synchronized (OrganizationInterceptor.class) {
                    OrganizationProperties bean = SpringContextUtil.getBean(OrganizationProperties.class);
                    if (bean != null) {
                        orgTables = bean.getTables();
                    } else {
                        throw new RuntimeException("未能获取TenantProperties参数配置");
                    }
                }
            }

            /**
             * 从当前线程获取登录用户的所属用户组织ID及其子孙组织ID
             */
            CurrentUser user = UserContext.current();
            orgId = user.getOrganizationId();
            orgIds = user.getOrganizationIds();

            log.info("组织筛选解析开始,原始SQL:{}", sql);
            Statements statements = CCJSqlParserUtil.parseStatements(sql);
            StringBuilder sqlStringBuilder = new StringBuilder();
            int i = 0;
            for (Statement statement : statements.getStatements()) {
                if (null != statement) {
                    if (i++ > 0) {
                        sqlStringBuilder.append(';');
                    }
                    sqlStringBuilder.append(this.processParser(statement));
                }
            }
            String newSql = sqlStringBuilder.toString();
            log.info("组织筛选解析结束,解析后SQL:{}", newSql);
            return newSql;
        } catch (Exception e) {
            log.error("组织筛选解析失败,解析SQL异常{}", e.getMessage());
            e.printStackTrace();
        } finally {
            orgId = null;
        }
        return null;
    }

    private String processParser(Statement statement) {
        if (statement instanceof Insert) {
            this.processInsert((Insert) statement);
        } else if (statement instanceof Select) {
            this.processSelectBody(((Select) statement).getSelectBody());
        } else if (statement instanceof Update) {
            this.processUpdate((Update) statement);
        } else if (statement instanceof Delete) {
            this.processDelete((Delete) statement);
        }
        /**
         * 返回处理后的SQL
         */
        return statement.toString();
    }

    /**
     * select 语句处理
     */

    public void processSelectBody(SelectBody selectBody) {
        if (selectBody instanceof PlainSelect) {
            processPlainSelect((PlainSelect) selectBody);
        } else if (selectBody instanceof WithItem) {
            WithItem withItem = (WithItem) selectBody;
            if (withItem.getSelectBody() != null) {
                processSelectBody(withItem.getSelectBody());
            }
        } else {
            SetOperationList operationList = (SetOperationList) selectBody;
            if (operationList.getSelects() != null && operationList.getSelects().size() > 0) {
                operationList.getSelects().forEach(this::processSelectBody);
            }
        }
    }

    /**
     * insert 语句处理
     */

    public void processInsert(Insert insert) {
        if (orgTables.contains(insert.getTable().getFullyQualifiedName())) {
            insert.getColumns().add(new Column(orgIdColumn));
            if (insert.getSelect() != null) {
                processPlainSelect((PlainSelect) insert.getSelect().getSelectBody(), true);
            } else if (insert.getItemsList() != null) {
                // fixed github pull/295
                ItemsList itemsList = insert.getItemsList();
                if (itemsList instanceof MultiExpressionList) {
                    ((MultiExpressionList) itemsList).getExprList().forEach(el -> el.getExpressions().add(new StringValue(orgId)));
                } else {
                    ((ExpressionList) insert.getItemsList()).getExpressions().add(new StringValue(orgId));
                }
            } else {
                throw new RuntimeException("Failed to process multiple-table update, please exclude the tableName or statementId");
            }
        }
    }

    /**
     * update 语句处理
     */

    public void processUpdate(Update update) {
        final Table table = update.getTable();
        if (orgTables.contains(table.getFullyQualifiedName())) {
            update.setWhere(this.andExpression(table, update.getWhere()));
        }
    }

    /**
     * delete 语句处理
     */

    public void processDelete(Delete delete) {
        if (orgTables.contains(delete.getTable().getFullyQualifiedName())) {
            delete.setWhere(this.andExpression(delete.getTable(), delete.getWhere()));
        }
    }

    /**
     * delete update 语句 where 处理
     */
    protected BinaryExpression andExpression(Table table, Expression where) {
        //获得where条件表达式
        EqualsTo equalsTo = new EqualsTo();
        equalsTo.setLeftExpression(this.getAliasColumn(table));
        equalsTo.setRightExpression(new StringValue(orgId));
        if (null != where) {
            if (where instanceof OrExpression) {
                return new AndExpression(equalsTo, new Parenthesis(where));
            } else {
                return new AndExpression(equalsTo, where);
            }
        }
        return equalsTo;
    }

    /**
     * 处理 PlainSelect
     */
    protected void processPlainSelect(PlainSelect plainSelect) {
        if (plainSelect.getWhere() != null) {
            processPlainSelect(plainSelect, true);
        } else {
            processPlainSelect(plainSelect, false);
        }

    }

    /**
     * 处理 PlainSelect
     *
     * @param plainSelect ignore
     * @param addColumn   是否添加租户列,insert into select语句中需要
     */
    protected void processPlainSelect(PlainSelect plainSelect, boolean addColumn) {
        FromItem fromItem = plainSelect.getFromItem();
        if (fromItem instanceof Table) {
            Table fromTable = (Table) fromItem;
            if (orgTables.contains(fromTable.getFullyQualifiedName())) {
                //#1186 github
                plainSelect.setWhere(builderExpression(plainSelect.getWhere(), fromTable));
                if (addColumn) {
                    plainSelect.getSelectItems().add(new SelectExpressionItem(new Column(orgIdColumn)));
                }
            }
        } else {
            processFromItem(fromItem);
        }
        List<Join> joins = plainSelect.getJoins();
        if (joins != null && joins.size() > 0) {
            joins.forEach(j -> {
                processJoin(j);
                processFromItem(j.getRightItem());
            });
        }
    }

    /**
     * 处理子查询等
     */
    protected void processFromItem(FromItem fromItem) {
        if (fromItem instanceof SubJoin) {
            SubJoin subJoin = (SubJoin) fromItem;
            if (subJoin.getJoinList() != null) {
                subJoin.getJoinList().forEach(this::processJoin);
            }
            if (subJoin.getLeft() != null) {
                processFromItem(subJoin.getLeft());
            }
        } else if (fromItem instanceof SubSelect) {
            SubSelect subSelect = (SubSelect) fromItem;
            if (subSelect.getSelectBody() != null) {
                processSelectBody(subSelect.getSelectBody());
            }
        } else if (fromItem instanceof ValuesList) {
            log.debug("Perform a subquery, if you do not give us feedback");
        } else if (fromItem instanceof LateralSubSelect) {
            LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem;
            if (lateralSubSelect.getSubSelect() != null) {
                SubSelect subSelect = lateralSubSelect.getSubSelect();
                if (subSelect.getSelectBody() != null) {
                    processSelectBody(subSelect.getSelectBody());
                }
            }
        }
    }

    /**
     * 处理联接语句
     */
    protected void processJoin(Join join) {
        if (join.getRightItem() instanceof Table) {
            Table fromTable = (Table) join.getRightItem();
            if (orgTables.contains(fromTable.getFullyQualifiedName())) {
                join.setOnExpression(builderExpression(join.getOnExpression(), fromTable));
            }
        }
    }

    /**
     * 处理条件:
     * 创建InExpression,即封装where orgId in ('','')
     */
    protected Expression builderExpression(Expression currentExpression, Table table) {
        final InExpression organizationExpression = new InExpression();
        List<Expression> expressions = new ArrayList<>();
        orgIds.forEach(organizatinId -> {
            expressions.add(new StringValue(organizatinId));
        });
        ExpressionList expressionList = new ExpressionList(expressions);
        organizationExpression.setLeftExpression(this.getAliasColumn(table));
        organizationExpression.setRightItemsList(expressionList);

        Expression appendExpression  =  null;
        if (!(organizationExpression instanceof SupportsOldOracleJoinSyntax)) {
            appendExpression = new EqualsTo();
            ((EqualsTo) appendExpression).setLeftExpression(this.getAliasColumn(table));
            ((EqualsTo) appendExpression).setRightExpression(organizationExpression);
        }
        if (currentExpression == null) {
            return organizationExpression;
        }else {
            appendExpression  = organizationExpression;
        }
        if (currentExpression instanceof BinaryExpression) {
            BinaryExpression binaryExpression = (BinaryExpression) currentExpression;
            doExpression(binaryExpression.getLeftExpression());
            doExpression(binaryExpression.getRightExpression());
        } else if (currentExpression instanceof InExpression) {
            InExpression inExp = (InExpression) currentExpression;
            ItemsList rightItems = inExp.getRightItemsList();
            if (rightItems instanceof SubSelect) {
                processSelectBody(((SubSelect) rightItems).getSelectBody());
            }
        }
        if (currentExpression instanceof OrExpression) {
            return new AndExpression(new Parenthesis(currentExpression), appendExpression);
        } else {
            return new AndExpression(currentExpression, appendExpression);
        }
    }

    protected void doExpression(Expression expression) {
        if (expression instanceof FromItem) {
            processFromItem((FromItem) expression);
        } else if (expression instanceof InExpression) {
            InExpression inExp = (InExpression) expression;
            ItemsList rightItems = inExp.getRightItemsList();
            if (rightItems instanceof SubSelect) {
                processSelectBody(((SubSelect) rightItems).getSelectBody());
            }
        }
    }


    /**
     * 租户字段别名设置
     * <p>tableName.orgId 或 tableAlias.orgId</p>
     *
     * @param table 表对象
     * @return 字段
     */
    protected Column getAliasColumn(Table table) {
        StringBuilder column = new StringBuilder();
        if (null == table.getAlias()) {
            column.append(table.getName());
        } else {
            column.append(table.getAlias().getName());
        }
        column.append(".");
        column.append(orgIdColumn);
        return new Column(column.toString());
    }

}

4. 效果展示

如日志展示,拦截器为SQL添加了
WHERE user0_.org_id IN ('40285b8162669b9c016266a0a5320001', '40285b816266a8a2016266ad53360002', '40285b816266a8a2016266ad6c910003', '40285b816266a8a2016266ad85ae0004', '40285b816266a8a2016266ad9f3c0005')
这行代码,限制了该用户只能查看sys_user表中部分有访问权限的组织ID的数据

2020-01-12 14:56:44.037  INFO 16500 --- [nio-9050-exec-1] c.t.s.c.i.OrganizationInterceptor        : 组织筛选解析开始,原始SQL:select user0_.id as id1_10_, user0_.create_date as create_d2_10_, user0_.update_date as update_d3_10_, user0_.administrator as administ4_10_, user0_.org_id as org_id5_10_, user0_.password as password6_10_, user0_.real_name as real_nam7_10_, user0_.tenant_id as tenant_i8_10_, user0_.user_name as user_nam9_10_ from sys_user user0_ limit ?
2020-01-12 14:56:44.039  INFO 16500 --- [nio-9050-exec-1] c.t.s.c.i.OrganizationInterceptor        : 组织筛选解析结束,解析后SQL:SELECT user0_.id AS id1_10_, user0_.create_date AS create_d2_10_, user0_.update_date AS update_d3_10_, user0_.administrator AS administ4_10_, user0_.org_id AS org_id5_10_, user0_.password AS password6_10_, user0_.real_name AS real_nam7_10_, user0_.tenant_id AS tenant_i8_10_, user0_.user_name AS user_nam9_10_ FROM sys_user user0_ WHERE user0_.org_id IN ('40285b8162669b9c016266a0a5320001', '40285b816266a8a2016266ad53360002', '40285b816266a8a2016266ad6c910003', '40285b816266a8a2016266ad85ae0004', '40285b816266a8a2016266ad9f3c0005') LIMIT ?
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • 1. 简介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的...
    笨鸟慢飞阅读 5,266评论 0 4
  • 1. 功能介绍 针对表租户ID字段标识的多租户系统 参考了Mybatis-Plus插件的TenantSqlPars...
    KICHUN阅读 5,669评论 1 6
  • 最近的一个项目是将J2EE环境打包安装在客户端(使用nwjs+NSIS制作安装包)运行, 所有的业务操作在客户端完...
    Java大生阅读 4,492评论 2 6
  • 这篇文章是基于我开发读写分离中间件和数据库智能运维平台时的经验总结而成。网上对数据库连接系统分析的文章非常少,甚至...
    彦帧阅读 4,811评论 0 4
  • .h文件是头文件,是公开定义类的成员变量以及方法等等,外部是可以访问的。.m文件是对.h文件中方法是实现,对外部是...
    Jalon阅读 172评论 0 0