Files
cs-note/hhs/GIN/7-binding-advanced/skip-binding.md
T
2026-05-24 11:42:38 +08:00

132 lines
4.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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]] — 父文档,高级绑定与表单处理