Mybatis-數據權限插件


  本文介紹在持久化層使用Mybatis時,如何自動實現數據權限的SQL拼接。實現思路是通過注解配置數據權限信息,通過Mybatis的插件功能,動態的修改執行的SQL。通過解析原查詢SQL和注解配置信息,拼接數據權限SQL到查詢條件中。

  

  1.配置注解

    使用注解,可以方便配置和業務邏輯處理。只對配置了注解的Mapper方法進行SQL增強處理。

    代碼示例:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataPermissions {
    /**
     * 權限控制信息
     * [{table_name:column_name[:role_type]}]
     * role_type 與 列名相同時,可以不寫
     * 例如:
     * @DataPermissions(
     *  {
     *      'order_info:dept_id:dept',
     *      'dept_info:dept_id'
     *   }
     * )
     * @return
     */
    String[] value() ;
}
@Mapper
public interface TestMapper {


    @DataPermissions({"f_goods_info:org_id:"+Constant.PERMISSIONS_TYPE_ALL_DEPT})
    List<FResult> listAll(FParamDTO param);

}

 

  2.創建插件

    Mybatis插件通過實現接口org.apache.ibatis.plugin.Interceptor完成。具體業務邏輯在方法intercept中進行實現.

  1 @Component
  2 @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,Integer.class})})
  3 public class PermissionsInterceptor implements Interceptor {
  4 
  5     @Autowired
  6     private PermissionsValueUtil valueUtil;
  7 
  8     private Logger log = LoggerFactory.getLogger(PermissionsInterceptor.class);
  9 
 10     @Override
 11     public Object intercept(Invocation invocation) throws Throwable {
 12         // 方法一
 13         StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
 14         MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
 15         //先攔截到RoutingStatementHandler,里面有個StatementHandler類型的delegate變量,其實現類是BaseStatementHandler,然后就到BaseStatementHandler的成員變量mappedStatement
 16         MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
 17         //id為執行的mapper方法的全路徑名,如com.uv.dao.UserMapper.insertUser
 18         String id = mappedStatement.getId();
 19         //sql語句類型 select、delete、insert、update
 20         String sqlCommandType = mappedStatement.getSqlCommandType().toString();
 21         BoundSql boundSql = statementHandler.getBoundSql();
 22 
 23         //獲取到原始sql語句
 24         String sql = boundSql.getSql();
 25         String[] permissionsValue = null;
 26 
 27         //注解邏輯判斷  添加注解了才攔截
 28         Class<?> classType = Class.forName(mappedStatement.getId().substring(0, mappedStatement.getId().lastIndexOf(".")));
 29         String mName = mappedStatement.getId().substring(mappedStatement.getId().lastIndexOf(".") + 1, mappedStatement.getId().length());
 30         for (Method method : classType.getDeclaredMethods()) {
 31             if (method.isAnnotationPresent(DataPermissions.class) && (mName.equals(method.getName()) || mName.equals(method.getName()+"_COUNT") )) {
 32                 DataPermissions permissions = method.getAnnotation(DataPermissions.class);
 33                 permissionsValue = permissions.value();
 34             }
 35         }
 36 
 37         if(permissionsValue != null && permissionsValue.length > 0){
 38             try {
 39                 List<PermissionCondition> permissionConditions = new ArrayList<PermissionCondition>();
 40                 for(String permissions : permissionsValue){
 41                     String[] strs = permissions.split(":");
 42                     if(strs.length >= 2){
 43                         String valueName = strs[1];
 44                         if(strs.length>2){
 45                             valueName = strs[2];
 46                         }
 47                         List<String> values = valueUtil.getValues(valueName);
 48                         if(CollectionUtils.isNotEmpty(values)){
 49                             PermissionCondition condition = new PermissionCondition();
 50                             condition.setTableName(strs[0]);
 51                             condition.setColumnName(strs[1]);
 52                             condition.setValues(values);
 53                             //
 54                              60                             condition.setOperator(PermissionCondition.CONDITION_IN);
 61  63                             permissionConditions.add(condition);
 64                         }
 65                     }
 66                 }
 67 
 68                 //生成增強權限控制后的SQL語句
 69                 String newSql = DataPermissionsFdbSqlParseUtil.convert(sql,permissionConditions);
 70                 if(StringUtils.isNotBlank(newSql)){
 71                     Field field = boundSql.getClass().getDeclaredField("sql");
 72                     field.setAccessible(true);
 73                     field.set(boundSql, newSql);
 74                 }
 75             } catch (Exception e) {
 76                 log.error("生成權限SQL時,發生錯誤!"+e.getMessage(),e);
 77                 log.error("CLASS:"+classType.getName());
 78                 log.error("METHOD:"+mName);
 79             }
 80         }
 81         return invocation.proceed();
 82     }
 83 
 84     @Override
 85     public Object plugin(Object target) {
 86         if (target instanceof StatementHandler) {
 87             return Plugin.wrap(target, this);
 88         } else {
 89             return target;
 90         }
 91 
 92     }
 93 
 94     @Override
 95     public void setProperties(Properties properties) {
 96 
 97     }
 98 
 99 
100 }
備注:PermissionsValueUtil 需要自己實現,通過自己的業務邏輯返回權限值List

  先獲取到執行的sql類和方法名(
_COUNT后綴的方法是,分頁插件生成的,涉及分頁插件時,注意特殊處理一下)。
  通過方法是否配置注解,判斷是否需要sql處理。如果配置了數據權限,就需要解析注解配置信息。然后通過配置信息和用戶信息進行邏輯處理(此處根據具體業務進行實現),返回數據權限具體值。
  生成增強SQL后,通過反射替換執行sql。
 1 public class PermissionCondition {
 2 
 3     public final static String CONDITION_EQUALS = " = ";
 4 
 5     public final static String CONDITION_LIKE = " LIKE ";
 6 
 7     public final static String CONDITION_START_WITH = " START_WITH ";
 8 
 9     public final static String CONDITION_IN= " in ";
10 
11     private String tableName;
12     private String columnName;
13     private String operator;
14     private List<String> values;
15

    

    3.SQL編輯

  編輯SQL需要解析,在查詢片段的指定部分插入對應條件。sql的解析是一件很麻煩的事情,因為不同的數據庫語法有差異,還有子查詢、函數等很多種情況。所以采用第三方解析JAR,本人采用的是foundationdb,實際應用上不是很方便,很多SQL經處理后變化很大,需要加很多特殊處理。如果有更好的選擇,盡量在做此功能時不要采用他

  依賴:

1         <dependency>
2             <groupId>com.foundationdb</groupId>
3             <artifactId>fdb-sql-parser</artifactId>
4             <version>1.3.0</version>
5         </dependency>
DataPermissionsFdbSqlParseUtil:
 1 /**
 2  * 權限控制SQL處理類
 3  */
 4 public class DataPermissionsFdbSqlParseUtil {
 5 
 6     private static Pattern paramPattern = Pattern.compile("\\$\\d*");
 7     private static String LIMIT_OFFSET_STR = "LIMIT ? OFFSET ?";
 8     private static String LIMIT_STR = "LIMIT ?, ? ";
 9 
10     private static final String[] PARSE_KEY_WORD = {"year"};
11 
12     public static String convert(String sql,List<PermissionCondition> permissionConditions){
13         try {
14 
15             sql = before(sql);
16             //原SQL解析
17             SQLParser parser = new SQLParser();
18             StatementNode stmt = parser.parseStatement(sql);
19 
20             //解釋的節點文件轉換成SQL,轉換的過程中在WHERE中增加權限條件
21             NodeToString unparser = new FNodeToString(permissionConditions);
22             String newSql = unparser.toString(stmt);
23 
24             Matcher matcher = paramPattern.matcher(newSql);
25             StringBuffer retStr = new StringBuffer();
26             while (matcher.find()) {
27                 matcher.appendReplacement(retStr, "?");
28             }
29             matcher.appendTail(retStr);
30             newSql = retStr.toString();
31             if(newSql.indexOf(LIMIT_OFFSET_STR)>0){
32                 newSql = newSql.replace(LIMIT_OFFSET_STR,LIMIT_STR);
33             }
34             if(newSql.indexOf("(INTERVAL")>=0){
35                 newSql = newSql.replaceAll("(\\()(INTERVAL \\d MONTH)(\\))","$2");
36             }
37             if(newSql.indexOf("CAST(1 AS INTERVAL YEAR)")>=0){
38                 newSql = newSql.replaceAll("\\(CAST\\(1 AS INTERVAL YEAR\\)\\)","INTERVAL 1 YEAR");
39             }
40             newSql = after(newSql);
41             System.out.println("ORI SQL :");
42             System.out.println(sql);
43             System.out.println("NEW SQL :");
44             System.out.println(newSql);
45             return newSql;
46         } catch (StandardException e) {
47             sql = after(sql);
48             e.printStackTrace();
49         }
50         return sql;
51     }
52 
53     public static String before(String sql){
54 
55         if(sql.indexOf("INTERVAL 1 YEAR")<0){
56             for(String kw : PARSE_KEY_WORD){
57                 int index = sql.toUpperCase().indexOf(kw.toUpperCase());
58                 while(index >= 0){
59                     String[] ks = kw.split("");
60                     String w =StringUtils.join(ks,"_");
61                     sql = sql.substring(0,index)+w+sql.substring(index+kw.length());
62                     index = sql.toUpperCase().indexOf(kw.toUpperCase());
63                 }
64             }
65         }
66         return sql;
67     }
68 
69     public static String after(String sql){
70         for(String kw : PARSE_KEY_WORD){
71             String[] ks = kw.split("");
72             String w =StringUtils.join(ks,"_");
73             int index = sql.toUpperCase().indexOf(w.toUpperCase());
74             while(index >= 0){
75                 sql = sql.substring(0,index)+kw+sql.substring(index+w.length());
76                 index = sql.toUpperCase().indexOf(w.toUpperCase());
77             }
78         }
79         return sql;
80     }81 }

  此處代碼比較混亂,因為在使用中發現很多時候好用SQL變成不好用了,加了很多特殊處理。

  通過FNodeToString類來處理SQL,處理后,根據具體情況,進行一些SQL修正。

 

  SQL編輯核心處理類 FNodeToString :
  核心業務邏輯是,在解析FROM時記錄表名和別名,然后在解析WHERE時(所以必須包含WHERE條件),根據表名和傳進來的權限信息,進行動態SQL拼接。處理完成后,清空FROM解析的表名,以便UNION或其他多段SQL情況時,解析不完全。

  通過fromList方法和parseTable方法進行表名解析和記錄,binaryLogicalOperatorNode、binaryComparisonOperatorNode、inListOperatorNode幾個方法是目前處理時,涉及可以添加where條件的方法。getWhere方法,生成where條件。里面的其他重寫方法,是為了處理sql解析時會出現的問題的特殊處理。
  如果使用的是其他JAR,可能不用我這里寫的這么混亂。

  1 /**
  2  * 節點轉換SQL
  3  * 過程中會根據權限配置,動態增加WHERE條件
  4  * 處理過程
  5  * 1.解析原SQL語句為節點結構
  6  * 2.將節點結構在組裝成SQL
  7  * 自定義處理部分
  8  * 1.解析出SQL包含的表
  9  * 2.根據表增加權限where條件
 10  */
 11 public class FNodeToString extends NodeToString {
 12 
 13 
 14     //SQL中解析設計的表
 15     private List<FdbSqlParseTable> tables = new ArrayList<>();
 16     //需要添加的權限
 17     private List<PermissionCondition> conditions = new ArrayList<>();
 18     //條件標識符
 19     private boolean initCondition = true;
 20     private String where = "";
 21 
 22     /**
 23      * FROM 節點
 24      *
 25      * @param node
 26      * @return
 27      * @throws StandardException
 28      */
 29     @Override
 30     protected String fromList(FromList node) throws StandardException {
 31         this.initCondition = true;
 32         String fromList = super.fromList(node);
 33         node.forEach(new Consumer<FromTable>() {
 34             @Override
 35             public void accept(FromTable fromTable) {
 36                 parseTable(fromTable);
 37             }
 38         });
 39         return fromList;
 40     }
 41 
 42     /**
 43      * 解析表
 44      * 主要解析,直接的表和 外關聯的表
 45      *
 46      * @param fromTable
 47      */
 48     private void parseTable(ResultSetNode fromTable) {
 49         if (fromTable == null) {
 50 
 51         } else if (fromTable instanceof FromBaseTable) {
 52             FromBaseTable baseTable = (FromBaseTable) fromTable;
 53             FdbSqlParseTable table = new FdbSqlParseTable(baseTable.getOrigTableName().getFullTableName(), baseTable.getCorrelationName());
 54             tables.add(table);
 55         } else if (fromTable instanceof FromSubquery) {
 56 //            FromSubquery subquery = (FromSubquery)fromTable;
 57 //            FdbSqlParseTable table = new FdbSqlParseTable(subquery.getExposedName(),subquery.getCorrelationName());
 58 //            tables.add(table);
 59 //            System.out.println(table);
 60         } else if (fromTable instanceof HalfOuterJoinNode) {
 61             HalfOuterJoinNode outerJoinNode = (HalfOuterJoinNode) fromTable;
 62             parseTable(outerJoinNode.getLeftResultSet());
 63             parseTable(outerJoinNode.getRightResultSet());
 64         }else if (fromTable instanceof JoinNode) {
 65             JoinNode joinNode = (JoinNode) fromTable;
 66             parseTable(joinNode.getLeftResultSet());
 67             parseTable(joinNode.getRightResultSet());
 68         } else {
 69         }
 70     }
 71 
 72     @Override
 73     protected String subqueryNode(SubqueryNode node) throws StandardException {
 74         String subQuery = super.subqueryNode(node);
 75         System.out.println("subQuery =>" + subQuery);
 76         return subQuery;
 77     }
 78 
 79     @Override
 80     protected String fromSubquery(FromSubquery node) throws StandardException {
 81         String fromSubquery = super.fromSubquery(node);
 82         System.out.println("fromSubquery =>" + fromSubquery);
 83         return fromSubquery;
 84     }
 85 
 86     @Override
 87     protected String unionNode(UnionNode node) throws StandardException {
 88         String union = " UNION ";
 89         if(node.isAll()){
 90             union += " ALL ";
 91         }
 92         String sql = this.toString(node.getLeftResultSet()) + union + this.toString(node.getRightResultSet());
 93         return sql;
 94     }
 95 
 96     @Override
 97     protected String javaToSQLValueNode(JavaToSQLValueNode node) throws StandardException {
 98         JavaValueNode jNode = node.getJavaValueNode();
 99 
100         if (jNode != null && jNode instanceof StaticMethodCallNode) {
101             StaticMethodCallNode smcn = (StaticMethodCallNode) jNode;
102             if ("CONCAT".equals(smcn.getMethodName().toUpperCase())) {
103 
104                 JavaValueNode[] values = smcn.getMethodParameters();
105                 if (values != null) {
106                     Object[] vArray = new Object[values.length];
107                     for(int i=0;i<values.length;i++){
108                         JavaValueNode v = values[i];
109                         if(v instanceof  SQLToJavaValueNode){
110                             ValueNode vn = ((SQLToJavaValueNode)v).getSQLValueNode();
111                             if(vn instanceof  ColumnReference){
112                                 vArray[i] = ((ColumnReference)vn).getColumnName();
113                             }else if(vn instanceof CharConstantNode){
114                                 vArray[i] = "'"+((CharConstantNode)vn).getValue()+"'";
115                             }else if(vn instanceof ParameterNode){
116                                 vArray[i] = "?";
117                             }
118                         }else{
119                             vArray[i] = v;
120                         }
121 
122                     }
123                     return " CONCAT(" + StringUtils.join(vArray, ",") + ") ";
124                 }
125             }
126 
127         }
128         return super.javaToSQLValueNode(node);
129     }
130 
131     @Override
132     protected String resultColumnList(ResultColumnList node) throws StandardException {
133         return super.resultColumnList(node);
134     }
135 
136     protected String aggregateNode(AggregateNode node) throws StandardException {
137         String distinct = node.isDistinct()?"DISTINCT ":"";
138         return node.getOperand() == null?node.getAggregateName():node.getAggregateName() + "("+distinct + this.toString(node.getOperand()) + ")";
139     }
140 
141     @Override
142     protected String resultColumn(ResultColumn node) throws StandardException {
143         String ret = super.resultColumn(node);
144         ValueNode expNode = node.getExpression();
145         if (expNode != null && expNode instanceof GroupConcatNode) {
146             ValueNode operNode = ((GroupConcatNode) expNode).getOperand();
147             if (operNode != null && operNode instanceof JavaToSQLValueNode) {
148                 String str = javaToSQLValueNode((JavaToSQLValueNode)operNode);
149                 String operNodeStr = operNode.toString();
150                 ret = ret.replace(operNodeStr,str);
151             }
152         }
153         return ret;
154     }
155 
156     /**
157      * 解析WHERE 條件節點
158      *
159      * @param node
160      * @return
161      * @throws StandardException
162      */
163     @Override
164     protected String binaryLogicalOperatorNode(BinaryLogicalOperatorNode node) throws StandardException {
165         String binaryLogicalOperatorNode = super.binaryLogicalOperatorNode(node);
166         //條件節點會有多個,但權限條件的增加只調用一次
167         if (this.initCondition && tables.size() > 0) {
168             //生成WHERE 語句
169             this.where = getWhere(this.tables, this.conditions);
170             this.initCondition = false;
171             if (StringUtils.isNotBlank(this.where)) {
172                 binaryLogicalOperatorNode = this.where + " AND " + binaryLogicalOperatorNode;
173             }
174             this.tables.clear();
175         }
176 //        System.out.println("binaryLogicalOperatorNode =>" + binaryLogicalOperatorNode);
177         return binaryLogicalOperatorNode;
178     }
179 
180 //    binaryComparisonOperatorNode
181 
182     @Override
183     protected String binaryComparisonOperatorNode(BinaryComparisonOperatorNode node) throws StandardException {
184         String binaryLogicalOperatorNode = super.binaryComparisonOperatorNode(node);
185         if (this.initCondition && tables.size() > 0) {
186             //生成WHERE 語句
187             this.where = getWhere(this.tables, this.conditions);
188             this.initCondition = false;
189             if (StringUtils.isNotBlank(this.where)) {
190                 binaryLogicalOperatorNode = this.where + " AND " + binaryLogicalOperatorNode;
191             }
192             this.tables.clear();
193         }
194         return binaryLogicalOperatorNode;
195     }
196 
197     @Override
198     protected String inListOperatorNode(InListOperatorNode node) throws StandardException {
199         String inListOperatorNode = super.inListOperatorNode(node);
200         if (this.initCondition && tables.size() > 0) {
201             //生成WHERE 語句
202             this.where = getWhere(this.tables, this.conditions);
203             this.initCondition = false;
204             if (StringUtils.isNotBlank(this.where)) {
205                 inListOperatorNode = this.where + " AND " + inListOperatorNode;
206             }
207             this.tables.clear();
208         }
209         return inListOperatorNode;
210     }
211 
212     @Override
213     protected String castNode(CastNode node) throws StandardException {
214         String castNodeSql =  super.castNode(node);
215         if(node.getType().toString().equals("INTERVAL MONTH")){
216             castNodeSql = "INTERVAL "+this.toString(node.getCastOperand())+" MONTH";
217         }
218         return castNodeSql;
219     }
220 
221 
222     /**
223      * 將權限語句生成WHERE 條件
224      *
225      * @param tables
226      * @param conditions
227      * @return
228      */
229     public static String getWhere(List<FdbSqlParseTable> tables, List<PermissionCondition> conditions) {
230         StringBuffer ret = new StringBuffer();
231         if (tables != null && conditions != null) {
232             //解析表名
233             Map<String, FdbSqlParseTable> tableMap = new HashMap<String, FdbSqlParseTable>();
234             for (FdbSqlParseTable table : tables) {
235                 tableMap.put(table.getTableName(), table);
236             }
237             boolean first = true;
238             //處理條件
239             for (PermissionCondition condition : conditions) {
240                 FdbSqlParseTable table = tableMap.get(condition.getTableName());
241                 //解析出該表時才動態添加該條件 and
242                 if (table != null) {
243                     if (first) {
244                         first = false;
245                     } else {
246                         ret.append(" AND ");
247                     }
248                     //條件
249                     ret.append(condition.toWhere(table.getAlias()));
250                 }
251             }
252         }
253         return ret.toString();
254     }
255 
256     public BrokerNodeToString(List<PermissionCondition> conditions) {
257         this.conditions = conditions;
258     }
259 
260     public List<FdbSqlParseTable> getTables() {
261         return tables;
262     }
263 
264     public void setTables(List<FdbSqlParseTable> tables) {
265         this.tables = tables;
266     }
267 
268     public List<PermissionCondition> getConditions() {
269         return conditions;
270     }
271 
272     public void setConditions(List<PermissionCondition> conditions) {
273         this.conditions = conditions;
274     }
275 
276     public boolean isInitCondition() {
277         return initCondition;
278     }
279 
280     public void setInitCondition(boolean initCondition) {
281         this.initCondition = initCondition;
282     }
283 
284     public String getWhere() {
285         return where;
286     }
287 
288     public void setWhere(String where) {
289         this.where = where;
290     }
291 }

     4.配置插件

1 <plugins>
2         <plugin interceptor="com.f.common.mybatis.PermissionsInterceptor">
3         </plugin>
4     </plugins>

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM