Java data access control using the Mybatis

Access control is divided into two, authentication (Authentication) and authorization (Authorization). After confirming the identity authentication is correct, the system will be licensed business, the industry is now more popular model is the RBAC (Role-Based Access Control) . RBAC to contain the following four elements: users, roles, permissions, resources. User is the source of resources is the goal, to bind to the roles, resources and associated permissions, and privileges associated with the role eventually, formed a relatively complete and flexible access control model.
Resources need to control is the ultimate subject matter, but which elements we want as a resource to be controlled in a business system it? I will control the resources in the system to be divided into three categories:

  1. URL access resources (interfaces and web pages)
  2. Interface element resource (CRUD buttons import and export of important business data to show or not, etc.)
  3. Data Resources

The industry is now widespread implementation is actually quite extensive, is simply " menu control " with the aim to achieve the control authority or not via the menu display.
I carefully analyzed, and now we do platform is divided into two To C and To B:

  1. To C generally do not have too many complex access control, or even most do not even control menus, all accessible.
  2. To B generally are not open, as long as doing certification mark, can enter the system only internal staff. Most of the staff inside knowledge of Internet companies is limited, and as a destructive internal staff did not dare try the system.

So for the present situation, consider the cost and output, most designers do not want to make too many R & D on the right.
Menu and interface elements are generally stored with the data encoded by the frontend realize, URL also control access to resources, such as some framework SpringSecurity, Shiro.
I have not been to the framework or method of data access control, so he put together a.

Data access control principle

The final effect is the data access control request will require the same process data, and returns data sets depending on the authority and need not and can not be controlled by the developed encoded. So I first thought should be the AOP, intercept all the underlying method, adding filter conditions. Such a way that compatibility is strong, but the complexity will be higher. Our present system, using the plugin is to use Mybatis mechanism, replace the filter condition when parsing increase in the underlying SQL.
Such a set of control mechanisms exist obvious advantages and disadvantages, first of all Disadvantages:

  1. Applicability is limited, based on the underlying Mybatis.
  2. Limited dialect, for some kind of database (we use Mysql), but also because of the need to resolve the underlying process conditions so may result in different databases are not compatible. Of course, Redis NoSQL and can not be restricted.

Of course, if you now use Mybatis, and the database is Mysql, this area is not much affected.

Then talk about the advantages:

  1. Reducing the number of interfaces and interface complexity. Originally for different roles, you could distinguish the different interfaces or to distinguish the different conditions in the use of process control logic interface. With access control data, the code written only basic logic rights filter automatically handled by the underlying mechanism.
  2. Increase the flexibility of data access control. For example, was only able to check in charge of the organization under the department structure / order data, and now the new assistant roles, can query the department under this organizational structure, the order can not be queried. Such ordinary writing, then the control logic would need to be adjusted, then use the data access control, directly modify the configuration like.

Data permissions achieve

On a mention on the implementation principle is based on Mybatis of plugins (see the official documentation) implementation.

MyBatis mapping process allows you to have a statement execution of a point to intercept calls. By default, MyBatis allows the method to use plug-ins to intercept calls include:
Executor (Update, Query, flushStatements, the commit, ROLLBACK, getTransaction, use Close, isClosed)
ParameterHandler (getParameterObject, the setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (PREPARE, to Parameterize , batch, update, query)

Mybatis plug-in mechanism is more famous should realize that PageHelper project, and in doing this realization also made reference implementations PageHelper project. Therefore, access control plug-in class is named PermissionHelper.
Mechanism is relying on plugins mechanism Mybatis, and when the actual SQL processing based jsqlparser this package.
Design consists of two classes, one is to preserve the role and authority of the entity class named PermissionRule, is based on a solid body changes the underlying SQL statements for class PermissionHelper.

First look at the PermissionRule structure:

public class PermissionRule {

    private static final Log log = LogFactory.getLog(PermissionRule.class);
    /**
     * codeName<br>
     * 适用角色列表<br>
     * 格式如: ,RoleA,RoleB,
     */
    private String roles;
    /**
     * codeValue<br>
     * 主实体,多表联合
     * 格式如: ,SystemCode,User,
     */
    private String fromEntity;
    /**
     * codeDesc<br>
     * 过滤表达式字段, <br>
     * <code>{uid}</code>会自动替换为当前用户的userId<br>
     * <code>{me}</code> main entity 主实体名称
     * <code>{me.a}</code> main entity alias 主实体别名
     * 格式如:
     * <ul>
     * <li>userId = {uid}</li>
     * <li>(userId = {uid} AND authType > 3)</li>
     * <li>((userId = {uid} AND authType) > 3 OR (dept in (select dept from depts where manager.id = {uid})))</li>
     * </ul>
     */
    private String exps;

    /**
     * codeShowName<br>
     * 规则说明
     */
    private String ruleComment;

}

After reading this structure, able to understand the basic idea of ​​the design. Data structure that holds the number of fields as follows:

  • List of roles: the role requires the use of this rule, may be multiple, comma separated.
  • List of entities: Entities corresponding rule applies (here refers to the table structure of the table name, you may hump and database entity is serpentine, so here to put the snake), may be multiple, comma separated open.
  • Expression : Expression is the core of the data access control. Simply put, here is some expression SQL statements, which can be set up to replace the value of the underlying variable replaces the corresponding content with the corresponding run-time, so as to achieve the effect of increasing the number of conditions.
  • Rule Description: a simple description field.

Core processes
at system startup, the first load all the rules from the database. Underlying the use of plug-in mechanism to intercept all of the query, enter the query interception method, first screened PermissionRule list based on the current list of permissions the user, and then cycle the list of rules, to table the statement in line with the entity list of conditional increase, and ultimately generate after processing SQL statements, quit the interceptor, after Mybatis SQL execution processing and returns the result.

Finished PermissionRule, look at PermissionHelper, First head:

@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class PermissionHelper implements Interceptor {
}

Head just standard Mybatis interceptor written annotations in your code Signature determine which method to intercept, update actually ** for modification (Update), delete (Delete) entered into force, query is a query (Select) ** effect.

Given below for Select complete code injection of query conditions:


    private String processSelectSql(String sql, List<PermissionRule> rules, UserDefaultZimpl principal) {
        try {
            String replaceSql = null;
            Select select = (Select) CCJSqlParserUtil.parse(sql);
            PlainSelect selectBody = (PlainSelect) select.getSelectBody();
            String mainTable = null;
            if (selectBody.getFromItem() instanceof Table) {
                mainTable = ((Table) selectBody.getFromItem()).getName().replace("`", "");
            } else if (selectBody.getFromItem() instanceof SubSelect) {
                replaceSql = processSelectSql(((SubSelect) selectBody.getFromItem()).getSelectBody().toString(), rules, principal);
            }
            if (!ValidUtil.isEmpty(replaceSql)) {
                sql = sql.replace(((SubSelect) selectBody.getFromItem()).getSelectBody().toString(), replaceSql);
            }
            String mainTableAlias = mainTable;
            try {
                mainTableAlias = selectBody.getFromItem().getAlias().getName();
            } catch (Exception e) {
                log.debug("当前sql中, " + mainTable + " 没有设置别名");
            }


            String condExpr = null;
            PermissionRule realRuls = null;
            for (PermissionRule rule :
                    rules) {
                for (Object roleStr :
                        principal.getRoles()) {
                    if (rule.getRoles().indexOf("," + roleStr + ",") != -1) {
                        if (rule.getFromEntity().indexOf("," + mainTable + ",") != -1) {
                            // 若主表匹配规则主体,则直接使用本规则
                            realRuls = rule;

                            condExpr = rule.getExps().replace("{uid}", UserDefaultUtil.getUserId().toString()).replace("{bid}", UserDefaultUtil.getBusinessId().toString()).replace("{me}", mainTable).replace("{me.a}", mainTableAlias);
                            if (selectBody.getWhere() == null) {
                                selectBody.setWhere(CCJSqlParserUtil.parseCondExpression(condExpr));
                            } else {
                                AndExpression and = new AndExpression(selectBody.getWhere(), CCJSqlParserUtil.parseCondExpression(condExpr));
                                selectBody.setWhere(and);
                            }
                        }

                        try {
                            String joinTable = null;
                            String joinTableAlias = null;
                            for (Join j :
                                    selectBody.getJoins()) {
                                if (rule.getFromEntity().indexOf("," + ((Table) j.getRightItem()).getName() + ",") != -1) {
                                    // 当主表不能匹配时,匹配所有join,使用符合条件的第一个表的规则。
                                    realRuls = rule;
                                    joinTable = ((Table) j.getRightItem()).getName();
                                    joinTableAlias = j.getRightItem().getAlias().getName();

                                    condExpr = rule.getExps().replace("{uid}", UserDefaultUtil.getUserId().toString()).replace("{bid}", UserDefaultUtil.getBusinessId().toString()).replace("{me}", joinTable).replace("{me.a}", joinTableAlias);
                                    if (j.getOnExpression() == null) {
                                        j.setOnExpression(CCJSqlParserUtil.parseCondExpression(condExpr));
                                    } else {
                                        AndExpression and = new AndExpression(j.getOnExpression(), CCJSqlParserUtil.parseCondExpression(condExpr));
                                        j.setOnExpression(and);
                                    }
                                }
                            }
                        } catch (Exception e) {
                            log.debug("当前sql没有join的部分!");
                        }
                    }
                }
            }
            if (realRuls == null) return sql; // 没有合适规则直接退出。

            if (sql.indexOf("limit ?,?") != -1 && select.toString().indexOf("LIMIT ? OFFSET ?") != -1) {
                sql = select.toString().replace("LIMIT ? OFFSET ?", "limit ?,?");
            } else {
                sql = select.toString();
            }

        } catch (JSQLParserException e) {
            log.error("change sql error .", e);
        }
        return sql;
    }

Key ideas
focus on the fact that the resolution and Sql injection conditions, the use of open-source projects JSqlParser .

  • Parse out MainTable and JoinTable. After follow from the called MainTable, followed later join is called JoinTable. The two need to match what we PermissionRule table name, PermissionRule :: fromEntity field.
  • Where the condition MainTable parsed and behind the on JoinTable. Conditions employed and connected to the original condition and to be injected, PermissionRule :: EXPS field.
  • User information currently logged (in the cache), the replacement value of the conditional expression.
  • Some cases need to ignore authority, consider using ThreadLocal (single) / Redis (clusters) to control.

Conclusion

Want to achieve no perceivable data access control, control mechanism so that only one way. Herein is selected by intercepting the underlying Sql statements, conditional statements, and such an approach for the injection correspondence table. Should be very economical way, just text-based, and do not bring too much burden on the system, but also to achieve ideal results. You can also make other insights and ideas.

Guess you like

Origin blog.csdn.net/pluto4596/article/details/91048233