package app import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strings" "sync" "time" cos "github.com/tencentyun/cos-go-sdk-v5" ) const cvmCAMCredentialMetadataBaseURL = "http://metadata.tencentyun.com/latest/meta-data/cam/security-credentials" type cvmCAMCredential struct { TmpSecretID string `json:"TmpSecretId"` TmpSecretKey string `json:"TmpSecretKey"` Token string `json:"Token"` ExpiredTime int64 `json:"ExpiredTime"` Code string `json:"Code"` } // cvmCAMCredentialProvider 在凭证过期前 5 分钟刷新,整个结构不暴露凭证方法,避免误打日志。 type cvmCAMCredentialProvider struct { roleName string client *http.Client mu sync.Mutex cached cvmCAMCredential } func newCVMCAMCredentialProvider(roleName string) *cvmCAMCredentialProvider { return &cvmCAMCredentialProvider{ roleName: strings.TrimSpace(roleName), // metadata 是启动/请求链路的外部依赖,必须在 2 秒内失败,不能把 COS 归档 goroutine 长时间卡死。 client: &http.Client{Timeout: 2 * time.Second}, } } func (p *cvmCAMCredentialProvider) get(ctx context.Context) (string, string, string, error) { p.mu.Lock() defer p.mu.Unlock() now := time.Now().UTC().Unix() if p.cached.ExpiredTime > now+int64((5*time.Minute).Seconds()) { return p.cached.TmpSecretID, p.cached.TmpSecretKey, p.cached.Token, nil } previous := p.cached if p.roleName == "" { return "", "", "", fmt.Errorf("wallet outbox archive CVM CAM role name is empty") } metadataURL := cvmCAMCredentialMetadataBaseURL + "/" + url.PathEscape(p.roleName) request, err := http.NewRequestWithContext(ctx, http.MethodGet, metadataURL, nil) if err != nil { return "", "", "", err } response, err := p.client.Do(request) if err != nil { if previous.ExpiredTime > now { return previous.TmpSecretID, previous.TmpSecretKey, previous.Token, nil } return "", "", "", fmt.Errorf("load wallet outbox archive CVM role credential: %w", err) } defer response.Body.Close() if response.StatusCode < 200 || response.StatusCode > 299 { // 不读取/回传 metadata 错误体,避免平台异常响应携带临时凭证。 if previous.ExpiredTime > now { return previous.TmpSecretID, previous.TmpSecretKey, previous.Token, nil } return "", "", "", fmt.Errorf("load wallet outbox archive CVM role credential: metadata status %d", response.StatusCode) } var credential cvmCAMCredential if err := json.NewDecoder(response.Body).Decode(&credential); err != nil { return "", "", "", fmt.Errorf("decode wallet outbox archive CVM role credential: %w", err) } if credential.Code != "Success" || strings.TrimSpace(credential.TmpSecretID) == "" || strings.TrimSpace(credential.TmpSecretKey) == "" || strings.TrimSpace(credential.Token) == "" || credential.ExpiredTime <= now+30 { return "", "", "", fmt.Errorf("wallet outbox archive CVM role returned incomplete or expired credential") } p.cached = credential return credential.TmpSecretID, credential.TmpSecretKey, credential.Token, nil } // cvmRoleAuthorizationTransport 每次签名前从缓存 provider 取 STS;只有刷新窗口才访问 metadata。 type cvmRoleAuthorizationTransport struct { provider *cvmCAMCredentialProvider transport http.RoundTripper } func (t *cvmRoleAuthorizationTransport) GetCredential() (string, string, string, error) { return t.provider.get(context.Background()) } func (t *cvmRoleAuthorizationTransport) RoundTrip(request *http.Request) (*http.Response, error) { secretID, secretKey, token, err := t.provider.get(request.Context()) if err != nil { return nil, err } return (&cos.AuthorizationTransport{ SecretID: secretID, SecretKey: secretKey, SessionToken: token, Transport: t.transport, }).RoundTrip(request) }