我们有一个 Spring Boot 后端,尝试执行商家验证并检索 Apple Pay 的支付会话。按照 Apple 的这些步骤,我们能够生成商家验证证书。当我尝试以编程方式创建密钥库并添加 Apple 颁发的证书以在我的 Web 客户端调用中使用时,出现以下错误:
Caused by: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
这是代码片段:
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
val certificateFactory = CertificateFactory.getInstance("X.509")
val keyStore: KeyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load(null)
val merchantValidationInputStream = FileInputStream(<PATH_TO_CERT_FROM_APPLE>)
val merchantValidationCert= certificateFactory.generateCertificate(merchantValidationInputStream)
keyStore.setCertificateEntry("applePayMerchantValidationCert", merchantValidationCert)
val sslContext = SslContextBuilder
.forClient()
.trustManager(trustManagerFactory)
.build()
val httpClient = HttpClient.create()
.secure { it.sslContext(sslContext) }
val webClient = builder
.clientConnector(ReactorClientHttpConnector(httpClient))
.baseUrl(<VALIDATION_URL_PASSED_FROM_WEB>)
.build()
return webClient.post()
.bodyValue(<REQUEST_BODY_WITH_MERCHANTID_DISPLAYNAME_INITIATIVE_INITIATIVE_CONTEXT>)
.retrieve()
.toBodilessEntity()
// For now I am just trying to get this call working and not doing anything with the response.
// Using https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/requesting_an_apple_pay_payment_session#3199965
// as reference for the request body structure
我可以获得一些帮助来弄清楚这里可能发生什么吗?
工具/框架版本
我尝试过的事情:
与@janani-subbiah 合作后,我们确定以下是解决方案。
有关完整说明,请访问 Apple Pay 文档,但我想对我们需要进行的证书转换以及使用它的 Spring Boot 代码进行高级概述。另请注意,您可以使用 OpenSSL 进行大部分证书更改,但为了简单起见,我只是使用 Mac 钥匙串工具。
data class PaymentSessionRequest(
val merchantIdentifier: String = "merchant.your merchant identifier",
val displayName: String = "Any Apple Pay Display Name",
val initiative: String = "web",
val initiativeContext: String = "Fully qualified domain of the frontend"
)
val pass = "Password you set when exporting the p12 cert in step 8" // Should be set as a secret and exposed as an environment variable.
val inputStream = javaClass.getResourceAsStream("/merchant_id.p12") // Should be mounted as a secret file and loaded from there.
val keyStore = KeyStore.getInstance("PKCS12")
keyStore.load(inputStream, pass.toCharArray())
val keyManagerFactory = KeyManagerFactory.getInstance("SunX509")
keyManagerFactory.init(keyStore, pass.toCharArray())
val context = SslContextBuilder.forClient()
.keyManager(keyManagerFactory)
.build()
val client: HttpClient = HttpClient.create().secure { spec -> spec.sslContext(context) }
val connector: ClientHttpConnector = ReactorClientHttpConnector(client)
val webClient = WebClient.builder()
.baseUrl("https://apple-pay-gateway.apple.com") // Should use validation URL passed in from the Apple Pay JS SDK and include IP address validation based on Apple's docs
.clientConnector(connector)
.build()
webClient.post()
.uri("/paymentservices/paymentSession")
.bodyValue(PaymentSessionRequest())
.retrieve()
.bodyToMono<String>()
.doOnNext { logger.info("Apple Pay Session response: $it") }
尝试使用 OpenSSL 将证书文件从 Apple (.cer) 转换为 PEM 格式来执行此转换。
外壳 openssl x509 -inform der -in your_certificate.cer -out your_certificate.pem
创建Java KeyStore并将转换后的PEM证书导入其中。
Java 代码 导入 java.io.FileInputStream; 导入 java.security.KeyStore; 导入 java.security.cert.Certificate;
// ...
String keyStorePath = "path/to/your/keystore.jks";
char[] keyStorePassword = "your_keystore_password".toCharArray();
String certificatePath = "path/to/your_certificate.pem";
String alias = "your_certificate_alias";
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, keyStorePassword);
FileInputStream certificateFileInputStream = new FileInputStream(certificatePath);
Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(certificateFileInputStream);
keyStore.setCertificateEntry(alias, certificate);
keyStore.store(new FileOutputStream(keyStorePath), keyStorePassword);
配置您的 SSLContext 以使用 KeyStore。 导入 javax.net.ssl.KeyManagerFactory; 导入 javax.net.ssl.SSLContext;
// ...
String keyStoreType = "JKS";
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keyStorePassword);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
在 HttpClient 中使用配置的 SSLContext。 导入 org.apache.http.conn.ssl.SSLContextBuilder; 导入 org.apache.http.impl.client.CloseableHttpClient; 导入 org.apache.http.impl.client.HttpClients;
// ...
CloseableHttpClient httpClient = HttpClients.custom()
.setSSLContext(sslContext)
.build();
这应该可以解决
SSLHandshakeException
问题。
此外,请将我上面给出的示例中的占位符值 (
your_certificate.cer, your_certificate.pem, path/to/your/keystore.jks, your_keystore_password, and your_certificate_alias
) 替换为您应用程序中的值。