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;");
}
}
...@@ -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