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

【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://www.xdnf.cn/news/20264.html

相关文章:

  • C++和OpenGL实现3D游戏编程【连载30】——文字的多行显示
  • MySQL集群——主从复制进阶
  • 2025年上海市星光计划第十一届职业院校技能大赛高职组“信息安全管理与评估”赛项交换部分前6题详解(仅供参考)
  • FlashAttention:突破Transformer内存瓶颈的IO感知革命
  • Web漏洞挖掘篇(二)—信息收集
  • 浪潮CD1000-移动云电脑-RK3528芯片-2+32G-安卓9-2种开启ADB ROOT刷机教程方法
  • Chat with RTX-NVIDIA推出的本地AI聊天机器人
  • .NET Core 应用部署深度解析:从 IIS 到 Docker+Kestrel 的迁移与性能优化实战
  • 电脑音频录制 | 系统麦克混录 / 系统声卡直录 | 方法汇总 / 常见问题
  • Unity与硬件交互终极指南:从Arduino到自定义USB设备
  • 零基础Linux操作基础小白快速掌握Shell脚本--流程控制和循环(二)
  • CAD:注释
  • PPTist,一个完全免费的 AI 生成 PPT 在线网站
  • 贪心算法应用:流行病干预策略问题详解
  • redis的数据类型:Hash
  • 【数据结构】带哨兵位双向循环链表
  • 50系显卡训练深度学习YOLO等算法报错的解决方法
  • 《动手学深度学习v2》学习笔记 | 2.4 微积分 2.5 自动微分
  • 深度学习——PyTorch保存模型与调用模型
  • JUC之并发编程
  • MyBatis入门到精通:CRUD实战指南
  • 使用UniApp实现下拉框和表格组件页面
  • Android Kotlin 动态注册 Broadcast 的完整封装方案
  • uv教程 虚拟环境
  • kotlin - 2个Fragment实现左右显示,左边列表,右边详情,平板横、竖屏切换
  • 【LeetCode 每日一题】2348. 全 0 子数组的数目
  • 开源OpenHarmony润开鸿HH-SCDAYU800A开发板开箱体验
  • AI热点周报(8.31~9.6): Qwen3‑Max‑Preview上线、GLM-4.5提供一键迁移、Gemini for Home,AI风向何在?
  • C++进阶——继承(2)
  • 基于STM32的交通灯设计—紧急模式、可调时间