在生产环境中直接在代码或配置里保存长期 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)
- 直接长期 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();
- 使用阿里云 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);
- 自定义 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 的最小值。