代码拉取完成,页面将自动刷新
package cos
import (
"container/list"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"sort"
"sync"
"time"
"golang.org/x/sync/singleflight"
)
// =============================================================================
// STS 临时凭证缓存(Phase 2:LRU + singleflight + canonical key)
//
// 行为概览:
// - canonical key:把 (instance_name, normalized_scope) 序列化为字段字典序的
// canonical JSON 后做 sha256;保证 scope 字段顺序、Action / SourceIP 数组顺序、
// 重复元素都不会影响 cache key(语义等价的请求一定命中同一条目)。
// - LRU 容量:包级常量 stsCacheCapacity(默认 256),用 doubly-linked list + map
// 的经典 O(1) 实现;满载时按访问顺序淘汰最久未使用项。
// - TTL 下界:剩余有效期 < stsCacheMinResidualSecs(30s)的凭证不写入缓存;
// 读命中后再校验一次相同条件,防止 race 条件下取出即将过期的凭证。
// - 并发合并:同 cache key 的并发签发请求由 singleflight 合并到一次实际调用,
// 其余 goroutine 共享同一个结果 / 错误。
//
// 设计权衡:
// - 自实现轻量 LRU 而非引入 hashicorp/golang-lru:仅依赖 stdlib container/list,
// 避免新增任何一个第三方间接依赖;
// - LRU 容量与 TTL 下界为包级常量(不暴露为配置项):按 8.4 节「禁止无用配置」,
// 这两个参数的合理区间极窄,配置化收益小于复杂度成本。
//
// 并发安全:lruCache 内部全局互斥;singleflight 自身并发安全。
// =============================================================================
const (
// stsCacheCapacity LRU 缓存最大条目数;按经验值 256 足以覆盖单实例下的
// 业务级 scope 维度(Action 组合 × Key 前缀维度,正常业务远小于 256)。
stsCacheCapacity = 256
// stsCacheMinResidualSecs 写入 / 读出缓存时要求的最小剩余有效期(秒)。
// 与 stsSafetyPaddingSecs 同值,目的是确保拿到凭证的调用方至少有 30s 可用时间。
stsCacheMinResidualSecs = 30
)
// 包级缓存与 singleflight 单例。
//
// 这里有意使用包级单例而非附着在 *COS 上:缓存语义对所有 *COS 实例完全一致
// (cache key 已经把 instance_name 编码进去),跨 *COS 共享缓存可避免在
// 多次 NewCOS 场景下重复签发同一份临时凭证,也方便单测通过 export hook 一键清空。
var (
stsCache = newLRUCache(stsCacheCapacity)
stsCacheGroup singleflight.Group
stsCacheNowFn = time.Now // 单测可通过 export hook 替换为可控时钟
)
// IssueSTSCached 带缓存的临时凭证签发(Phase 2:LRU + singleflight + canonical key)。
//
// 行为:
// - 先按 (name, scope) canonical key 在 LRU 中查找;命中且剩余有效期 ≥ 30s 时直接返回缓存副本;
// - 未命中则走 singleflight 合并并发调用 → 实际触发一次 IssueSTS;
// - 签发成功后剩余有效期 ≥ 30s 时写入缓存;< 30s 时不写入(避免立即过期的凭证污染缓存)。
//
// 与 IssueSTS 的差异:
// - 错误语义完全一致:scope 校验失败 / 实例配置缺失 / 网络错误均原样透传;
// - 同时段内对相同 (name, scope) 的多次调用只会触发一次实际网络请求;
// - 返回的 STSCredential 与 IssueSTS 同样已扣 30s 安全垫。
//
// 并发安全:可被多个 goroutine 并发调用;缓存与 singleflight 内部已加锁。
func (c *COS) IssueSTSCached(ctx context.Context, name string, scope STSScope) (*STSCredential, error) {
// 1. 计算 cache key(含 scope 校验前的轻量规整:仅排序去重,不触发硬错误;
// 硬错误由后续 IssueSTS 内部的 validateScope 统一拦截,保持错误语义不变)
key := buildCacheKey(name, scope)
// 2. 命中 + 剩余有效期足够 → 返回副本
if cred, ok := stsCache.get(key); ok {
if cred.ExpiredTime-stsCacheNowFn().Unix() >= stsCacheMinResidualSecs {
return cloneCredential(cred), nil
}
// 临界凭证:本次访问已不安全,提前驱逐避免后续读者再次命中
stsCache.remove(key)
}
// 3. singleflight 合并:同 key 的并发请求只会触发一次 IssueSTS
v, err, _ := stsCacheGroup.Do(key, func() (interface{}, error) {
// 二次检查:等待 singleflight 期间可能已有 sibling 请求把结果塞回缓存
if cred, ok := stsCache.get(key); ok {
if cred.ExpiredTime-stsCacheNowFn().Unix() >= stsCacheMinResidualSecs {
return cloneCredential(cred), nil
}
}
cred, issueErr := c.IssueSTS(ctx, name, scope)
if issueErr != nil {
return nil, issueErr
}
// TTL 下界:剩余有效期 ≥ 30s 才写入;否则跳过缓存(保留正确返回)
if cred.ExpiredTime-stsCacheNowFn().Unix() >= stsCacheMinResidualSecs {
stsCache.put(key, cloneCredential(cred))
}
return cred, nil
})
if err != nil {
return nil, err
}
cred := v.(*STSCredential)
// singleflight 共享给多 goroutine 时禁止返回同一指针,避免上层意外修改污染缓存
return cloneCredential(cred), nil
}
// =============================================================================
// canonical key:(name, scope) → sha256 hex
// =============================================================================
// canonicalScope 是 STSScope 的 canonical 序列化中间结构。
//
// 仅暴露在本文件内:JSON tag 严格按字典序声明(Action / ContentType / DurationSecs /
// Key / KeyPrefix / MaxObjectSize / SourceIP);string slice 字段在 buildCacheKey 中
// 排序去重后再赋值,保证语义等价的 scope 一定生成相同 canonical bytes。
type canonicalScope struct {
Action []string `json:"action"`
ContentType string `json:"content_type"`
DurationSecs int `json:"duration_secs"`
Key string `json:"key"`
KeyPrefix string `json:"key_prefix"`
MaxObjectSize int64 `json:"max_object_size"`
SourceIP []string `json:"source_ip"`
}
// canonicalEnvelope 包裹 instance_name 与 canonical scope,序列化后做 sha256。
//
// 字段名同样字典序声明(instance / scope)。
type canonicalEnvelope struct {
Instance string `json:"instance"`
Scope canonicalScope `json:"scope"`
}
// buildCacheKey 计算 (name, scope) 的稳定 cache key。
//
// 算法:
// 1. Action / SourceIP 排序去重(语义无序);
// 2. 拼装 canonicalEnvelope,json.Marshal 输出严格字典序 JSON;
// 3. sha256(canonical_bytes) → 16 进制字符串。
//
// 异常路径:json.Marshal 在本文件 canonical 结构上不会失败(无 chan / func 字段),
// 但仍做 defensive fallback:失败时退化为「绝对不命中缓存」的随机 key(用纳秒时间戳 +
// instance name,确保 LRU put/get 都不会命中),避免 panic 影响调用方。
func buildCacheKey(name string, scope STSScope) string {
env := canonicalEnvelope{
Instance: name,
Scope: canonicalScope{
Action: sortDedupStrings(scope.Action),
ContentType: scope.ContentType,
DurationSecs: scope.DurationSecs,
Key: scope.Key,
KeyPrefix: scope.KeyPrefix,
MaxObjectSize: scope.MaxObjectSize,
SourceIP: sortDedupStrings(scope.SourceIP),
},
}
bts, err := json.Marshal(env)
if err != nil {
// canonical 结构不会触发 json.Marshal 失败;此处兜底防御
return "fallback-" + name + "-" + time.Now().Format(time.RFC3339Nano)
}
sum := sha256.Sum256(bts)
return hex.EncodeToString(sum[:])
}
// sortDedupStrings 返回排序去重后的副本,nil 与空切片归一为 nil。
//
// 不修改入参;空集统一返回 nil 是为了让 canonical JSON 的 null 序列化保持稳定
// (`null` vs `[]` 在 sha256 后会得到不同 cache key,统一为 null 避免歧义)。
func sortDedupStrings(in []string) []string {
if len(in) == 0 {
return nil
}
tmp := make([]string, 0, len(in))
seen := make(map[string]struct{}, len(in))
for _, s := range in {
if _, ok := seen[s]; ok {
continue
}
seen[s] = struct{}{}
tmp = append(tmp, s)
}
sort.Strings(tmp)
return tmp
}
// cloneCredential 深拷贝 STSCredential(结构体仅含基本类型,简单字段复制即可)。
//
// 必要性:缓存命中时返回的指针不能与缓存内部共享——若调用方对返回值做任何写操作
// 都会污染下一次命中。
func cloneCredential(in *STSCredential) *STSCredential {
if in == nil {
return nil
}
out := *in
return &out
}
// =============================================================================
// 轻量 LRU 缓存(goroutine-safe)
// =============================================================================
// lruEntry 是 LRU 内部链表节点的元素值。
type lruEntry struct {
key string
cred *STSCredential
}
// lruCache 是基于 container/list 的 O(1) LRU 缓存。
//
// 实现要点:
// - 双向链表:表头是最近使用、表尾是最久未使用;
// - map[key]*list.Element:O(1) 命中;
// - sync.Mutex 单锁:缓存条目数 ≤ 256,操作 O(1),单锁竞争可忽略;
// 如未来容量提升至 K 级再考虑分片或 sync.RWMutex。
type lruCache struct {
mu sync.Mutex
capacity int
ll *list.List
items map[string]*list.Element
}
// newLRUCache 创建一个容量为 cap 的 lruCache;cap <= 0 时按 1 处理。
func newLRUCache(cap int) *lruCache {
if cap <= 0 {
cap = 1
}
return &lruCache{
capacity: cap,
ll: list.New(),
items: make(map[string]*list.Element, cap),
}
}
// get 命中时返回 (cred, true) 并把条目移到表头;未命中返回 (nil, false)。
func (l *lruCache) get(key string) (*STSCredential, bool) {
l.mu.Lock()
defer l.mu.Unlock()
e, ok := l.items[key]
if !ok {
return nil, false
}
l.ll.MoveToFront(e)
return e.Value.(*lruEntry).cred, true
}
// put 写入或更新条目;超过容量时淘汰表尾最久未使用项。
func (l *lruCache) put(key string, cred *STSCredential) {
l.mu.Lock()
defer l.mu.Unlock()
if e, ok := l.items[key]; ok {
l.ll.MoveToFront(e)
e.Value.(*lruEntry).cred = cred
return
}
e := l.ll.PushFront(&lruEntry{key: key, cred: cred})
l.items[key] = e
if l.ll.Len() > l.capacity {
oldest := l.ll.Back()
if oldest != nil {
l.ll.Remove(oldest)
delete(l.items, oldest.Value.(*lruEntry).key)
}
}
}
// remove 显式删除指定 key(命中即将过期凭证时使用)。
func (l *lruCache) remove(key string) {
l.mu.Lock()
defer l.mu.Unlock()
if e, ok := l.items[key]; ok {
l.ll.Remove(e)
delete(l.items, key)
}
}
// len 返回当前缓存条目数;仅供单测断言。
func (l *lruCache) len() int {
l.mu.Lock()
defer l.mu.Unlock()
return l.ll.Len()
}
// clear 清空所有条目;仅供单测在用例间隔离。
func (l *lruCache) clear() {
l.mu.Lock()
defer l.mu.Unlock()
l.ll.Init()
l.items = make(map[string]*list.Element, l.capacity)
}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。