如何在Spring Feign客户端中使用p12客户端证书

5
我有一个Spring Boot应用程序,调用远程服务。
这个远程Web服务提供了一个p12文件,可以验证我的应用程序。
如何配置我的Feign客户端以使用p12证书?
我尝试设置以下属性:
-Djavax.net.ssl.keyStore=path_to_cert.p12 -Djavax.net.ssl.keyStorePassword=xxx -Djavax.net.ssl.keyStoreType=PKCS12

但是这并没有改变什么,我仍然收到这个错误提示:
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
3个回答

7

我最终通过大量的盲目尝试和错误来解决了它。

问题在于,默认情况下,Feign构建器使用null SSLSocketFactory构建Feign客户端:

org.springframework.cloud.openfeign.FeignClientsConfiguration#feignBuilder:

@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public Feign.Builder feignBuilder(Retryer retryer) {
    return Feign.builder().retryer(retryer);
}

feign.Feign.Builder:

  public static class Builder {
    // ...
    private Client client = new Client.Default(null, null);

因此,我需要在@Configuration中定义这个bean:
@Bean
@Profile({"prod", "docker"})
public Feign.Builder feignBuilder() {
    return Feign.builder()
        .retryer(Retryer.NEVER_RETRY)
        .client(new Client.Default(getSSLSocketFactory(), null));

使用这种方法:(无法记起来源)
SSLSocketFactory getSSLSocketFactory() {
    char[] allPassword = keyStorePassword.toCharArray();
    SSLContext sslContext = null;
    try {
        sslContext = SSLContextBuilder
            .create()
            .setKeyStoreType(keyStoreType)
            .loadKeyMaterial(ResourceUtils.getFile(keyStore), allPassword, allPassword)
            .build();
    } catch (Exception e) { /* *** */ }
    return sslContext.getSocketFactory();
}

现在,对我来说它可以工作了。我通过调试 feign 客户端调用发现 sslSocketFactory 已被正确传递到底层连接。


所有的 keyStorePassword、keyStoreType 和 keyStore 变量是从哪里来的? - Rotem Linik
一种简单的方法是将值存储在application.properties/yml中,并将它们传递到使用@ConfigurationProperties注释的类的字段keyStorePassword,keyStoreType,keyStore中。 - mpoznyak
1
我假设您正在使用spring-cloud-starter-openfeign。因此,从上面可以得出结论,默认情况下传递javax.net.ssl.keyStore=path_to_cert.p12 -Djavax.net.ssl.keyStorePassword=xxx -Djavax.net.ssl.keyStoreType=PKCS12是无用的,除非我们手动执行上述操作以创建sslSocketFactory。不知道是否有其他替代方法让客户端配置openfeign使用SSL,而无需编写任何代码,仅通过向配置文件添加一些设置即可。 - Nick Wills
凭证确实存储在属性中,并与@Value绑定。这是2年前的事情了,也许自那时以来已经有简化的变化了。 - BiAiB

4
如若不想使用keytool程序,你也可以通过编程实现上述效果。具体方法如下:
class CustomFeignConfiguration {

    private val log = Logger.getLogger(this.javaClass.name)

    @Value("\${client_p12_base64_encoded_string}")
    private val clientP12: String = ""

    @Value("\${client_p12_password}")
    private val clientP12Pass: String = ""

    @Bean
    fun feignClient(): Client {
        val sslSocketFactory= getSSLSocketFactory()
        log.info("CUSTOM FEIGN CLIENT CALLED")
        return Client.Default(sslSocketFactory, DefaultHostnameVerifier())
    }

    private fun getSSLSocketFactory(): SSLSocketFactory {
        val decoder = java.util.Base64.getDecoder()
        val p12 = decoder.decode(clientP12)
        val p12File = File("clientCer.p12")
        p12File.writeBytes(p12)

        try {
            val sslContext = SSLContexts
                .custom()
                .loadKeyMaterial(p12File, clientP12Pass.toCharArray(), clientP12Pass.toCharArray())
                .build()
            return sslContext.socketFactory
        } catch (exception: Exception) {
            throw RuntimeException(exception)
        }

    }
}

使用该配置的FeignClient接口必须明确加载它。
@FeignClient(name = "client", configuration = [CustomFeignConfiguration::class], url = "\${url}")
interface Client {
  ....
  ....
}

SSLContexts库只能使用p12证书,我们需要将PEM格式的证书和密钥转换为P12格式。

使用以下SSL命令从您的PEM证书和密钥创建p12证书:

openssl pkcs12 -export -inkey domain.key -in domain.crt -out domain.p12

请在运行此命令后记录输入的密码。
使用以下命令将此p12证书转换为base64字符串。
base64 domain.p12 > domain.p12.base64

使用以下命令将此多行字符串转换为单行字符串:

tr -d "\n\r" < domain.p12.base64 > domain.p12.base64.singleline

使用该命令中的单行字符串和您在application.properties中记录的密码。

1
与@BiAiB的答案相反,如果有人在2023年遇到同样的问题。我在Feign配置中将客户端配置为Bean,并在那里设置了SSL Socket Factory详细信息。
Gradle导入:
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:3.1.6'
implementation 'io.github.openfeign:feign-httpclient:12.3'

Feign配置中的客户端Bean:

@Bean
public Client feignClient() throws Exception {
    log.info("Configuring SSL Context for Feign Client");
    return new Client.Default(createSSLContext(), SSLConnectionSocketFactory.getDefaultHostnameVerifier());
}

并从资源文件中创建了SSL Socket Factory,如下所示:

private SSLSocketFactory createSSLContext() throws Exception {
    String trustStorePath = "classpath:cacerts"
    String keyStorePath = "classpath:client-key.pfx"

    log.info("Trust Store for Feign Client: " + trustStorePath);
    log.info("Key Store for Feign Client: " + keyStorePath);

    KeyStore keyStore = KeyStore.getInstance("PKCS12"); // PKCS12 for PFX files. Change this to 'JKS' if you are using java keystore
    keyStore.load(new FileInputStream(ResourceUtils.getFile(keyStorePath)), keyStorePassword.toCharArray());

    SSLContext context = SSLContextBuilder.create()
            .loadTrustMaterial(ResourceUtils.getFile(trustStorePath), trustStorePassword.toCharArray())
            .loadKeyMaterial(keyStore, keyStorePassword.toCharArray())
            .build();
    return context.getSocketFactory();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接