SSM+MySQL已是Java开发的主流架构。在SSM+MySQL架构中,SQL一般写在mapper映射文件中,对于mapper文件的审计一直是个头疼的问题,测试人员很难覆盖mapper文件中动态SQL的所有情况,开发人员更是无法罗列每一个select id对应的所有SQL。在和开发同事沟通后,觉得自己可以写一个小工具来解决这一痛点!笔者作为一名DBA,自学了golang,经过七个月的努力初步完成了myaudit这一款实用性极强的SQLAudit小工具的开发,欢迎使用和吐槽,项目地址:https://github.com/gaozhongzheng/myaudit 。
下面简单介绍下myaudit这个小工具:
myaudit是一个审计SSM + MySQL开发架构中SQL映射文件(xml文件)的SQLAudit工具。
设计思路如下:
- 1.格式化xml文件
- 2.重写xml文件
- 3.构造Mybatis工程
- 4.调起Mybatis工程打印所有可能的SQL到日志
- 5.解析日志获取SQL,并根据动态字段类型将SQL中的'?'替换为具体值
-
6.调用SOAR获取审计报告
为了上首页,上一段代码:func ReplaceQuestionMark(query string, db *sql.DB, dbName string) string { stmt, err := sqlparser.Parse(query) if err != nil { common.LogIfError(err, "parse failed: "+query) return query } columns, tables, _, err := GetInfo(stmt) if err != nil { common.LogIfError(err, "get info failed: "+query) return query } var column_name string var data_type string column_type := make(map[string]string) for _, tab := range tables { rows, _ := db.Query("select column_name, data_type from columns where table_name = ? and TABLE_SCHEMA = '"+dbName+"'", tab) for rows.Next() { if err := rows.Scan(&column_name, &data_type); err == nil { if _, ok := column_type[column_name]; !ok { column_type[column_name] = data_type } } else { fmt.Println(err) } } rows.Close() } i := 0 for _, col := range columns { if column, ok := column_type[col[0]]; ok { i++ delete(column_type, col[0]) // fmt.Println("here is", query) question_index := GetIndex(query, "?") if question_index != nil { // fmt.Println("this is :", question_index) idx, _ := strconv.Atoi(col[1]) // fmt.Println("idx is :", idx) index := question_index[idx-i] // fmt.Println("index is :", index) if strings.Contains(column, "char") || strings.Contains(column, "text") || strings.Contains(column, "lob") { query = query[:index] + "'SQLAudit'" + query[index+1:] } else if strings.Contains(column, "int") { query = query[:index] + "1" + query[index+1:] } else if strings.Contains(column, "decimal") || strings.Contains(column, "float") || strings.Contains(column, "double") || strings.Contains(column, "numeric") { query = query[:index] + "1.1" + query[index+1:] } else if strings.Contains(column, "timestamp") || strings.Contains(column, "datatime") { query = query[:index] + "'2019-01-01 00:00:00'" + query[index+1:] } else if strings.Contains(column, "date") { query = query[:index] + "'2019-01-01'" + query[index+1:] } else if strings.Contains(column, "time") { query = query[:index] + "'00:00:00'" + query[index+1:] } else if strings.Contains(column, "year") { query = query[:index] + "'2019'" + query[index+1:] } else if strings.Contains(column, "enum") { query = query[:index] + "'SQLAudit'" + query[index+1:] } else { query = query[:index] + "none" + query[index+1:] } } } } stmt, err = sqlparser.Parse(query) if err != nil { common.LogIfError(err, "parse failed: "+query) return query } _, _, limits, err := GetInfo(stmt) if err != nil { common.LogIfError(err, "get info failed: "+query) return query } // 替换limit中的? for _, ques := range limits { question_index := GetIndex(query, "?") if ques[0] != -1 { index := question_index[ques[0]-1] query = query[:index] + "1" + query[index+1:] } if ques[1] != -1 { index := question_index[ques[1]-1] query = query[:index] + "1" + query[index+1:] } } // TODO group by和order by中的?替换。 // 放弃,因为group by和order by的对象是列而不是值,而列我们是不知道的,所以没法替换 // 打印信息 // fmt.Println("columns:", columns) // fmt.Println("tables:", tables) // fmt.Println("limits:", limits) // fmt.Println("new query:", query) return query }
特点:
- 1.与业务代码深度解耦
- 2.解析并审计动态SQL所有可能的情况
- 3.根据字段类型替换绑定变量值,解决了SQL指纹无法在MySQL中获取执行计划的问题
安装/部署(Linux)
方式一
- step1.克隆源代码至本地
- step2.编译命令文件:cd cmd;go build myaudit.go
- step3.执行createDir.sh创建相应目录,目录含义见目录说明。
- step4.上传log4j-1.2.17.jar、mybatis-3.2.8.jar、mysql-connector-java-5.1.47.jar至/usr/local/myaudit/lib,可从此处获取jar包
- step5.将step2中编译好的命令文件拷贝至/usr/local/myaudit/bin并将此目录添加至环境变量
注:需先安装SOAR
方式二(免编译快速部署)
- step1.克隆deployment至本地
- step2.将/usr/local/myaudit/bin添加至环境变量
使用myaudit(如有疑问或建议,请邮件至[email protected])
Usage of myaudit: -ftype mapper|slowlog|normal, default mapper --待审计文件种类,默认为mapper(暂时只支持Mybatis项目中的mapper文件) -fname file waitting for audit --待审计文件名,不需加扩展名,如MapperSakila.xml传参时为MapperSakila -conn $IP:$PORT/$DB, like 127.0.0.1:3306/sakila --测试环境连接串,待审计文件对应的测试环境数据库 -u username --MySQL用户名 -p password --MySQL用户密码 -report-type html|json, default html --报告类型,默认为html
- step1.上传待审计的SQL映射文件至/usr/local/myaudit/file
- step2.执行审计(示例):
myaudit -ftype mapper -fname MapperSakila -conn 127.0.0.1:3306/sakila -u root -p 123456 -report-type html Dir '/usr/local/myaudit/tmp/MapperSakila' was created/rebuilt successfully! /usr/local/myaudit/file/MapperSakila.xml read ok! /usr/local/myaudit/tmp/MapperSakila/tmp.xml read ok! /usr/local/myaudit/tmp/MapperSakila/oldMapper.xml read ok! /usr/local/myaudit/tmp/MapperSakila/oldMapperFormated.xml read ok! Compile and execute java files successfully! /usr/local/myaudit/log/mapperLog/MapperSakila.log read ok! Database connect success! Dir '/usr/local/myaudit/sql/MapperSakila' was created/rebuilt successfully! Dir '/usr/local/myaudit/audit/MapperSakila' was created/rebuilt successfully! xml file was successfully audited, you can find the audit file 'getFilmInfoTest1_audit.html' in directory /usr/local/myaudit/audit/MapperSakila. xml file was successfully audited, you can find the audit file 'getFilmInfoTest2_audit.html' in directory /usr/local/myaudit/audit/MapperSakila.
从MapperSakila.xml解析出的SQL文件在此。
审计报告截图: