记一次Tomcat证书由jks更换为pfx的艰辛过程

背景介绍

https不了解,对https双向认证更是一脸懵

客户方要求系统提供https的服务,一年前申请某网站的免费证书,下载后包含了各种web容器的证书,应用程序的web容器为tomcat8,最后选用了tomcat下的证书,为jks格式。

一年后免费服务到期,需要更换证书,客户申请了阿里云的证书,下载下来的格式为pfx,客户说只有这种格式的。坑已挖好。

操作记录

百度搜索结果说可以将pfx转换成jks
命令如下:

keytool -importkeystore -srckeystore xxx.pfx -destkeystore xxx.jks -srcstoretype PKCS12 -deststoretype JKS -srcstorepass xxx -deststorepass xxx -srcalias alias -destalias destalias

因为原来tomcat中配置了jks相关的信息,想着直接将新的jks的文件名和密码保持一致,直接替换原文件就可以了。结果被坑。

按原来的文件名、密码生成新证书后,启动tomcat,事实证明想的还是太简单。
报错信息如下:

15-Nov-2019 12:20:20.221 SEVERE [main] org.apache.coyote.AbstractProtocol.init Failed to initialize end point associated with ProtocolHandler ["http-nio-443"]
 java.security.UnrecoverableKeyException: Cannot recover key
    at sun.security.provider.KeyProtector.recover(Unknown Source)
    at sun.security.provider.JavaKeyStore.engineGetKey(Unknown Source)
    at sun.security.provider.JavaKeyStore$JKS.engineGetKey(Unknown Source)
    at java.security.KeyStore.getKey(Unknown Source)
    at sun.security.ssl.SunX509KeyManagerImpl.(Unknown Source)
    at sun.security.ssl.KeyManagerFactoryImpl$SunX509.engineInit(Unknown Source)
    at javax.net.ssl.KeyManagerFactory.init(Unknown Source)
    at org.apache.tomcat.util.net.jsse.JSSESocketFactory.getKeyManagers(JSSESocketFactory.java:617)
    at org.apache.tomcat.util.net.jsse.JSSESocketFactory.getKeyManagers(JSSESocketFactory.java:546)
    .
    .
    .
    Caused by: org.apache.catalina.LifecycleException: Protocol handler initialization failed
    at org.apache.catalina.connector.Connector.initInternal(Connector.java:962)
    at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:102)
    ... 12 more
Caused by: java.security.UnrecoverableKeyException: Cannot recover key
    at sun.security.provider.KeyProtector.recover(Unknown Source)
    at sun.security.provider.JavaKeyStore.engineGetKey(Unknown Source)

处理办法:
将新的jks的密码和pfx的密码保持一致,修改tomcat配置文件中的密码。

其实tomcat可以直接配置pfx格式证书,需要指定 keystoreType="PKCS12"
keystoreFile="/证书路径/名称.pfx" keystoreType="PKCS12" keystorePass="证书密码"
但这次的客户有做https双向认证,之前的代码已经写死支持jks证书库,所以需要生成jks

遇到的异常说明

密码错误异常

15-Nov-2019 11:59:55.017 SEVERE [main] org.apache.coyote.AbstractProtocol.init Failed to initialize end point associated with ProtocolHandler ["http-nio-443"]
 java.io.IOException: Keystore was tampered with, or password was incorrect
    at sun.security.provider.JavaKeyStore.engineLoad(Unknown Source)
    at sun.security.provider.JavaKeyStore$JKS.engineLoad(Unknown Source)
    .
    .
    .
    Caused by: java.security.UnrecoverableKeyException: Password verification failed
    ... 25 more

pfx和jks密码不匹配

异常信息如下:

15-Nov-2019 12:20:20.221 SEVERE [main] org.apache.coyote.AbstractProtocol.init Failed to initialize end point associated with ProtocolHandler ["http-nio-443"]
 java.security.UnrecoverableKeyException: Cannot recover key

双向认证

示例代码


import javax.net.ssl.*;
import java.io.*;
import java.net.URL;
import java.nio.charset.Charset;
import java.security.KeyStore;


/**
 * #2
 * HTTPS 双向认证 - use truststore
 * 原生方式
 *
 * @Author soft_xiang
 * @Date 7/11/2017
 */
public class HttpsTruststoreNativeDemo {
    // 客户端证书路径,用了本地绝对路径,需要修改 调用方证书
    private final static String CLIENT_CERT_FILE = "D:\\xxx\\https\\xxx-ip.p12";
    // 客户端证书密码
    private final static String CLIENT_PWD = "pwd";
    // 信任库路径 (被调用方证书合集)
    private final static String TRUST_STRORE_FILE = "D:\\xxx\\https\\xxx.jks";
    // 信任库密码
    private final static String TRUST_STORE_PWD = "xxx";


    private static String readResponseBody(InputStream inputStream) throws IOException {
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
            StringBuffer sb = new StringBuffer();
            String buff = null;
            while ((buff = br.readLine()) != null) {
                sb.append(buff + "\n");
            }
            return sb.toString();
        } finally {
            inputStream.close();
        }
    }

    public static void httpsCall() throws Exception {
        // 初始化密钥库
        KeyManagerFactory keyManagerFactory = KeyManagerFactory
                .getInstance("SunX509");
        KeyStore keyStore = getKeyStore(CLIENT_CERT_FILE, CLIENT_PWD, "PKCS12");
        keyManagerFactory.init(keyStore, CLIENT_PWD.toCharArray());

        // 初始化信任库
        TrustManagerFactory trustManagerFactory = TrustManagerFactory
                .getInstance("SunX509");
        KeyStore trustkeyStore = getKeyStore(TRUST_STRORE_FILE, TRUST_STORE_PWD, "JKS");
        trustManagerFactory.init(trustkeyStore);

        // 初始化SSL上下文
        SSLContext ctx = SSLContext.getInstance("SSL");
        ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory
                .getTrustManagers(), null);
        SSLSocketFactory sf = ctx.getSocketFactory();

        HttpsURLConnection.setDefaultSSLSocketFactory(sf);
        String url = "post url";
        URL urlObj = new URL(url);
        HttpsURLConnection con = (HttpsURLConnection) urlObj.openConnection();
        con.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36");
        con.setRequestProperty("Accept-Language", "zh-CN;en-US,en;q=0.5");
        con.setRequestProperty("Content-Type", "text/xml");
        con.setRequestMethod("POST");
        con.setRequestProperty("Content-Length", "0");
        con.setDoInput(true);
        con.setDoOutput(true);
        DataOutputStream os = new DataOutputStream(con.getOutputStream());
        os.write("".getBytes("UTF-8"), 0, 0);
        os.flush();
        os.close();
        con.connect();
        InputStream is = con.getInputStream();
        Integer code = con.getResponseCode();
        final String contentType = con.getContentType();
        System.out.println(code + ":" + contentType);
        String response = readResponseBody(is);
        System.out.println(response);
    }

    /**
     * 获得KeyStore
     *
     * @param keyStorePath
     * @param password
     * @return
     * @throws Exception
     */
    private static KeyStore getKeyStore(String keyStorePath, String password, String type)
            throws Exception {
        FileInputStream is = new FileInputStream(keyStorePath);
        KeyStore ks = KeyStore.getInstance(type);
        ks.load(is, password.toCharArray());
        is.close();
        return ks;
    }


    public static void main(String[] args) throws Exception {
        httpsCall();
    }

}

双向认证示例中可能碰到的问题


在允许调用的服务器,直接浏览器打开需要访问的地址,调用出现401,sap问题,协调sap解决


证书库密码错误


证书错误,确认sap加入信任库的证书和代码中调用的证书库中的证书是同一个

常用命令

keytool -import -alias xxx -file "xxx.der" -keystore xxx.jks -storepass pass
将证书导入信任库

keytool -export -alias xxx -keystore abc.jks -storepass pass -file xxx.cer
将证书库abc.jks中别名为xxx的证书导出为xxx.cer,cer客户端证书

keytool -import -alias xxx -file xxx.cer -keystore "%JAVA_HOME%/jre/lib/security/cacerts" -storepass changeit -trustcacerts
将客户端证书导入jdk的默认信任库(cacerts为jdk默认信任库,导入之前记得备份)

你可能感兴趣的