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: 比对失败 ✗