Files
2026-05-24 11:42:38 +08:00

4.6 KiB
Raw Permalink Blame History

tags, create time
tags create time
后端
Go
Gin
绑定
安全
2026-04-28 00:00

跳过绑定 binding:"-"

概述

在 Gin 的自动绑定机制中,结构体标签 binding:"-" 用于完全排除某个字段,使其不参与任何请求数据的绑定和校验。这是防止前端篡改敏感数据的第一道防线。

正文

工作原理

Gin 在调用 ShouldBind / ShouldBindJSON 等方法时,底层会遍历结构体的所有字段,检查其 binding 标签:

type User struct {
    ID    uint      `json:"id" gorm:"primaryKey"`
    Name  string    `json:"name" binding:"required"`   // ✅ 正常绑定 + 校验
    Role  string    `json:"role" binding:"required"`   // ✅ 正常绑定 + 校验
    Admin bool      `json:"admin" binding:"-"`         // ❌ 完全不参与绑定
}

当请求携带 { "name": "Alice", "role": "admin", "admin": true } 时:

字段 绑定行为 最终值
Name 从 JSON 取值 "Alice"
Role 从 JSON 取值 "admin"
Admin 直接忽略,读取不到 false(Go 零值)

为什么需要跳过绑定?

最常见且最重要的场景是权限防护。

[!CAUTION] 没有 binding:"-" 的风险

假设接口设计如下:

type CreateUserRequest struct {
    Name  string `json:"name" binding:"required"`
    Role  string `json:"role" binding:"required"`
    Admin bool   `json:"admin"` // 忘记加 "-"!
}

恶意用户只需在请求中添加 "admin": true,就能把自己创建为管理员——因为 Gin 会自动把前端传来的值填入结构体。数据绑定时机远早于任何业务校验逻辑,框架不会区分"普通字段"和"特权字段"。

跳过绑定 vs 不序列化

binding:"-" 与 json:"-" 是两个不同的概念,经常组合使用:

type User struct {
    ServerID uint   `json:"-" gorm:"primaryKey"`  // 不序列化、不绑定
    Internal string `binding:"-"`                 // 可序列化但不绑定
    Admin    bool   `json:"admin" binding:"-"`    // 可序列化但不绑定
}
标签 影响范围 效果
binding:"-" 只读入方向(请求 → 结构体) 请求中的值不会被填入该字段
json:"-" 只写方向(结构体 → JSON) 该字段不会出现在 JSON 响应中
两个同时存在 双向隔离 字段完全不可见
flowchart TD
    A["客户端请求"] --> B{"是否有 binding 排除标签"}
    B -->|"是"| C["值被丢弃"]
    B -->|"否"| D["填入结构体字段"]
    D --> E["业务逻辑处理"]
    E --> F{"是否有 json 排除标签"}
    F -->|"是"| G["不出现在响应 JSON"]
    F -->|"否"| H["返回给客户端"]
    C -.->|"保持 Go 零值"| E

实战清单

在以下字段上务必加上 binding:"-":

  • 权限相关:Admin、IsSuperuser、PermissionLevel
  • 服务端生成:ID(通常由数据库自增)、CreatedAt、UpdatedAt
  • 计算派生:FullName(由 FirstName + LastName 拼接)、Balance(由账户记录汇总)
  • 内部状态:retryCount、lockVersion、tenantID(应从 JWT Token 中提取)

提问: 如果 ID 已经在路由参数中通过 c.Param("id") 提取了,还需要加 binding:"-" 吗?

点击展开答案

需要。 c.Param 手动提取是一回事,但如果有另一个端点用 ShouldBindJSON 接收更新请求,前端依然可能传入 id 字段来篡改目标记录(Horizontal / Vertical Privilege Escalation)。最安全的做法是在所有涉及绑定的结构体中标记 binding:"-",让编译器帮你兜底。

常见误区

// ❌ 错误:只依赖手动赋值,没有显式跳过绑定
type UpdateReq struct {
    Name  string `json:"name" binding:"required"`
    Admin bool   // 没有 binding 标签 → 仍会被绑定!
}

func handler(c *gin.Context) {
    var req UpdateReq
    c.ShouldBindJSON(&req)
    req.Admin = false // 试图覆盖,但已经晚了——恶意数据已被填入
    // ...
}

// ✅ 正确:在结构体层面就拦截
type UpdateReq struct {
    Name  string `json:"name" binding:"required"`
    Admin bool   `json:"admin" binding:"-"` // 先拦截,再在后端设值
}

func handler(c *gin.Context) {
    var req UpdateReq
    c.ShouldBindJSON(&req)
    // req.Admin 必定是 false,从外部注入不了任何值
    req.Admin = isAdmin(c) // 从 Session / JWT 取真实身份
}

关联笔记