Data Rights Management Center
Since most of the company's projects use mybatis, and also use the mybatis interceptor for paging processing, technically, we also directly choose to start with the interceptor.
demand scenario
The first scenario: row-level data processing
original sql:
select id,username,region from sys_user ;
It needs to be packaged as:
select * from (
select id,username,region from sys_user
) where 1=1 and region like “3210%";
explain
Users can only query the data of the current city and subordinate cities. The like part can also be a dynamic parameter (described below)
This scenario also includes the following:
# judge select * from (select id,username,region from sys_user ) where 1=1 and region != 320101; # enumerate select * from (select id,username,region from sys_user ) where 1=1 and region in (320101,320102,320103); ...
The second scenario: column-level data processing
original sql:
select id,username,region from sys_user ;
User A can see id, username, region
User B can only view the values of id and username, but has no permission to view the values of region.
Application flow chart
Application Link Logic Diagram
Technical realization
mybatis interceptor
Before writing the interceptor of mybatis, let's first understand the interception target method of mybaits
-
1、Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
-
2、ParameterHandler (getParameterObject, setParameters)
-
3、StatementHandler (prepare, parameterize, batch, update, query)
-
4、ResultSetHandler (handleResultSets, handleOutputParameters)
Here, the prepare method of StatementHandler is selected as the interception before sql execution to encapsulate sql, and the handleResultSets method of ResultSetHandler is used as the result interception and filtering after sql execution.
Before sql is executed
PrepareInterceptor.java
/** * mybatis data permission interceptor - prepare * @author GaoYuan * @date 4/17/2018 9:52 AM */ @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class,Integer.class }) }) @Component public class PrepareInterceptor implements Interceptor { /** 日志 */ private static final Logger log = LoggerFactory.getLogger(PrepareInterceptor.class); @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) {} @Override public Object intercept(Invocation invocation) throws Throwable { if(log.isInfoEnabled()){ log.info( "Entering PrepareInterceptor..." ); } if(invocation.getTarget() instanceof RoutingStatementHandler) { RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget(); StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(handler, "delegate" ); // Get the mappedStatement property of the delegate parent class BaseStatementHandler through reflection MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement" ); // Ten million You cannot use the method annotated below, it will cause the object to be lost and the conversion will fail // MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; PermissionAop permissionAop = PermissionUtils.getPermissionByDelegate(mappedStatement); if (permissionAop == null ) { if (log.isInfoEnabled()){ log.info( "Data permission release..." ); } return invocation.proceed(); } if(log.isInfoEnabled()){ log.info( "Data permission processing [splicing SQL]..." ); } BoundSql boundSql = delegate.getBoundSql(); ReflectUtil.setFieldValue(boundSql, "sql", permissionSql(boundSql.getSql())); } return invocation.proceed(); } /** * Permission sql wrapper * @author GaoYuan * @date 4/17/2018 9:51 AM */ protected String permissionSql(String sql) { StringBuilder sbSql = new StringBuilder(sql); String userMethodPath = PermissionConfig.getConfig("permission.client.userid.method" ); // Current login person String userId = (String)ReflectUtil.reflectByPath(userMethodPath); // If the user is 1, only the first if ("1" .equals(userId)){ // sbSql = sbSql.append(" limit 1 "); // if there is dynamic parameter regionCd if ( true ){ String premission_param = "regionCd"; //select * from (select id,name,region_cd from sys_exam ) where region_cd like '${}%' String methodPath = PermissionConfig.getConfig("permission.client.params." + premission_param); String regionCd = (String)ReflectUtil.reflectByPath(methodPath); sbSql = new StringBuilder("select * from (").append(sbSql).append(" ) s where s.regionCd like concat("+ regionCd +",'%') "); } } return sbSql.toString(); } }
After sql is executed
ResultInterceptor.java
/** * mybatis data permission interceptor - handleResultSets * filter the result set * @author GaoYuan * @date 4/17/2018 9:52 AM */ @Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args={Statement.class}) }) @Component public class ResultInterceptor implements Interceptor { /** 日志 */ private static final Logger log = LoggerFactory.getLogger(ResultInterceptor.class); @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) {} @Override public Object intercept(Invocation invocation) throws Throwable { if(log.isInfoEnabled()){ log.info( "Enter ResultInterceptor..." ); } ResultSetHandler resultSetHandler1 = (ResultSetHandler) invocation.getTarget(); // Get the mappedStatement attribute value through java reflection // You can get the resulttype MappedStatement in mybatis mappedStatement = (MappedStatement)ReflectUtil.getFieldValue(resultSetHandler1, "mappedStatement" ); // Get the aspect Object PermissionAop permissionAop = PermissionUtils.getPermissionByDelegate(mappedStatement); // Execute the request method and save the result to result Object result = invocation.proceed(); if (permissionAop != null ) { if (result instanceof ArrayList) { ArrayList resultList = (ArrayList) result; for (int i = 0; i < resultList.size(); i++) { Object oi = resultList.get(i); Class c = oi.getClass(); Class[] types = {String.class}; Method method = c.getMethod("setRegionCd", types); // 调用obj对象的 method 方法 method.invoke(oi, ""); if(log.isInfoEnabled()){ log.info( "Data permission processing [filter result]..." ); } } } } return result; } }
Among them, PermissionAop is a custom aspect of the dao layer, which is used to switch and control whether to enable data permission filtering.
difficulty
-
How to get the annotation content of the dao layer in the interceptor;
-
How to get the current login ID;
-
How to pass dynamic parameters;
-
Need to take into account the priority of paging with sql.
answer
Interceptor gets dao layer annotations
The interceptor acquisition methods of different methods are slightly different, and you can view them by yourself in the code of PrepareInterceptor.java and ResultInterceptor.java above.
Get the current login ID
Due to different frameworks or different projects, the method of obtaining the logged-in person of the day may be different, so the method of obtaining the current logged-in person can only be dynamically passed to the permission center through configuration. Add to the config file:
# The client gets the current login ID
permission.client.userid.method=com.raising.sc.permission.example.util.UserUtils.getUserId
Then use the Java reflection mechanism to trigger the getUserId( ) method.
Pass dynamic parameters
For example, user A can only query all data of his own unit and its subordinate units; the sql of the where part of the configuration center configuration is as follows:
org_cd like concat(${orgCd},'%')
Then read the above sql through PrepareInterceptor.java, and configure the corresponding permissions in the permission parameter (orgCd) in advance through the method associated with the parameter [orgCd] set in the database or configuration file (similar to the method of obtaining the current login ID). Method path, parameter value type, return value type, etc.
The configuration file or database gets the method path corresponding to orgCd:
com.raising.sc.permission.example.util.UserUtils.getRegionCdByUserId
Of course, this is just a simple dynamic parameter now, and the rest still need follow-up development, here is just the simplest attempt.
expand
From a product point of view, this module needs to consist of three parts:
1. foruo-permission-admin data permission management platform 2. foruo-permission-server data permission server (providing permission-related interfaces) 3. foruo-permission-client data permission client (encapsulating API)
The content of this module can be completed in conjunction with the application link logic diagram.
Knowledge points involved:
-
Mybatis interceptor
-
Java reflection mechanism
Project source code
Code Cloud: https://gitee.com/gmarshal/foruo-sc-permission
Related content recommendation: http://www.roncoo.com/course/list.html?courseName=mybatis