当前位置: 首页 > news >正文

【Go项目基建】GORM框架实现SQL校验拦截器(完整源码+详解)

背景

阅读之前实习的公司里的文档观察到,之前组内的同事在Java项目当中编写了一个基于Mybatis和JSQLParser SQL开发的SQL统一校验拦截器,有效的规避了一定程度上的慢查问题

考虑到部门内相关的新业务功能都在Go当中开发

因此也针对以下情况

  1. 列表查询无查询条件导致全表扫描,
  2. 查询SQL复杂关联多表查询

设计一个封装统一的SQL校验工具引入项目中避免慢SQL产生,以减少整个部门技术SQL慢查数量

功能介绍

  1. 支持GORM持久层框架的SQL查询
  2. 校验SQL语句是否具备where查询条件
  3. 校验SQL语句where查询条件是否为有效查询(排除 where 1=1, where is_deleted="N" 此类)
  4. 支持指定白名单表不校验
  5. 支持校验开关配置
  6. 支持多表关联查询限制连接表数量
  7. 项目启动时,会生成日志记录

TODO

  1. 无索引,索引不生效的情况,拿到表的索引,与当前的SQL进行分析
  2. 数据量每天统计增长情况,每张表定一个数据量级,增长趋势,量级预警报告

版本

1.0.0

sql拦截校验,白名单表配置,多表关联查询限制数量

如何使用

1. 新建文件,复制校验代码

在util/component包下新建一个go文件

package utilimport ("app/config/logger""fmt""regexp""strings""gorm.io/gorm"
)// SQLValidatorPlugin GORM SQL校验插件(避免全表扫描和过多表关联)
type SQLValidatorPlugin struct {allowList    map[string]bool // 允许全表查询的表名白名单(不区分大小写)maxJoinCount int             // 最大允许的表关联数量
}// NewSQLValidatorPlugin 创建插件实例
// allowList:允许全表查询的表名列表
// maxJoinCount:最大允许的表关联数量,0表示不限制
func NewSQLValidatorPlugin(maxJoinCount int, allowList []string) *SQLValidatorPlugin {allowMap := make(map[string]bool, len(allowList))for _, table := range allowList {allowMap[strings.ToLower(table)] = true}// 输出启动日志logger.Info("SQL校验拦截器已开启,配置信息:最大表关联数量=%d,白名单表=%v", maxJoinCount, allowList)return &SQLValidatorPlugin{allowList:    allowMap,maxJoinCount: maxJoinCount,}
}func (p *SQLValidatorPlugin) Name() string {return "sql_validator"
}// Initialize 初始化插件
func (p *SQLValidatorPlugin) Initialize(db *gorm.DB) error {// 将回调注册在"gorm:prepare_stmt"之后,此时SQL已初步构建return db.Callback().Query().After("gorm:prepare_stmt").Register(p.Name(), func(db *gorm.DB) {if err := p.validateSQL(db); err != nil {logger.Errorf("SQL校验失败: %v, 具体SQL: %s", err, db.Dialector.Explain(db.Statement.SQL.String(), db.Statement.Vars...))db.AddError(err)}})
}// 预编译正则
var (selectRegex            = regexp.MustCompile(`(?i)^SELECT`)whereRegex             = regexp.MustCompile(`(?i)\bWHERE\b`)limitOneRegex          = regexp.MustCompile(`(?i)LIMIT\s+1`)fromTableRegex         = regexp.MustCompile(`(?i)FROM\s+([^\s,]+)`)identityConditionRegex = regexp.MustCompile(`(?i)WHERE\s+(\d+)\s*=+\s*\\1\s*(AND|OR|$)`)isDeletedOnlyRegex     = regexp.MustCompile(`(?i)WHERE\s*([\w]+\.)?is_deleted\s*=`)hasLogicOpRegex        = regexp.MustCompile(`(?i)\bAND\b|\bOR\b`)joinRegex              = regexp.MustCompile(`(?i)\b(INNER|LEFT|RIGHT|FULL|CROSS)\s+JOIN\b|\bJOIN\b`)
)// validateSQL 核心校验逻辑(使用GORM原生方法获取完整SQL)
func (p *SQLValidatorPlugin) validateSQL(db *gorm.DB) error {if db.Error != nil {return nil}// 通过Dialector.Explain获取带参数的完整SQLfullSQL := db.Dialector.Explain(db.Statement.SQL.String(), db.Statement.Vars...)if fullSQL == "" {// SQL未生成,跳过校验return nil}cleanSQL := removeComments(fullSQL)// 1. 检查多表关联数量if p.maxJoinCount > 0 {joinCount := p.countJoins(cleanSQL)if joinCount > p.maxJoinCount {return fmt.Errorf("表关联数量超过限制(最大允许: %d, 实际: %d),具体SQL: %s",p.maxJoinCount, joinCount, cleanSQL)}}// 2. 基础规则校验isSelect := selectRegex.MatchString(cleanSQL)hasWhere := whereRegex.MatchString(cleanSQL)hasLimitOne := limitOneRegex.MatchString(cleanSQL)isOnlyIdentity := hasWhere && identityConditionRegex.MatchString(cleanSQL)isOnlyIsDeleted := falseif hasWhere && isDeletedOnlyRegex.MatchString(cleanSQL) {wherePart := p.extractWhereClause(cleanSQL)isOnlyIsDeleted = !hasLogicOpRegex.MatchString(wherePart)}if !isSelect || hasLimitOne || (!isOnlyIdentity && !isOnlyIsDeleted) {return nil}// 3. 白名单校验tableName := extractMainTableName(cleanSQL)if tableName != "" && p.allowList[strings.ToLower(tableName)] {return nil}return fmt.Errorf("禁止执行等效全表扫描的SELECT查询,具体SQL: %s", cleanSQL)
}// 计算表关联数量
func (p *SQLValidatorPlugin) countJoins(sql string) int {// 查找所有匹配的JOIN关键字matches := joinRegex.FindAllString(sql, -1)return len(matches)
}// 提取WHERE子句
func (p *SQLValidatorPlugin) extractWhereClause(sql string) string {whereIdx := strings.Index(strings.ToUpper(sql), "WHERE")if whereIdx == -1 {return ""}whereContent := sql[whereIdx+5:]for _, clause := range []string{"GROUP BY", "ORDER BY", "LIMIT", "OFFSET"} {if idx := strings.Index(strings.ToUpper(whereContent), clause); idx != -1 {whereContent = whereContent[:idx]}}return strings.TrimSpace(whereContent)
}// 移除注释
func removeComments(sql string) string {re := regexp.MustCompile(`(?s)(--.*?\n|/\*.*?\*/)`)return re.ReplaceAllString(sql, " ")
}// 提取主表名
func extractMainTableName(sql string) string {matches := fromTableRegex.FindStringSubmatch(sql)if len(matches) < 2 {return ""}tablePart := strings.Split(matches[1], " ")[0]return strings.Trim(tablePart, "`'\"")
}

需要注意,日志的几行需要根据项目所用日志进行变动

目前front-go当中已经封装好了logrus的方法

2. application.yml 增加配置
#SQL校验拦截器相关配置
sqlValidator:# 校验开关enable: true# 允许全表查询的表名白名单(不区分大小写)allowList: ["vehicle_claim_push_repair_case"]# 最大允许的表关联数量,0表示不限制maxJoinCount: 0

3. main方法当中初始化校验拦截器
// 初始化数据库信息
db := database.Init(logger.GetLogger())
// Sql校验拦截器开关配置
if config.GetString("sqlValidator.enable") == constant.Flag.True {// 开启Sql校验拦截器// 获取白名单allowList := config.GetStringSlice("sqlValidator.allowList")// 获取最大允许的表关联数量maxJoinCount := config.GetInt("sqlValidator.maxJoinCount")db.Use(util.NewSQLValidatorPlugin(maxJoinCount, allowList))
}

在初始化数据库信息之后,从配置文件当中校验开关

如果开关开启,那么从配置文件获取白名单和最大允许的表关联数量

然后初始化校验拦截器

实现原理

该 SQL 校验拦截器基于 GORM 插件机制,在 SQL 查询执行前对 SQL 语句进行多维度校验,以避免全表扫描和过多表关联带来的性能问题,其核心原理如下

1. 插件注册与执行时机

通过 GORM 的回调机制,将校验逻辑注册在 gorm:prepare_stmt 之后执行。此时 SQL 已初步构建完成,能获取到较为完整的 SQL 语句,确保校验的准确性。

2. SQL语句的完整获取

利用 GORM 自身的 Dialector.Explain 方法,获取最终要执行的、带参数的完整 SQL 语句,避免因手动构建 SQL 可能导致的语法错误等问题

3. 多维度校验

表关联数量校验

使用正则表达式 joinRegex 匹配 SQL 中的各类 JOIN 关键字(如 INNER JOINLEFT JOIN 等),统计表关联的数量。若关联数量超过配置的 maxJoinCount(且 maxJoinCount 大于 0),则判定为非法查询,阻止执行。

// 预编译正则
var (selectRegex            = regexp.MustCompile(`(?i)^SELECT`)whereRegex             = regexp.MustCompile(`(?i)\bWHERE\b`)limitOneRegex          = regexp.MustCompile(`(?i)LIMIT\s+1`)fromTableRegex         = regexp.MustCompile(`(?i)FROM\s+([^\s,]+)`)identityConditionRegex = regexp.MustCompile(`(?i)WHERE\s+(\d+)\s*=+\s*\\1\s*(AND|OR|$)`)isDeletedOnlyRegex     = regexp.MustCompile(`(?i)WHERE\s*([\w]+\.)?is_deleted\s*=`)hasLogicOpRegex        = regexp.MustCompile(`(?i)\bAND\b|\bOR\b`)joinRegex              = regexp.MustCompile(`(?i)\b(INNER|LEFT|RIGHT|FULL|CROSS)\s+JOIN\b|\bJOIN\b`)
)

基础查询规则校验

  • SELECT 语句判断:通过 selectRegex 判断是否为 SELECT 语句,非 SELECT 语句直接放行。
  • LIMIT 1 放行:若 SQL 中包含 LIMIT 1(由 limitOneRegex 匹配),说明是简单查询,直接放行。
  • WHERE 子句有效性校验
    • 若存在 WHERE 子句,但仅为恒等条件(如 1=1,由 identityConditionRegex 匹配),判定为无效查询。
    • 若存在 WHERE 子句,但仅包含逻辑删除字段 is_deleted 的条件且无其他有效筛选条件(由 isDeletedOnlyRegexhasLogicOpRegex 配合判断),也判定为无效查询。
    • 若不满足上述无效情况(即 WHERE 子句包含有效筛选条件),则放行。
// 2. 基础规则校验isSelect := selectRegex.MatchString(cleanSQL)hasWhere := whereRegex.MatchString(cleanSQL)hasLimitOne := limitOneRegex.MatchString(cleanSQL)isOnlyIdentity := hasWhere && identityConditionRegex.MatchString(cleanSQL)isOnlyIsDeleted := falseif hasWhere && isDeletedOnlyRegex.MatchString(cleanSQL) {wherePart := p.extractWhereClause(cleanSQL)isOnlyIsDeleted = !hasLogicOpRegex.MatchString(wherePart)}if !isSelect || hasLimitOne || (!isOnlyIdentity && !isOnlyIsDeleted) {return nil}

白名单校验

提取 SQL 中的主表名(由 fromTableRegex 匹配并处理),若主表名在配置的白名单 allowList 中(不区分大小写),则放行该查询,允许全表扫描等操作。

// 3. 白名单校验tableName := extractMainTableName(cleanSQL)if tableName != "" && p.allowList[strings.ToLower(tableName)] {return nil}
4. 错误处理

若校验不通过,会记录错误日志(包含具体 SQL 语句),并通过 db.AddError 方法将错误绑定到 GORM 上下文,阻断后续 SQL 执行,防止低效查询对数据库性能造成影响

仓库地址

因为是Go应用,不需要依赖,直接把文件复制过去复用即可


文章转载自:

http://EZVBP3Z1.sfqtf.cn
http://UVX8Z2yf.sfqtf.cn
http://Lzz4ak4l.sfqtf.cn
http://ZMf17pjL.sfqtf.cn
http://ZcOYHcAt.sfqtf.cn
http://5t7uFy1f.sfqtf.cn
http://14VKOGw6.sfqtf.cn
http://6A3hFK3k.sfqtf.cn
http://bWvFrrDz.sfqtf.cn
http://cvnRMyMr.sfqtf.cn
http://TFuoKF0P.sfqtf.cn
http://NyAXvKIm.sfqtf.cn
http://NvAS5zJW.sfqtf.cn
http://iqRJTZBI.sfqtf.cn
http://L711i39Y.sfqtf.cn
http://8nXbPsTy.sfqtf.cn
http://R47PPN3o.sfqtf.cn
http://sbIXhpJd.sfqtf.cn
http://RdF5BJxu.sfqtf.cn
http://jXF338cl.sfqtf.cn
http://01XcjW6i.sfqtf.cn
http://LCGKGJtT.sfqtf.cn
http://lPgWPZgM.sfqtf.cn
http://OjBEG6Zq.sfqtf.cn
http://tHNpQ3vv.sfqtf.cn
http://wMT9ikZp.sfqtf.cn
http://hLvNZAqV.sfqtf.cn
http://GDT9FAeI.sfqtf.cn
http://4pCscv8v.sfqtf.cn
http://DWE5HA8h.sfqtf.cn
http://www.dtcms.com/a/370825.html

相关文章:

  • 数据结构——栈(Java)
  • golang连接influxdb的orm操作
  • C#中一段程序类比博图
  • rh134第三章复习总结
  • Spring的事件监听机制(一)
  • 【鸿蒙 NEXT】V1迁移V2状态管理
  • FRCNet
  • git 冲突,Merge
  • NAND Flash块擦除与数据状态解析
  • 分享一个基于Python+大数据的房地产一手房成交数据关联分析与可视化系统,基于机器学习的深圳房产价格走势分析与预测系统
  • 超文本的定义
  • LeetCode 2461.长度为K子数组中的最大和
  • 【机器学习入门】6.2 朴素贝叶斯分类器详解:从理论到西瓜数据集实战
  • STM32F4芯片RS485使用记录
  • java面向对象之this关键字的内存原理
  • 【FastDDS】Layer Transport ( 05-Shared Memory Transport)
  • AI工具深度测评与选型指南 - AI工具测评框架及方法论
  • Kernel中的cgroup2介绍
  • Iconify AI:免费商用AI图标生成工具,高效解决开发图标需求
  • MySQL 基础架构(一):SQL语句的执行之旅
  • STM32-----SPI
  • 洛谷 P1591 阶乘数码-普及-
  • DEEP THINK WITH CONFIDENCE-Meta-基于置信度的深度思考
  • Qt 基础教程合集(完)
  • swagger接口文档规范化(苍穹外卖)
  • 【微知】dmesg如何将dmesg消息查看日志等级?(dmesg -x; prefix)
  • 基于STM32智能阳台监控系统
  • Ubuntu 22.04.1上安装MySQL 8.0及设置root密码
  • 【混元AIGC+腾讯云智能体+首创Coze核心流思维导图MCP】:打造一个文思通-智能写作助手Agent
  • B.50.10.09-RPC核心原理与电商应用