diff --git a/.env.release.example b/.env.release.example
index 3f020fc75..930ab826f 100644
--- a/.env.release.example
+++ b/.env.release.example
@@ -34,6 +34,8 @@ SKILLHUB_STORAGE_PROVIDER=local
SKILLHUB_STORAGE_S3_ENDPOINT=https://oss-cn-example.aliyuncs.com
SKILLHUB_STORAGE_S3_PUBLIC_ENDPOINT=
SKILLHUB_STORAGE_S3_BUCKET=skillhub-prod
+# Static credentials for S3-compatible storage (MinIO, Alibaba OSS, etc.).
+# Leave both blank to use IAM authentication (EC2 instance profile, ECS task role, EKS IRSA).
SKILLHUB_STORAGE_S3_ACCESS_KEY=replace-me
SKILLHUB_STORAGE_S3_SECRET_KEY=replace-me
SKILLHUB_STORAGE_S3_REGION=cn-shanghai
diff --git a/document/docs/04-developer/plugins/storage-spi.md b/document/docs/04-developer/plugins/storage-spi.md
index f81189e17..3b7f4dbaa 100644
--- a/document/docs/04-developer/plugins/storage-spi.md
+++ b/document/docs/04-developer/plugins/storage-spi.md
@@ -34,6 +34,8 @@ S3 协议兼容实现,支持:
## 配置
+### 静态凭据(Access Key / Secret Key)
+
```bash
# 选择存储提供方
SKILLHUB_STORAGE_PROVIDER=s3
@@ -45,6 +47,27 @@ SKILLHUB_STORAGE_S3_ACCESS_KEY=xxx
SKILLHUB_STORAGE_S3_SECRET_KEY=xxx
```
+### IAM 认证
+
+部署在 AWS 上时,可以不配置 Access Key / Secret Key,让 SDK 自动使用 IAM 角色认证([Default Credentials Provider Chain](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/credentials-chain.html)):
+
+```bash
+SKILLHUB_STORAGE_PROVIDER=s3
+SKILLHUB_STORAGE_S3_BUCKET=skillhub
+SKILLHUB_STORAGE_S3_REGION=us-east-1
+# 留空或不设置 ACCESS_KEY / SECRET_KEY,SDK 自动使用 IAM 认证
+SKILLHUB_STORAGE_S3_ACCESS_KEY=
+SKILLHUB_STORAGE_S3_SECRET_KEY=
+```
+
+支持的 IAM 认证方式(按 SDK 优先级):
+- 环境变量(`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`)
+- Java 系统属性
+- Web Identity Token(EKS IRSA)
+- AWS 配置文件(`~/.aws/credentials`)
+- EC2 Instance Profile
+- ECS Task Role
+
## 自定义实现
实现 `ObjectStorageService` 接口,注册为 Spring Bean 即可。
diff --git a/document/i18n/en/docusaurus-plugin-content-docs/current/04-developer/plugins/storage-spi.md b/document/i18n/en/docusaurus-plugin-content-docs/current/04-developer/plugins/storage-spi.md
index f3a0a57d1..2667e6155 100644
--- a/document/i18n/en/docusaurus-plugin-content-docs/current/04-developer/plugins/storage-spi.md
+++ b/document/i18n/en/docusaurus-plugin-content-docs/current/04-developer/plugins/storage-spi.md
@@ -34,6 +34,8 @@ S3 protocol compatible implementation, supports:
## Configuration
+### Static Credentials (Access Key / Secret Key)
+
```bash
# Select storage provider
SKILLHUB_STORAGE_PROVIDER=s3
@@ -45,6 +47,27 @@ SKILLHUB_STORAGE_S3_ACCESS_KEY=xxx
SKILLHUB_STORAGE_S3_SECRET_KEY=xxx
```
+### IAM Authentication
+
+When deployed on AWS, you can omit the Access Key / Secret Key and let the SDK use IAM role authentication via the [Default Credentials Provider Chain](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/credentials-chain.html):
+
+```bash
+SKILLHUB_STORAGE_PROVIDER=s3
+SKILLHUB_STORAGE_S3_BUCKET=skillhub
+SKILLHUB_STORAGE_S3_REGION=us-east-1
+# Leave ACCESS_KEY / SECRET_KEY blank to use IAM authentication
+SKILLHUB_STORAGE_S3_ACCESS_KEY=
+SKILLHUB_STORAGE_S3_SECRET_KEY=
+```
+
+Supported IAM authentication methods (in SDK priority order):
+- Environment variables (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`)
+- Java system properties
+- Web Identity Token (EKS IRSA)
+- AWS config file (`~/.aws/credentials`)
+- EC2 Instance Profile
+- ECS Task Role
+
## Custom Implementation
Implement `ObjectStorageService` interface and register as Spring Bean.
diff --git a/server/skillhub-storage/pom.xml b/server/skillhub-storage/pom.xml
index ec1674c31..939c0514b 100644
--- a/server/skillhub-storage/pom.xml
+++ b/server/skillhub-storage/pom.xml
@@ -30,6 +30,11 @@
apache-client
2.20.26
+
+ software.amazon.awssdk
+ sts
+ 2.20.26
+
org.springframework.boot
spring-boot-starter-test
diff --git a/server/skillhub-storage/src/main/java/com/iflytek/skillhub/storage/S3StorageService.java b/server/skillhub-storage/src/main/java/com/iflytek/skillhub/storage/S3StorageService.java
index d6a6327fd..e13adb402 100644
--- a/server/skillhub-storage/src/main/java/com/iflytek/skillhub/storage/S3StorageService.java
+++ b/server/skillhub-storage/src/main/java/com/iflytek/skillhub/storage/S3StorageService.java
@@ -6,6 +6,8 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.http.apache.ApacheHttpClient;
@@ -50,19 +52,32 @@ void init() {
.connectionAcquisitionTimeout(properties.getConnectionAcquisitionTimeout());
this.s3Client = buildS3Client(httpClientBuilder);
this.s3Presigner = buildPresigner();
+ String authMode = isStaticCredentials() ? "static credentials" : "IAM credentials (default provider chain)";
if (properties.isAutoCreateBucket()) {
- log.info("Initialized S3 storage client for bucket '{}' (bucket auto-creation is deferred until first write)",
- properties.getBucket());
+ log.info("Initialized S3 storage client for bucket '{}' using {} (bucket auto-creation is deferred until first write)",
+ properties.getBucket(), authMode);
} else {
- log.info("Initialized S3 storage client for bucket '{}'", properties.getBucket());
+ log.info("Initialized S3 storage client for bucket '{}' using {}", properties.getBucket(), authMode);
}
}
+ private boolean isStaticCredentials() {
+ return properties.getAccessKey() != null && !properties.getAccessKey().isBlank()
+ && properties.getSecretKey() != null && !properties.getSecretKey().isBlank();
+ }
+
+ AwsCredentialsProvider buildCredentialsProvider() {
+ if (isStaticCredentials()) {
+ return StaticCredentialsProvider.create(
+ AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey()));
+ }
+ return DefaultCredentialsProvider.create();
+ }
+
protected S3Client buildS3Client(ApacheHttpClient.Builder httpClientBuilder) {
var builder = S3Client.builder()
.region(Region.of(properties.getRegion()))
- .credentialsProvider(StaticCredentialsProvider.create(
- AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey())))
+ .credentialsProvider(buildCredentialsProvider())
.forcePathStyle(properties.isForcePathStyle())
.httpClientBuilder(httpClientBuilder)
.overrideConfiguration(config -> config
@@ -77,8 +92,7 @@ protected S3Client buildS3Client(ApacheHttpClient.Builder httpClientBuilder) {
S3Presigner buildPresigner() {
var presignerBuilder = S3Presigner.builder()
.region(Region.of(properties.getRegion()))
- .credentialsProvider(StaticCredentialsProvider.create(
- AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey())))
+ .credentialsProvider(buildCredentialsProvider())
.serviceConfiguration(S3Configuration.builder()
.pathStyleAccessEnabled(properties.isForcePathStyle())
.build());
diff --git a/server/skillhub-storage/src/test/java/com/iflytek/skillhub/storage/S3StorageServiceTest.java b/server/skillhub-storage/src/test/java/com/iflytek/skillhub/storage/S3StorageServiceTest.java
index 4e2439521..b18c737ce 100644
--- a/server/skillhub-storage/src/test/java/com/iflytek/skillhub/storage/S3StorageServiceTest.java
+++ b/server/skillhub-storage/src/test/java/com/iflytek/skillhub/storage/S3StorageServiceTest.java
@@ -1,6 +1,9 @@
package com.iflytek.skillhub.storage;
import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.http.apache.ApacheHttpClient;
@@ -183,6 +186,52 @@ void putObjectShouldRetryWhenBucketWasCreatedConcurrently() {
verify(client, times(2)).putObject(any(PutObjectRequest.class), any(RequestBody.class));
}
+ @Test
+ void shouldUseStaticCredentialsWhenAccessKeyAndSecretKeyProvided() {
+ S3StorageProperties props = createProperties(true);
+ S3StorageService service = new S3StorageService(props);
+
+ AwsCredentialsProvider provider = service.buildCredentialsProvider();
+
+ assertThat(provider).isInstanceOf(StaticCredentialsProvider.class);
+ }
+
+ @Test
+ void shouldUseDefaultCredentialsProviderWhenAccessKeyIsBlank() {
+ S3StorageProperties props = createProperties(true);
+ props.setAccessKey("");
+ props.setSecretKey("");
+ S3StorageService service = new S3StorageService(props);
+
+ AwsCredentialsProvider provider = service.buildCredentialsProvider();
+
+ assertThat(provider).isInstanceOf(DefaultCredentialsProvider.class);
+ }
+
+ @Test
+ void shouldUseDefaultCredentialsProviderWhenAccessKeyIsNull() {
+ S3StorageProperties props = createProperties(true);
+ props.setAccessKey(null);
+ props.setSecretKey(null);
+ S3StorageService service = new S3StorageService(props);
+
+ AwsCredentialsProvider provider = service.buildCredentialsProvider();
+
+ assertThat(provider).isInstanceOf(DefaultCredentialsProvider.class);
+ }
+
+ @Test
+ void shouldUseDefaultCredentialsProviderWhenOnlyAccessKeyIsSet() {
+ S3StorageProperties props = createProperties(true);
+ props.setAccessKey("some-key");
+ props.setSecretKey(null);
+ S3StorageService service = new S3StorageService(props);
+
+ AwsCredentialsProvider provider = service.buildCredentialsProvider();
+
+ assertThat(provider).isInstanceOf(DefaultCredentialsProvider.class);
+ }
+
private S3StorageProperties properties(boolean autoCreateBucket) {
S3StorageProperties properties = createProperties(true);
properties.setBucket("skillhub");