132 lines
4.6 KiB
Markdown
132 lines
4.6 KiB
Markdown
|
|
---
|
|||
|
|
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]] — 父文档,高级绑定与表单处理
|