Commit 24356261 by 真的三个金的鑫

接入versityGW

parent 40c6a67b
...@@ -15,7 +15,12 @@ public enum OssTypeEnum { ...@@ -15,7 +15,12 @@ public enum OssTypeEnum {
AWS("aws"), AWS("aws"),
MINIO("minio"); MINIO("minio"),
/**
* VersityGW(S3 兼容)。下发给 Pilot/机场时请映射为 {@link #MINIO} 或 {@link #AWS}。
*/
VERSITYGW("versity");
private String type; private String type;
...@@ -27,4 +32,11 @@ public enum OssTypeEnum { ...@@ -27,4 +32,11 @@ public enum OssTypeEnum {
public String getType() { public String getType() {
return type; return type;
} }
/**
* Pilot / 机场仅识别 ali / aws / minio,VersityGW 对外按 minio 上报。
*/
public OssTypeEnum toClientProvider() {
return this == VERSITYGW ? MINIO : this;
}
} }
...@@ -23,6 +23,9 @@ ...@@ -23,6 +23,9 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<javax-activation.version>1.1.1</javax-activation.version> <javax-activation.version>1.1.1</javax-activation.version>
<mqtt.version>5.5.5</mqtt.version> <mqtt.version>5.5.5</mqtt.version>
<!-- OSS 配置 -->
<aws.sdk.version>2.28.22</aws.sdk.version>
<aws.crt.version>0.31.3</aws.crt.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
...@@ -67,7 +70,24 @@ ...@@ -67,7 +70,24 @@
<groupId>com.fasterxml.jackson.datatype</groupId> <groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId> <artifactId>jackson-datatype-jsr310</artifactId>
</dependency> </dependency>
<!-- AWS SDK for Java 2.x -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<!-- 使用AWS基于 CRT 的 S3 客户端 -->
<dependency>
<groupId>software.amazon.awssdk.crt</groupId>
<artifactId>aws-crt</artifactId>
<version>${aws.crt.version}</version>
</dependency>
<!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3-transfer-manager</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
......
...@@ -44,6 +44,17 @@ public class OssConfiguration { ...@@ -44,6 +44,17 @@ public class OssConfiguration {
public static String objectDirPrefix; public static String objectDirPrefix;
/**
* VersityGW STS 模式:temp=Admin 创建临时用户;static=直接下发长期 AK/SK。
*/
public static String stsMode = "temp";
/**
* 临时用户角色:user / userplus / admin。
* 共享 bucket 且未改 ownership 时建议 admin(可访问全部 bucket)。
*/
public static String stsUserRole = "admin";
public void setProvider(OssTypeEnum provider) { public void setProvider(OssTypeEnum provider) {
OssConfiguration.provider = provider; OssConfiguration.provider = provider;
} }
...@@ -87,6 +98,14 @@ public class OssConfiguration { ...@@ -87,6 +98,14 @@ public class OssConfiguration {
public void setObjectDirPrefix(String objectDirPrefix) { public void setObjectDirPrefix(String objectDirPrefix) {
OssConfiguration.objectDirPrefix = objectDirPrefix; OssConfiguration.objectDirPrefix = objectDirPrefix;
} }
public void setStsMode(String stsMode) {
OssConfiguration.stsMode = stsMode;
}
public void setStsUserRole(String stsUserRole) {
OssConfiguration.stsUserRole = stsUserRole;
}
} }
......
package com.dji.sample.component.oss.service.impl;
import com.dji.sample.component.oss.model.OssConfiguration;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.signer.AwsS3V4Signer;
import software.amazon.awssdk.auth.signer.params.AwsS3V4SignerParams;
import software.amazon.awssdk.http.SdkHttpFullRequest;
import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.regions.Region;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.Map;
/**
* VersityGW Admin API 客户端(S3 SigV4)。
* 使用 {@link AwsS3V4Signer},会自动带上并签名 x-amz-content-sha256。
*/
@Slf4j
@Component
public class VersityGwAdminClient {
private static final String SERVICE = "s3";
private final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
private final AwsS3V4Signer signer = AwsS3V4Signer.create();
public void createUser(String accessKey, String secretKey, String role) {
String body = ""
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<Account>"
+ "<Access>" + xmlEscape(accessKey) + "</Access>"
+ "<Secret>" + xmlEscape(secretKey) + "</Secret>"
+ "<Role>" + xmlEscape(role) + "</Role>"
+ "</Account>";
HttpResponse<String> response = execute(SdkHttpMethod.PATCH, "/create-user", null, body);
int code = response.statusCode();
if (code != 201 && code != 200) {
throw new IllegalStateException("VersityGW create-user failed, status=" + code + ", body=" + response.body());
}
log.info("VersityGW temp user created: access={}", accessKey);
}
public void deleteUser(String accessKey) {
HttpResponse<String> response = execute(
SdkHttpMethod.PATCH, "/delete-user", Map.of("access", accessKey), null);
int code = response.statusCode();
if (code != 204 && code != 200 && code != 404) {
throw new IllegalStateException("VersityGW delete-user failed, status=" + code + ", body=" + response.body());
}
log.info("VersityGW temp user deleted: access={}", accessKey);
}
public String listUsers() {
HttpResponse<String> response = execute(SdkHttpMethod.PATCH, "/list-users", null, null);
int code = response.statusCode();
if (code != 200 && code != 204) {
throw new IllegalStateException("VersityGW list-users failed, status=" + code + ", body=" + response.body());
}
return response.body() == null ? "" : response.body();
}
private HttpResponse<String> execute(SdkHttpMethod method, String path,
Map<String, String> query, String body) {
try {
byte[] payload = body == null ? new byte[0] : body.getBytes(StandardCharsets.UTF_8);
URI endpoint = URI.create(trimTrailingSlash(OssConfiguration.endpoint));
SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder()
.method(method)
.protocol(endpoint.getScheme())
.host(endpoint.getHost())
.port(resolvePort(endpoint))
.encodedPath(path)
.contentStreamProvider(() -> new ByteArrayInputStream(payload));
if (query != null) {
query.forEach(requestBuilder::putRawQueryParameter);
}
if (body != null) {
requestBuilder.putHeader("Content-Type", "application/xml");
}
SdkHttpFullRequest signed = signer.sign(
requestBuilder.build(),
AwsS3V4SignerParams.builder()
.awsCredentials(AwsBasicCredentials.create(
OssConfiguration.accessKey, OssConfiguration.secretKey))
.signingName(SERVICE)
.signingRegion(Region.of(OssConfiguration.region))
.build());
HttpRequest.Builder httpBuilder = HttpRequest.newBuilder()
.uri(signed.getUri())
.timeout(Duration.ofSeconds(30))
.method(method.name(), HttpRequest.BodyPublishers.ofByteArray(payload));
boolean hasContentSha256 = false;
for (Map.Entry<String, List<String>> header : signed.headers().entrySet()) {
String name = header.getKey();
if ("Content-Length".equalsIgnoreCase(name) || "Host".equalsIgnoreCase(name)) {
continue;
}
if ("x-amz-content-sha256".equalsIgnoreCase(name)) {
hasContentSha256 = true;
}
for (String value : header.getValue()) {
httpBuilder.header(name, value);
}
}
if (!hasContentSha256) {
throw new IllegalStateException("Signed request missing x-amz-content-sha256 header");
}
return httpClient.send(httpBuilder.build(), HttpResponse.BodyHandlers.ofString());
} catch (IllegalStateException e) {
throw e;
} catch (Exception e) {
throw new IllegalStateException("VersityGW admin API call failed: " + path, e);
}
}
private static int resolvePort(URI endpoint) {
if (endpoint.getPort() != -1) {
return endpoint.getPort();
}
return "https".equalsIgnoreCase(endpoint.getScheme()) ? 443 : 80;
}
private static String trimTrailingSlash(String endpoint) {
if (endpoint != null && endpoint.endsWith("/")) {
return endpoint.substring(0, endpoint.length() - 1);
}
return endpoint;
}
private static String xmlEscape(String value) {
return value
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&apos;");
}
}
package com.dji.sample.component.oss.service.impl;
import com.dji.sample.component.oss.model.OssConfiguration;
import com.dji.sample.component.oss.service.IOssService;
import com.dji.sample.component.redis.RedisConst;
import com.dji.sample.component.redis.RedisOpsUtils;
import com.dji.sdk.cloudapi.storage.CredentialsToken;
import com.dji.sdk.cloudapi.storage.OssTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import javax.annotation.PreDestroy;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* VersityGW S3 兼容实现(AWS SDK v2 + path-style)。
* <p>
* STS:默认通过 Admin API 创建短期 IAM 用户({@code oss.sts-mode=temp}),到期后删除;
* 仍以明文 AK/SK 下发,但不再泄露长期 root,且可回收。
*/
@Service
@Slf4j
public class VersityGwServiceImpl implements IOssService {
private static final SecureRandom RANDOM = new SecureRandom();
private static final char[] SECRET_CHARS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray();
private S3Client client;
private S3Presigner presigner;
@Autowired
private VersityGwAdminClient adminClient;
@Override
public OssTypeEnum getOssType() {
return OssTypeEnum.VERSITYGW;
}
@Override
public CredentialsToken getCredentials() {
if ("static".equalsIgnoreCase(OssConfiguration.stsMode)) {
return new CredentialsToken(
OssConfiguration.accessKey,
OssConfiguration.secretKey,
"",
OssConfiguration.expire);
}
return createTempCredentials();
}
private CredentialsToken createTempCredentials() {
String accessKey = "tmp" + UUID.randomUUID().toString().replace("-", "").substring(0, 17);
String secretKey = randomSecret(40);
String role = StringUtils.hasText(OssConfiguration.stsUserRole)
? OssConfiguration.stsUserRole : "admin";
adminClient.createUser(accessKey, secretKey, role);
long expireSec = OssConfiguration.expire == null ? 3600L : OssConfiguration.expire;
long expireAt = System.currentTimeMillis() / 1000 + expireSec;
RedisOpsUtils.hashSet(RedisConst.VGW_STS_USERS, accessKey, expireAt);
return new CredentialsToken(accessKey, secretKey, "", expireSec);
}
private static String randomSecret(int length) {
char[] buf = new char[length];
for (int i = 0; i < length; i++) {
buf[i] = SECRET_CHARS[RANDOM.nextInt(SECRET_CHARS.length)];
}
return new String(buf);
}
/**
* 清理已过期的临时 IAM 用户。
*/
@Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
public void cleanupExpiredTempUsers() {
if (!OssConfiguration.enable || OssConfiguration.provider != OssTypeEnum.VERSITYGW) {
return;
}
if ("static".equalsIgnoreCase(OssConfiguration.stsMode)) {
return;
}
Set<Object> fields = RedisOpsUtils.hashKeys(RedisConst.VGW_STS_USERS);
if (fields == null || fields.isEmpty()) {
return;
}
long now = System.currentTimeMillis() / 1000;
for (Object field : fields) {
String accessKey = String.valueOf(field);
Object value = RedisOpsUtils.hashGet(RedisConst.VGW_STS_USERS, accessKey);
long expireAt;
try {
expireAt = Long.parseLong(String.valueOf(value));
} catch (Exception e) {
RedisOpsUtils.hashDel(RedisConst.VGW_STS_USERS, new Object[]{accessKey});
continue;
}
if (expireAt > now) {
continue;
}
try {
adminClient.deleteUser(accessKey);
} catch (Exception e) {
log.warn("Failed to delete expired VersityGW user {}: {}", accessKey, e.getMessage());
} finally {
RedisOpsUtils.hashDel(RedisConst.VGW_STS_USERS, new Object[]{accessKey});
}
}
}
@Override
public URL getObjectUrl(String bucket, String objectKey) {
try {
GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofSeconds(OssConfiguration.expire))
.getObjectRequest(b -> b.bucket(bucket).key(objectKey))
.build();
return presigner.presignGetObject(presignRequest).url();
} catch (Exception e) {
log.error("Failed to generate presigned URL for {}/{}", bucket, objectKey, e);
throw new RuntimeException("The file does not exist on the OssConfiguration.");
}
}
@Override
public Boolean deleteObject(String bucket, String objectKey) {
try {
client.deleteObject(DeleteObjectRequest.builder().bucket(bucket).key(objectKey).build());
return true;
} catch (Exception e) {
log.error("Failed to delete file {}/{}", bucket, objectKey, e);
return false;
}
}
@Override
public InputStream getObject(String bucket, String objectKey) {
try {
byte[] bytes = client.getObjectAsBytes(
GetObjectRequest.builder().bucket(bucket).key(objectKey).build()).asByteArray();
return new ByteArrayInputStream(bytes);
} catch (Exception e) {
log.error("Failed to get object {}/{}", bucket, objectKey, e);
return InputStream.nullInputStream();
}
}
@Override
public void putObject(String bucket, String objectKey, InputStream input) {
if (Boolean.TRUE.equals(objectExists(bucket, objectKey))) {
throw new RuntimeException("The filename already exists.");
}
try {
byte[] bytes = input.readAllBytes();
PutObjectResponse response = client.putObject(
PutObjectRequest.builder().bucket(bucket).key(objectKey).build(),
RequestBody.fromBytes(bytes));
log.info("Upload FlighttaskCreateFile: {}", response.eTag());
} catch (Exception e) {
log.error("Failed to upload FlighttaskCreateFile {}.", objectKey, e);
}
}
@Override
public void createClient() {
if (Objects.nonNull(this.client)) {
return;
}
StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
AwsBasicCredentials.create(OssConfiguration.accessKey, OssConfiguration.secretKey));
S3Configuration s3Configuration = S3Configuration.builder()
.pathStyleAccessEnabled(true)
.chunkedEncodingEnabled(false)
.build();
URI endpoint = URI.create(OssConfiguration.endpoint);
Region region = Region.of(OssConfiguration.region);
this.client = S3Client.builder()
.endpointOverride(endpoint)
.region(region)
.credentialsProvider(credentialsProvider)
.serviceConfiguration(s3Configuration)
.build();
this.presigner = S3Presigner.builder()
.endpointOverride(endpoint)
.region(region)
.credentialsProvider(credentialsProvider)
.serviceConfiguration(s3Configuration)
.build();
}
@PreDestroy
public void destroy() {
if (presigner != null) {
presigner.close();
presigner = null;
}
if (client != null) {
client.close();
client = null;
}
}
@Override
public List<Map<String, String>> listFolders(String bucket, String prefix) {
List<Map<String, String>> folders = new ArrayList<>();
try {
String normalizedPrefix = prefix == null ? "" : prefix;
if (!normalizedPrefix.isEmpty() && !normalizedPrefix.endsWith("/")) {
normalizedPrefix += "/";
}
final String listPrefix = normalizedPrefix;
String continuationToken = null;
do {
ListObjectsV2Request.Builder requestBuilder = ListObjectsV2Request.builder()
.bucket(bucket)
.prefix(listPrefix)
.delimiter("/");
if (continuationToken != null) {
requestBuilder.continuationToken(continuationToken);
}
ListObjectsV2Response response = client.listObjectsV2(requestBuilder.build());
if (response.commonPrefixes() != null) {
for (CommonPrefix commonPrefix : response.commonPrefixes()) {
String folderPath = commonPrefix.prefix();
String folderName = folderPath.substring(listPrefix.length());
if (folderName.endsWith("/")) {
folderName = folderName.substring(0, folderName.length() - 1);
}
Map<String, String> folderInfo = new HashMap<>();
folderInfo.put("name", folderName);
folderInfo.put("prefix", folderPath);
folders.add(folderInfo);
}
}
continuationToken = Boolean.TRUE.equals(response.isTruncated())
? response.nextContinuationToken()
: null;
} while (continuationToken != null);
} catch (Exception e) {
log.error("Failed to list folders: {}", e.getMessage());
}
return folders;
}
@Override
public Boolean objectExists(String bucket, String objectKey) {
try {
client.headObject(HeadObjectRequest.builder().bucket(bucket).key(objectKey).build());
return true;
} catch (NoSuchKeyException e) {
return false;
} catch (S3Exception e) {
if (e.statusCode() == 404) {
return false;
}
throw e;
}
}
@Override
public Boolean copyObject(String bucket, String sourceKey, String destKey) {
try {
client.copyObject(CopyObjectRequest.builder()
.sourceBucket(bucket)
.sourceKey(sourceKey)
.destinationBucket(bucket)
.destinationKey(destKey)
.build());
return true;
} catch (Exception e) {
log.error("Failed to copy object: {}", e.getMessage());
return false;
}
}
}
...@@ -101,4 +101,7 @@ public final class RedisConst { ...@@ -101,4 +101,7 @@ public final class RedisConst {
*/ */
public static final String DEVICE_TOTAL_FLIGHT_TIME_PREFIX = DEVICE_DETAIL_PREFIX + "total_flight_time" + DELIMITER; public static final String DEVICE_TOTAL_FLIGHT_TIME_PREFIX = DEVICE_DETAIL_PREFIX + "total_flight_time" + DELIMITER;
/** VersityGW 临时 STS 用户:hash field=accessKey, value=expireEpochSec */
public static final String VGW_STS_USERS = "oss" + DELIMITER + "vgw_sts_users";
} }
\ No newline at end of file
...@@ -46,7 +46,7 @@ public class StorageServiceImpl extends AbstractMediaService implements IStorage ...@@ -46,7 +46,7 @@ public class StorageServiceImpl extends AbstractMediaService implements IStorage
.setEndpoint(OssConfiguration.endpoint) .setEndpoint(OssConfiguration.endpoint)
.setBucket(OssConfiguration.bucket) .setBucket(OssConfiguration.bucket)
.setCredentials(ossService.getCredentials()) .setCredentials(ossService.getCredentials())
.setProvider(OssConfiguration.provider) .setProvider(OssConfiguration.provider.toClientProvider())
.setObjectKeyPrefix(OssConfiguration.objectDirPrefix) .setObjectKeyPrefix(OssConfiguration.objectDirPrefix)
.setRegion(OssConfiguration.region); .setRegion(OssConfiguration.region);
} }
...@@ -57,7 +57,7 @@ public class StorageServiceImpl extends AbstractMediaService implements IStorage ...@@ -57,7 +57,7 @@ public class StorageServiceImpl extends AbstractMediaService implements IStorage
.setEndpoint(OssConfiguration.endpoint) .setEndpoint(OssConfiguration.endpoint)
.setBucket(OssConfiguration.bucket) .setBucket(OssConfiguration.bucket)
.setCredentials(ossService.getCredentials()) .setCredentials(ossService.getCredentials())
.setProvider(OssConfiguration.provider) .setProvider(OssConfiguration.provider.toClientProvider())
.setObjectKeyPrefix(OssConfiguration.objectDirPrefix + "/" + workspaceId) .setObjectKeyPrefix(OssConfiguration.objectDirPrefix + "/" + workspaceId)
.setRegion(OssConfiguration.region); .setRegion(OssConfiguration.region);
} }
......
...@@ -161,12 +161,15 @@ url: ...@@ -161,12 +161,15 @@ url:
# bucket: cloudapi-bucket # bucket: cloudapi-bucket
# object-dir-prefix: wayline # object-dir-prefix: wayline
# VersityGW:provider=versitygw(OssTypeEnum.VERSITYGW)。下发给 Pilot/机场时会映射为 minio。
# sts-mode=temp:Admin API 创建短期用户(需开启 VersityGW 多租户 IAM);static:直接下发长期 AK/SK。
# 若仍用 MinIO,改为 provider: minio 即可。
oss: oss:
enable: true enable: true
provider: minio provider: versitygw
# 香港 # 香港
# endpoint: https://gt7-oss.geotwin.cc # endpoint: https://gt7-oss.geotwin.cc
# 深圳 # 深圳 / VersityGW
endpoint: https://gt-oss-dev.geotwin.cn endpoint: https://gt-oss-dev.geotwin.cn
access-key: minioadmin access-key: minioadmin
secret-key: minioadmin secret-key: minioadmin
...@@ -174,6 +177,8 @@ oss: ...@@ -174,6 +177,8 @@ oss:
expire: 86400 # 24 * 3600 24小时 expire: 86400 # 24 * 3600 24小时
region: us-east-1 # us-east-1 region: us-east-1 # us-east-1
object-dir-prefix: wayline object-dir-prefix: wayline
sts-mode: temp
sts-user-role: admin
logging: logging:
level: level:
......
...@@ -164,12 +164,15 @@ url: ...@@ -164,12 +164,15 @@ url:
# bucket: cloudapi-bucket # bucket: cloudapi-bucket
# object-dir-prefix: wayline # object-dir-prefix: wayline
# VersityGW:provider=versitygw(OssTypeEnum.VERSITYGW)。下发给 Pilot/机场时会映射为 minio。
# sts-mode=temp:Admin API 创建短期用户(需开启 VersityGW 多租户 IAM);static:直接下发长期 AK/SK。
# 若仍用 MinIO,改为 provider: minio 即可。
oss: oss:
enable: true enable: true
provider: minio provider: versitygw
# 香港 # 香港
# endpoint: https://gt7-oss.geotwin.cc # endpoint: https://gt7-oss.geotwin.cc
# 深圳 # 深圳 / VersityGW
endpoint: https://gt-oss-dev.geotwin.cn endpoint: https://gt-oss-dev.geotwin.cn
access-key: minioadmin access-key: minioadmin
secret-key: minioadmin secret-key: minioadmin
...@@ -177,6 +180,8 @@ oss: ...@@ -177,6 +180,8 @@ oss:
expire: 86400 # 24 * 3600 24小时 expire: 86400 # 24 * 3600 24小时
region: us-east-1 # us-east-1 region: us-east-1 # us-east-1
object-dir-prefix: wayline object-dir-prefix: wayline
sts-mode: temp
sts-user-role: admin
logging: logging:
level: level:
......
package com.dji.sample.component.oss;
import com.dji.sample.component.oss.model.OssConfiguration;
import com.dji.sample.component.oss.service.impl.VersityGwAdminClient;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.model.S3Exception;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
/**
* VersityGW Admin 用户创建/删除冒烟测试(IDE 直接跑 main)。
* <p>
* 前提:网关已开启多租户 IAM(如 --iam-dir),否则 create-user 会失败。
*/
public class VersityGwAdminClientTest {
private static final String ENDPOINT = "https://gt-oss-dev.geotwin.cn";
private static final String ADMIN_ACCESS = "minioadmin";
private static final String ADMIN_SECRET = "minioadmin";
private static final String REGION = "us-east-1";
private static final String BUCKET = "gtfly";
public static void main(String[] args) {
initOssConfig();
VersityGwAdminClient admin = new VersityGwAdminClient();
String tempAccess = "tmp" + UUID.randomUUID().toString().replace("-", "").substring(0, 17);
String tempSecret = "Secret" + UUID.randomUUID().toString().replace("-", "");
try {
System.out.println("=== 1. list-users (before) ===");
System.out.println(admin.listUsers());
System.out.println("=== 2. create-user access=" + tempAccess + " role=admin ===");
admin.createUser(tempAccess, tempSecret, "admin");
System.out.println(" created OK");
System.out.println("=== 3. list-users (after create) ===");
String listed = admin.listUsers();
System.out.println(listed);
if (!listed.contains(tempAccess)) {
System.err.println(" WARN: list-users 响应中未找到 " + tempAccess);
}
System.out.println("=== 4. temp user S3 put/delete ===");
try (S3Client s3 = buildS3(tempAccess, tempSecret)) {
String key = "wayline/_vgw_admin_test_" + System.currentTimeMillis() + ".txt";
s3.putObject(b -> b.bucket(BUCKET).key(key).contentType("text/plain"),
RequestBody.fromBytes(("admin-user-test " + tempAccess).getBytes(StandardCharsets.UTF_8)));
System.out.println(" put OK: " + key);
s3.deleteObject(b -> b.bucket(BUCKET).key(key));
System.out.println(" delete object OK");
}
System.out.println("=== 5. delete-user ===");
admin.deleteUser(tempAccess);
System.out.println(" deleted OK");
System.out.println("=== 6. list-users (after delete) ===");
String afterDelete = admin.listUsers();
System.out.println(afterDelete);
if (afterDelete.contains(tempAccess)) {
System.err.println(" WARN: 删除后 list-users 仍包含 " + tempAccess);
}
System.out.println("=== 7. deleted user should NOT access S3 ===");
try (S3Client s3 = buildS3(tempAccess, tempSecret)) {
s3.listBuckets();
System.err.println(" FAIL: 已删除用户仍能 listBuckets");
} catch (S3Exception e) {
System.out.println(" expected fail: " + e.statusCode() + " " + e.awsErrorDetails().errorMessage());
}
System.out.println("=== 8. delete-user idempotent (404 ok) ===");
admin.deleteUser(tempAccess);
System.out.println(" delete again OK");
System.out.println("VersityGW Admin user lifecycle OK");
} catch (Exception e) {
System.err.println("VersityGW Admin test FAILED: " + e.getMessage());
e.printStackTrace();
try {
admin.deleteUser(tempAccess);
} catch (Exception ignore) {
// best-effort cleanup
}
}
}
private static void initOssConfig() {
OssConfiguration.endpoint = ENDPOINT;
OssConfiguration.accessKey = ADMIN_ACCESS;
OssConfiguration.secretKey = ADMIN_SECRET;
OssConfiguration.region = REGION;
OssConfiguration.bucket = BUCKET;
OssConfiguration.enable = true;
}
private static S3Client buildS3(String access, String secret) {
return S3Client.builder()
.endpointOverride(URI.create(ENDPOINT))
.region(Region.of(REGION))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(access, secret)))
.serviceConfiguration(S3Configuration.builder()
.pathStyleAccessEnabled(true)
.chunkedEncodingEnabled(false)
.build())
.build();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment