Files
cs-note/hhs/GIN/7-binding-advanced/skip-binding.md
T

132 lines
4.6 KiB
Markdown
Raw Normal View History

2026-05-24 11:42:38 +08:00
---
tags: [后端, Go, Gin, 绑定, 安全]
create time: 2026-04-28 00:00
---
# 跳过绑定 `binding:"-"`
## 概述
在 Gin 的自动绑定机制中,结构体标签 `binding:"-"` 用于**完全排除某个字段**,使其不参与任何请求数据的绑定和校验。这是防止前端篡改敏感数据的第一道防线。
## 正文
### 工作原理
Gin 在调用 `ShouldBind` / `ShouldBindJSON` 等方法时,底层会遍历结构体的所有字段,检查其 `binding` 标签:
```go
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:"-"` 的风险
>
> 假设接口设计如下:
> ```go
> 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:"-"` 是两个不同的概念,经常组合使用:
```go
type User struct {
ServerID uint `json:"-" gorm:"primaryKey"` // 不序列化、不绑定
Internal string `binding:"-"` // 可序列化但不绑定
Admin bool `json:"admin" binding:"-"` // 可序列化但不绑定
}
```
| 标签 | 影响范围 | 效果 |
|------|---------|------|
| `binding:"-"` | 只读入方向(请求 → 结构体) | 请求中的值不会被填入该字段 |
| `json:"-"` | 只写方向(结构体 → JSON) | 该字段不会出现在 JSON 响应中 |
| 两个同时存在 | 双向隔离 | 字段完全不可见 |
```mermaid
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:"-"` 吗?
>
> <details>
> <summary>点击展开答案</summary>
>
> **需要。** `c.Param` 手动提取是一回事,但如果有另一个端点用 `ShouldBindJSON` 接收更新请求,前端依然可能传入 `id` 字段来篡改目标记录(Horizontal / Vertical Privilege Escalation)。最安全的做法是在所有涉及绑定的结构体中标记 `binding:"-"`,让编译器帮你兜底。
> </details>
### 常见误区
```go
// ❌ 错误:只依赖手动赋值,没有显式跳过绑定
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 取真实身份
}
```
## 关联笔记
- [[GIN/7-binding-advanced]] — 父文档,高级绑定与表单处理