Init
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
---
|
||||
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]] — 父文档,高级绑定与表单处理
|
||||
Reference in New Issue
Block a user