使用 KMS 管理阿里云 OSS 临时凭证(AK/SK/STS):原理、对比与实战代码示例

在生产环境中直接在代码或配置里保存长期 AK/SK 风险很高。更安全的做法是通过 KMS(或内部 Security Token 服务)下发短期 STS 凭证,让应用通过一个 CredentialsProvider 动态获取并刷新凭证。本文讲清两种方案的差异,并给出可运行的示例(不含任何公司私有实现)。


一、两种方式对比(概念)

  • 直接使用长期 AK/SK
    • 代码简单:在 OSSClientBuilder.build(endpoint, accessKeyId, accessKeySecret) 里传入 AK/SK。
    • 风险:长期凭证泄露后风险高,难以做细粒度审计与权限控制。
  • 通过 KMS 下发短期 STS
    • 优点:不在应用中保存长期凭证;凭证短期有效即可限制风险;KMS 可下发最小权限凭证并集中审计。
    • 实现:应用通过一个 CredentialsProvider 从 KMS 获取临时 AK/SK+Token,且应带缓存与提前失效(leadTimeout)策略以避免使用临近过期的 token。

二、代码示例(Java)

  1. 直接长期 AK/SK(官方最简单示例)
java 复制代码
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;

String endpoint = "oss-cn-region.aliyuncs.com";
String accessKeyId = "YOUR_AK";
String accessKeySecret = "YOUR_SK";

OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 结束时调用 ossClient.shutdown();
  1. 使用阿里云 STS(一次性 token)
java 复制代码
// 假设从 STS API 得到临时 ak/sk/token
String ak = "tempAk";
String sk = "tempSk";
String token = "tempSecurityToken";

OSS ossClient = new OSSClientBuilder().build(endpoint, ak, sk, token);
  1. 自定义 KMS + CredentialsProvider
    下面示例演示一个简单的"自拟 KMS 客户端 + 本地缓存 + CredentialsProvider"实现思路,用于演示提前失效(leadTimeout)和缓存 TTL 的计算。
java 复制代码
// MockKmsClient.java
public class MockKmsClient {
    // 模拟向 KMS 请求临时凭证
    public static class StsToken {
        public final String accessKeyId;
        public final String accessKeySecret;
        public final String securityToken;
        public final long expirationEpochSeconds; // token 到期时间(秒)
        public StsToken(String ak, String sk, String token, long exp) {
            this.accessKeyId = ak;
            this.accessKeySecret = sk;
            this.securityToken = token;
            this.expirationEpochSeconds = exp;
        }
    }

    public StsToken requestSts(String resource, int durationSeconds) {
        long now = System.currentTimeMillis()/1000;
        long exp = now + durationSeconds;
        // 这里用随机模拟值,实际应调用 KMS/STS 接口
        return new StsToken("AK_" + now, "SK_" + now, "TOKEN_" + now, exp);
    }
}
java 复制代码
// SimpleLocalCache.java
import java.util.concurrent.ConcurrentHashMap;

public class SimpleLocalCache {
    private static class Entry {
        final Object value;
        final long expireAtMillis;
        Entry(Object v, long expireAtMillis) { this.value = v; this.expireAtMillis = expireAtMillis; }
    }
    private final ConcurrentHashMap<String, Entry> cache = new ConcurrentHashMap<>();

    public void put(String key, Object value, long ttlSeconds) {
        long expireAt = System.currentTimeMillis() + ttlSeconds * 1000L;
        cache.put(key, new Entry(value, expireAt));
    }

    public Object get(String key) {
        Entry e = cache.get(key);
        if (e == null) return null;
        if (System.currentTimeMillis() > e.expireAtMillis) {
            cache.remove(key);
            return null;
        }
        return e.value;
    }
}
java 复制代码
// KmsCredentialsProvider.java
import com.aliyun.oss.common.auth.Credentials;
import com.aliyun.oss.common.auth.CredentialsProvider;

// 自定义 Credentials,包含 expiration(秒)便于外部判断
public class KmsCredentials implements Credentials {
    private final String accessKeyId;
    private final String accessKeySecret;
    private final String securityToken;
    private final long expirationEpochSeconds;
    public KmsCredentials(String ak, String sk, String token, long exp) {
        this.accessKeyId = ak; this.accessKeySecret = sk; this.securityToken = token; this.expirationEpochSeconds = exp;
    }
    @Override
    public String getAccessKeyId() { return accessKeyId; }
    @Override
    public String getAccessKeySecret() { return accessKeySecret; }
    @Override
    public String getSecurityToken() { return securityToken; }

    public long getExpirationEpochSeconds() { return expirationEpochSeconds; }
}

// Provider 实现(带本地缓存和提前失效 leadTimeout 秒)
public class KmsCredentialsProvider implements CredentialsProvider {
    private final MockKmsClient kmsClient;
    private final SimpleLocalCache cache;
    private final String resourceId;
    private final int leadTimeoutSeconds; // 提前失效(秒)

    public KmsCredentialsProvider(MockKmsClient kmsClient, SimpleLocalCache cache, String resourceId, int leadTimeoutSeconds) {
        this.kmsClient = kmsClient;
        this.cache = cache;
        this.resourceId = resourceId;
        this.leadTimeoutSeconds = leadTimeoutSeconds;
    }

    @Override
    public void setCredentials(Credentials credentials) {
        // 不需要实现
    }

    @Override
    public Credentials getCredentials() {
        String key = "sts:" + resourceId;
        KmsCredentials creds = (KmsCredentials) cache.get(key);
        if (creds != null) return creds;

        // 缓存未命中,去 KMS 拉取(带重试示例)
        int retries = 0;
        while (retries < 3) {
            try {
                MockKmsClient.StsToken token = kmsClient.requestSts(resourceId, 3600); // 请求 1 小时 token
                long nowSec = System.currentTimeMillis() / 1000L;
                long remaining = token.expirationEpochSeconds - nowSec;
                long effectiveCacheSec = remaining - leadTimeoutSeconds;
                if (effectiveCacheSec > 0) {
                    KmsCredentials kc = new KmsCredentials(token.accessKeyId, token.accessKeySecret, token.securityToken, token.expirationEpochSeconds);
                    cache.put(key, kc, effectiveCacheSec); // 本地缓存 TTL = remaining - leadTimeout
                    return kc;
                } else {
                    // token 剩余时间太短,不缓存,直接返回
                    return new KmsCredentials(token.accessKeyId, token.accessKeySecret, token.securityToken, token.expirationEpochSeconds);
                }
            } catch (Exception e) {
                retries++;
                try { Thread.sleep(200); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); }
            }
        }
        // 若最终失败,可以抛异常或返回 null(调用方需处理)
        return null;
    }
}

使用示例(把 Provider 传给 OSSClientBuilder):

java 复制代码
MockKmsClient kmsClient = new MockKmsClient();
SimpleLocalCache cache = new SimpleLocalCache();
KmsCredentialsProvider provider = new KmsCredentialsProvider(kmsClient, cache, "my-aliyun-id", 120);

OSS ossClient = new OSSClientBuilder().build(endpoint, provider);

说明:上面实现演示了**本地缓存 + 提前失效(leadTimeout)**的策略:当 KMS 返回 token 时,先计算 token 的剩余秒数 remaining,然后把 (remaining - leadTimeout) 作为本地缓存 TTL。这样可确保我们在 token 到期前 leadTimeout 秒就触发刷新,避免签名时用到快要过期的 token。


三、预签名(Presigned)URL 的过期规则(关键结论)

当使用临时 STS token 去生成预签名 URL 时,链接能否生效取决于两者:

  • 你在生成 URL 时设定的 expiration(URL 自身到期时间);
  • 以及用于签名的 STS token 的剩余有效期(KMS 返回的 expiration)。

因此:预签名 URL 的最终有效期 = min(URL expiration, STS token 剩余有效期)

举例说明:

  • 假设你用的 STS 还剩 10 分钟有效期,但你生成 URL 的 expiration 设置为 2 小时,那么实际有效期只有 10 分钟(STS 到期后签名失效)。
  • 建议:生成较长有效期的 URL 前,先确保当前 STS 的剩余时间 >= 你想要的 URL 有效期;否则应先强制刷新 token(从 KMS 拉取新的临时凭证),再签名生成 URL。

四、在生成预签名 URL 前强校验 token 的示例逻辑

java 复制代码
// 假设 provider 是 KmsCredentialsProvider,并且我们能取到 KmsCredentials(含 expiration)
KmsCredentials creds = (KmsCredentials) provider.getCredentials();
long now = System.currentTimeMillis() / 1000L;
long tokenRemaining = creds.getExpirationEpochSeconds() - now;
long desiredUrlSeconds = 3600; // 想要的 URL 有效期 1 小时

if (tokenRemaining < desiredUrlSeconds + 10) { // +10 秒安全边界
    // 强制刷新:直接调用 provider.getCredentials() 的刷新路径或专门的 forceRefresh 方法(取决实现)
    creds = (KmsCredentials) provider.getCredentials(); // 示例:我们直接再拿一次,具体验证/实现可自定义
}

Date expiration = new Date(System.currentTimeMillis() + desiredUrlSeconds * 1000L);
URL presigned = ossClient.generatePresignedUrl(bucketName, objectName, expiration);

五、工程化建议(要点)

  • 在生成预签名 URL 前,强制校验 token 剩余期,若不足则刷新;
  • 选择合适的 leadTimeout(示例中为 120 秒),用于本地提前失效,避免使用临近过期的 token 签名;
  • 为 KMS 请求添加重试与告警,监控 KMS 可用性与失败率;
  • 不要在每次操作中关闭共享的 ossClient,应在应用/容器关闭时统一 shutdown()
  • 记录并监控 provider 返回 null 或异常的事件,及时触发告警。

六、结语

  • 直接保存长期 AK/SK 实现简单但风险高;通过 KMS 下发短期 STS 并结合本地缓存与提前失效策略可以显著提升安全性与可控性。
  • 生成预签名 URL 时,务必考虑 STS 的剩余有效期:最终有效期等于 URL expiration 与 STS expiration 的最小值。
相关推荐
奇树谦18 小时前
FastDDS阿里云DDSRouter安装和使用(失败)
elasticsearch·阿里云·云计算
虎冯河19 小时前
阿里云 + 宝塔面板环境Python 项目从 0 到 1 部署全流
python·阿里云·云计算
China_Yanhy19 小时前
后端开发者的 AWS 大数据指南:从 RDS 到 Data Lake
大数据·云计算·aws
周之鸥20 小时前
宝塔面板 + 阿里云 DNS 实现 Let’s Encrypt 证书自动续签(详细图文教程)
阿里云·云计算·宝塔面板·let’s encrypt·自动续签
翼龙云_cloud1 天前
阿里云渠道商:如何手动一键扩缩容ECS实例?
运维·服务器·阿里云·云计算
AKAMAI1 天前
基准测试:Akamai云上的NVIDIA RTX Pro 6000 Blackwell
人工智能·云计算·测试
齐 飞1 天前
使用阿里云的MaxCompute查询sql时报错:DruidPooledPreparedStatement: getMaxFieldSize error
sql·阿里云·odps
China_Yanhy1 天前
AWS EKS三种类别,如何选择
云计算·aws
xybDIY1 天前
亚马逊云 Organizations 组织 Link 账号关联与解绑自动化解决方案
运维·自动化·云计算·aws