diff --git a/hhs/DEV/OAuth2与MFA/OAuth2-and-MFA.md b/hhs/DEV/OAuth2与MFA/OAuth2-and-MFA.md
index e0f97f0..5fa4666 100644
--- a/hhs/DEV/OAuth2与MFA/OAuth2-and-MFA.md
+++ b/hhs/DEV/OAuth2与MFA/OAuth2-and-MFA.md
@@ -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
---
diff --git a/hhs/DEV/OSS/OSS.md b/hhs/DEV/OSS/OSS.md
index a7d4cfd..0aee253 100644
--- a/hhs/DEV/OSS/OSS.md
+++ b/hhs/DEV/OSS/OSS.md
@@ -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
Body: (PNG binary data)"]
- O2["Key: docs/report.pdf
Body: (PDF binary data)"]
- O3["Key: backups/db-dump.sql.gz
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
alibabacloud-oss-go-sdk-v2"]
- A --> C["Minio Client SDK
minio/go-minio"]
- A --> D["AWS SDK for Go v2
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 直接上传
Content-Type: image/jpeg
文件二进制数据
+ FE->>OSS: PUT 直接上传 Content-Type image/jpeg 文件二进制数据
OSS-->>FE: 200 OK
opt 通知服务端完成
- FE->>GO: POST /upload/callback
文件名和元信息
+ 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
有效期15分钟
- GO-->>FE: (返回临时凭证)
+ GO->>STS: AssumeRole 角色Arn加SessionName
+ STS-->>GO: AccessKeyId加AccessKeySecret加SecurityToken有效期15分钟
+ GO-->>FE: 返回临时凭证
- FE->>OSS: 用临时凭证直接上传
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{"是否需要服务端
内容分析/转码?"}
+ B -- "<= 1MB" --> C{"是否需要服务端内容分析转码?"}
B -- "> 1MB 或不确定" --> D{"预期并发量?"}
- C -- "否" --> E["✅ 方案 B: 预签名 URL
最简单+零带宽开销"]
- C -- "是" --> F["✅ 方案 A: 服务端中转
可控但限并发"]
+ C -- "否" --> E["方案 B: 预签名 URL 最简单加零带宽开销"]
+ C -- "是" --> F["方案 A: 服务端中转 可控但限并发"]
- D -- "QPS < 100" --> G["✅ 方案 B: 预签名 URL"]
- D -- "QPS >= 100 或
大批量/大文件" --> H["✅ 方案 C: STS 临时凭证
灵活且可扩展"]
+ 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
?filename=photo.jpg
- Note over GO: 验证用户登录态和权限
- GO->>OSS: PresignedGetObject(photo.jpg, 15min)
- OSS-->>GO: (预签名URL)
含签名和过期参数
- GO-->>FE: (返回URL和类型信息)
-
- FE->>OSS: PUT直接上传
Content-Type: image/jpeg
文件二进制数据
- OSS-->>FE: 200 OK
-
- opt 通知服务端完成
- FE->>GO: POST /upload/callback
文件名和元信息
- 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泄露
导致账单爆炸"]
- T2["未限流
OSS请求被拒绝"]
- T3["命名冲突
同名文件覆盖"]
- T4["内存溢出
全量读入内存"]
- T5["内网跨地域
用了公网Endpoint"]
+ T1["AccessKey泄露 导致账单爆炸"]
+ T2["未限流 OSS请求被拒绝"]
+ T3["命名冲突 同名文件覆盖"]
+ T4["内存溢出 全量读入内存"]
+ T5["内网跨地域 用了公网Endpoint"]
end
subgraph FIXES["解决方案"]
- F1["使用RAM子账号+STS
临时凭证(15min过期)"]
- F2["客户端侧限速+重试退避
指数退避算法"]
- F3["使用UUID或NanoID重命名
fileUUID_扩展名"]
- F4["始终用io.Reader流式处理
避免ReadAll读大文件"]
- F5["确保ECS与Bucket同Region
使用内网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 或其他服务统一调用,避免重复实现。
diff --git a/hhs/DEV/XSS与CSRF攻击/XSS与CSRF攻击.md b/hhs/DEV/XSS与CSRF攻击/XSS与CSRF攻击.md
index a7053cd..64c00b6 100644
--- a/hhs/DEV/XSS与CSRF攻击/XSS与CSRF攻击.md
+++ b/hhs/DEV/XSS与CSRF攻击/XSS与CSRF攻击.md
@@ -45,15 +45,18 @@ graph LR
攻击者将恶意脚本存入数据库,每个访问该页面的用户都会中招。经典案例:留言板注入 ``。
```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. 用户访问恶意页面
伪造 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
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:
<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: 攻击者页面试图伪造请求
但没有 csrf_token
- end
Attacker->>SRV: POST /transfer (无 token)
SRV->>SRV: 比对失败 ✗