首页 > 解决方案 > 想要连接到同一个本地 S3 兼容的不同存储

问题描述

目前,我们正在本地 S3 兼容存储中连接到我们团队的存储桶。

配置类:

@Configuration
public class S3Config {

@Value("${aws.access_key_id}")
private String awsId;
@Value("${aws.secret_access_key}")
private String awsKey;
@Value("${s3.endpoint}")
private String endpoint;

@Value("${s3.keystorePD}")
private String keystorePD;

@Value("${s3.jksFile}")
private String jksFile;


private static final Logger log = LoggerFactory.getLogger(S3Config.class);
@PostConstruct
public void init()
{

    log.info("Initializing SSL Configuration for S3 bucket");
    System.setProperty(ApplicationConstants.KEY_STORE_TYPE,"jks");
    System.setProperty(ApplicationConstants.TRUST_STORE_TYPE,"jks");
    System.setProperty(ApplicationConstants.KEY_STORE_PROPERTY,Thread.currentThread().getContextClassLoader().getResource(jksFile).getFile());
    System.setProperty(ApplicationConstants.TRUST_STORE_PROPERTY,Thread.currentThread().getContextClassLoader().getResource(jksFile).getFile());
    System.setProperty(ApplicationConstants.KEY_STORE_PD,keystorePD);
    System.setProperty(ApplicationConstants.TRUST_STORE_PD,keystorePD);
    log.info("Successfully initialized SSL Configuration for S3 bucket");
}

@Bean
public AmazonS3 s3client() {
    log.info("Initializing the S3 client");
    AmazonS3 s3Client = null;
    BasicAWSCredentials awsCreds = new BasicAWSCredentials(awsId,
            awsKey);
    s3Client = new AmazonS3Client(awsCreds);
    s3Client.setEndpoint(endpoint);
    return s3Client;
}

用法 :

@Autowired
AmazonS3 s3Client;

public void uploadFile(File file, String targetLocation, String bucketName) {
    log.info("converted file size : " + file.getTotalSpace());
    log.info("bucketName : " + bucketName);
    log.info("targetLocation : " + targetLocation);
    s3Client.putObject(bucketName, targetLocation, file);
}

我的要求是为每个传入请求动态创建客户端。因此,如果团队 A 通过,则使用团队 A 的 jks 文件的 s3 配置连接到存储。

我在用 :

我该如何做到这一点?

更新:

从应用程序开始,我将拥有 jks 文件、access_keys、access_ids。

标签: javaspring-bootamazon-s3

解决方案


在这种情况下有两个独特的问题

  1. 使用非系统属性提供加密材料
  2. 为每个请求切换(并在需要时构建)凭据。

以下解决方案大纲将解决这两个问题。它使用 StaticEncryptionMaterialsProvider 来避免对系统属性的依赖。为了解决第二个问题,S3ClientResolver 使用一个 REQUEST 范围,将 proxyMode 作为 TARGET_CLASS 以确保在 spring 上下文中的接线在这种情况下正常工作。

@ConfigurationProperties(prefix = "teams")
@Component
public class CredentialsMap extends HashMap<String, S3Credentials> {
    // key will be team name and value will have s3Credentials
}

public class S3Credentials {
    private String awsId;
    private String awsKey;
    private String endpoint;
    private String keystorePD;
    private String jksFile;
    /* Add all getter and setters as well */
}

/**
 * Building client is an expensive operation. This provider lazily builds the clients
 * and cache them using Map for subsequent usage without incurring built cost agian.
 */
public class S3ClientProvider {
    @Autowired
    private CredentialsMap credentialsMap;

    private Map<String, AmazonS3> clients;

    public AmazonS3 getClient(String client) {
        if (!clients.containsKey(client))
            clients.put(client, buildClient(client));
        return clients.get(client);
    }

    private AmazonS3 buildClient(String client) {
        S3Credentials cred = credentialsMap.get(client);
        // TODO - add try/catch
        KeyPair kp = getKeyPair(cred);

        AWSStaticCredentialsProvider cp = new AWSStaticCredentialsProvider(new BasicAWSCredentials(
                cred.getAwsId(), cred.getAwsKey()));

        AmazonS3 as3 = AmazonS3EncryptionClientBuilder
                .standard()
                .withRegion(Regions.US_WEST_2)
                .withCredentials(cp)
                .withCryptoConfiguration(new CryptoConfiguration(CryptoMode.AuthenticatedEncryption))
                .withEncryptionMaterials(new StaticEncryptionMaterialsProvider(new EncryptionMaterials(kp)))
                .build();
        as3.setEndpoint(cred.getEndpoint());
        return as3;
    }

    private KeyPair getKeyPair(S3Credentials cred) throws Exception {
        FileInputStream is = new FileInputStream(Thread.currentThread().getContextClassLoader()
                .getResource(cred.getJksFile()).getFile());

        KeyStore keystore = KeyStore.getInstance("jks");
        keystore.load(is, cred.getKeystorePD().toCharArray());

        // the only key in your JKS must have this alias
        // you may also add the alias as another property in your configuration instead
        String alias = "myS3Key";

        Key key = keystore.getKey(alias, cred.getKeystorePD().toCharArray());
        if (key instanceof PrivateKey) {
            Certificate cert = keystore.getCertificate(alias);
            PublicKey publicKey = cert.getPublicKey();
            return new KeyPair(publicKey, (PrivateKey) key);
        }
        throw new RuntimeException("Invalid key");
    }
}

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class S3ClientResolver {

    /* You can inject current request OR any other bean for that matter here */
    /* Spring authenticaiotn object to get access to current logged in user */
    @Autowired
    private Authentication authentication;

    @Autowired
    private S3ClientProvider provider;

    public AmazonS3 getClient() {
        // deduce client value from current authenticaiotn object as may be applicable
        String client = "TODO";
        return provider.getClient(client);
    }
}

可以在 spring boot 的 application.yml 中指定不同客户端的配置,如下所示

teams:
  one:
    awsId: abc
    awsKey: abc
    endpoint: https://example.com
    keystorePD: onejskpass
    jksFile: one.jks
  two:
    awsId: abc2
    awsKey: abc2
    endpoint: https://example.com
    keystorePD: twojskpass
    jksFile: two.jks

有了这个,您的代码将如下所示

@Autowired
private S3ClientResolver resolver;

public void uploadFile(File file, String targetLocation, String bucketName) {
    log.info("converted file size : " + file.getTotalSpace());
    log.info("bucketName : " + bucketName);
    log.info("targetLocation : " + targetLocation);
    resolver.getClient().putObject(bucketName, targetLocation, file);
}

请根据自己的环境进行调整。我很快从我过去的一项类似工作中提取了这些碎片。我在上面的代码段中留下了许多项目作为评论。请仔细阅读并根据您的用例进行相应调整。

[编辑 - 2020 年 7 月 15 日]

jackfr0st,密钥对生成逻辑正在工作,我刚刚使用以下独立程序对其进行了验证,该程序是上面提到的内容的精确副本,并在线更正了 return 语句(之前缺少)new KeyPair(publicKey, (PrivateKey) key)- 为此我生成了一个新的密钥库(testjks .jks) 使用以下命令 -keytool -genkey -alias mydomain -keyalg RSA -keystore keystore.jks -keysize 2048

import java.io.FileInputStream;
import java.security.*;
import java.security.cert.Certificate;

public class ExtractKeypair {
    public static void main(String[] args)  throws Exception {
        KeyPair kp = generateKeypair("testjks.jks");
    }

    public static KeyPair generateKeypair(String file) throws Exception {
        FileInputStream is = new FileInputStream(Thread.currentThread().getContextClassLoader()
                .getResource(file).getFile());
        KeyStore keystore = KeyStore.getInstance("jks");
        keystore.load(is, "password".toCharArray());

        // the only key in your JKS must have this alias
        // you may also add the alias as another property in your configuration instead
        String alias = "mydomain";

        Key key = keystore.getKey(alias, "password".toCharArray());
        if (key instanceof PrivateKey) {
            Certificate cert = keystore.getCertificate(alias);
            PublicKey publicKey = cert.getPublicKey();
            return new KeyPair(publicKey, (PrivateKey) key);
        }
        throw new RuntimeException("Invalid key");
    }
}

如果您仍然面临问题,我建议您查看返回的资源 ( Thread.currentThread().getContextClassLoader().getResource(file).getFile()),因为如果不正确,这是唯一可能导致问题的变量。我用 oracle jdk 8 测试了上面的程序。


推荐阅读