SpringBoot结合keytool配置ssl双向认证通信


【关键词】:keytool、SpringBoot、restTemplate、ssl、双向认证、https、keystore、jks

一、需求

各对等机构各自部署一套服务平台(称作节点peer),要求在各机构平台之间内部通信使用ssl加密。支持自签名方式或ca认证方式。

二、环境

win10,jdk1.8.0_261。

三、技术储备

  • jdk自带的keytool命令;
  • server.ssl服务端配置;
  • restTemplate客户端集成ssl配置;
  • 支持https的同时也支持http,便于调测;
  • 打开ssl握手日志-Djavax.net.debug=ssl:handshake,便于分析ssl。

四、项目实现和测试

4.0、大体思路

  1. 首先准备一个简单的SpringBoot项目,peer1写个/api/doSomething接口,内部再用restTemplate调用peer2的https接口https://peer2/interact/reply/hello
  2. keytool为peer1和peer2分别生成秘钥库以及信任秘钥库,并配置进ssl服务端和客户端;
  3. 同时开启http端口,便于postman直接调用peer1的http接口http://peer1/api/doSomething
  4. 测试自签名的peer1和peer2互认证书,以及同一CA签发的peer1和peer2证书,是否可以调通。

4.1、项目准备

完整项目点这里:
https://github.com/oaHeZgnoS/ssl-peer

  • http外部接口:com.szh.peer.ctrl.SystemCtrl.doSomething()
  • https内部通信接口:com.szh.peer.ctrl.InteractCtrl.replyHello()

4.2、keytool生成证书并配置

4.2.1、自签名peer1/peer2

peer1和peer2两个机构各自生成自己秘钥库,并将对方配置在自己的信任秘钥库中,从而达到互认互信加密通信。信任秘钥库可以合并在秘钥库,也可以是独立于秘钥库的另一个文件中。

4.2.1.1、信任密钥库合并在密钥库

## 信任密钥库合并在密钥库

一、peer1生成密钥库以及导出公钥证书
1、生成peer1的密钥库peer1.jks
keytool -genkeypair -alias peer1 -keystore peer1.jks -storepass passwd1 -dname CN=peer1,OU=peer1,O=peer1,L=peer1,C=CN
2、查看密钥库详情
keytool -list -keystore peer1.jks -storepass passwd1 -v
3、peer1导出公钥证书
keytool -export -alias peer1 -file peer1.cer -keystore peer1.jks -storepass passwd1

二、peer2生成密钥库以及导出公钥证书
1、生成peer2的密钥库peer2.jks
keytool -genkeypair -alias peer2 -keystore peer2.jks -storepass passwd2 -dname CN=peer2,OU=peer2,O=peer2,L=peer2,C=CN
2、查看密钥库详情
keytool -list -keystore peer2.jks -storepass passwd2 -v
3、peer2导出公钥证书
keytool -export -alias peer2 -file peer2.cer -keystore peer2.jks -storepass passwd2

三、peer1/peer2互相导入对方密钥库,建立信任
1、peer2的证书导入peer1密钥库
keytool -import -alias peer2 -file peer2.cer -keystore peer1.jks -storepass passwd1
2、peer1的证书导入peer2密钥库
keytool -import -alias peer1 -file peer1.cer -keystore peer2.jks -storepass passwd2

四、检验peer1/peer2是否具有自己的privateKey和对方的cert
1、检验peer1是否具有自己的privateKey和peer2的cert
keytool -list -keystore peer1.jks -storepass passwd1
2、检验peer2是否具有自己的privateKey和peer1的cert
keytool -list -keystore peer2.jks -storepass passwd2

五、转换JKS格式为P12(便于在postman单向测试证书)
1、转换peer2.jks->peer2.p12
keytool -importkeystore -srckeystore peer2.jks -destkeystore peer2.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass passwd2 -deststorepass passwd2 -srckeypass passwd2 -destkeypass passwd2 -srcalias peer2 -destalias peer2 -noprompt
2、转换peer1.jks->peer1.p12
keytool -importkeystore -srckeystore peer1.jks -destkeystore peer1.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass passwd1 -deststorepass passwd1 -srckeypass passwd1 -destkeypass passwd1 -srcalias peer1 -destalias peer1 -noprompt

4.2.1.2、信任密钥库独立于密钥库

## 信任密钥库独立于密钥库

一、peer1生成密钥库以及导出公钥证书
1、生成peer1的密钥库peer1.jks
keytool -genkeypair -alias peer1 -keystore peer1.jks -storepass passwd1 -dname CN=peer1,OU=peer1,O=peer1,L=peer1,C=CN
2、查看密钥库详情
keytool -list -keystore peer1.jks -storepass passwd1 -v
3、peer1导出公钥证书
keytool -export -alias peer1 -file peer1.cer -keystore peer1.jks -storepass passwd1

二、peer2生成密钥库以及导出公钥证书
1、生成peer2的密钥库peer2.jks
keytool -genkeypair -alias peer2 -keystore peer2.jks -storepass passwd2 -dname CN=peer2,OU=peer2,O=peer2,L=peer2,C=CN
2、查看密钥库详情
keytool -list -keystore peer2.jks -storepass passwd2 -v
3、peer2导出公钥证书
keytool -export -alias peer2 -file peer2.cer -keystore peer2.jks -storepass passwd2

三、peer1/peer2互相导入对方信任密钥库
1、peer2的证书导入peer1信任密钥库
keytool -import -alias peer1Trust -file peer2.cer -keystore peer1Trust.jks -storepass passwd1
2、peer1的证书导入peer2信任密钥库
keytool -import -alias peer2Trust -file peer1.cer -keystore peer2Trust.jks -storepass passwd2

四、检验peer1/peer2是否具有对方的cert
1、检验peer1的信任密钥库是否有peer2的cert
keytool -list -keystore peer1Trust.jks -storepass passwd1
2、检验peer2的信任密钥库是否有peer1的cert
keytool -list -keystore peer2Trust.jks -storepass passwd2

4.2.2、CA签发peer1/peer2

peer1和peer2两个机构各自生成自己秘钥库jks,再生成csr证书请求文件,再通过同一个CA签发生成证书。再将ca和自身证书配置在自己的秘钥库和信任秘钥库中,从而达到互认互信加密通信。信任秘钥库可以合并在秘钥库,也可以是独立于秘钥库的另一个文件中。

CA的方式更灵活一些,不像自签名的方式,一旦加入了peer3,那么peer1和peer2都要再把peer3的证书导入到自己信任秘钥库中,一般需要重启服务;而CA方式避免了这个问题。

4.2.2.1、信任密钥库合并在密钥库

# CA jks
keytool -genkeypair -alias ca -dname "cn=Local Network - Development" -validity 10000 -keyalg RSA -keysize 2048 -ext bc:c -keystore ca.jks -keypass passwdca -storepass passwdca

# CA cert
keytool -exportcert -noprompt -rfc -alias ca -file ca.crt -keystore ca.jks -storepass passwdca

keytool -exportcert -noprompt -rfc -alias ca -file ca.pem -keystore ca.jks -storepass passwdca


#Peer1
# generate private keys (for peer1)
keytool -genkeypair -alias peer1 -dname cn=peer1 -validity 10000 -keyalg RSA -keysize 2048 -keystore peer1.jks -keypass passwd1 -storepass passwd1

# generate a certificate for peer1 signed by ca (ca -> peer1)

# generate csr
keytool -certreq -noprompt -alias peer1 -sigalg SHA256withRSA -file peer1.csr -keypass passwd1 -keystore peer1.jks -dname "CN=peer1" -storepass passwd1

# sign csr
keytool -gencert -noprompt -infile peer1.csr -outfile peer1.crt -alias ca -sigalg SHA256withRSA -validity 10000 -ext ku:c=dig,keyEnc -ext "san=dns:localhost,ip:127.0.0.1" -ext eku=sa,ca -keypass passwdca -keystore ca.jks -storepass passwdca -rfc

# import ca.crt into peer1.jks
keytool -keystore peer1.jks -storepass passwd1 -importcert -trustcacerts -noprompt -alias ca -file ca.crt

# import peer1.crt into peer1.jks (complete crt chain ca->peer1 imported)
keytool -keystore peer1.jks -storepass passwd1 -importcert -noprompt -alias peer1 -file peer1.crt



#Peer2
# generate private keys (for peer2)
keytool -genkeypair -alias peer2 -dname cn=peer2 -validity 10000 -keyalg RSA -keysize 2048 -keystore peer2.jks -keypass passwd2 -storepass passwd2

# generate a certificate for peer2 signed by ca (ca -> peer2)

# generate csr
keytool -certreq -noprompt -alias peer2 -sigalg SHA256withRSA -file peer2.csr -keypass passwd2 -keystore peer2.jks -dname "CN=peer2" -storepass passwd2

# sign csr
keytool -gencert -noprompt -infile peer2.csr -outfile peer2.crt -alias ca -sigalg SHA256withRSA -validity 10000 -ext ku:c=dig,keyEnc -ext "san=dns:localhost,ip:127.0.0.1" -ext eku=sa,ca -keypass passwdca -keystore ca.jks -storepass passwdca -rfc

# import ca.crt into peer2.jks
keytool -keystore peer2.jks -storepass passwd2 -importcert -trustcacerts -noprompt -alias ca -file ca.crt

# import peer2.crt into peer2.jks (complete crt chain ca->peer2 imported)
keytool -keystore peer2.jks -storepass passwd2 -importcert -noprompt -alias peer2 -file peer2.crt

4.2.2.2、信任密钥库独立于密钥库

可以单独把ca证书导入到独立的信任库。

# Trust Store containing ca.crt
keytool -importcert -noprompt -alias catrust -file ca.crt -keypass passwdtrust -keystore trust.jks -storepass passwdtrust

4.2.3、ssl服务端配置

## ssl start
server.ssl.pure-key-store=peer1.jks
server.ssl.pure-trust-store=trust.jks
# 私钥库
server.ssl.enabled=true
server.ssl.key-store-type=JKS
server.ssl.key-store=classpath:${server.ssl.pure-key-store}
server.ssl.key-store-password=passwd1
server.ssl.key-alias=peer1
# 受信任密钥库
server.ssl.trust-store=classpath:${server.ssl.pure-trust-store}
server.ssl.trust-store-password=passwdtrust
server.ssl.trust-store-provider=SUN
server.ssl.trust-store-type=JKS
server.ssl.client-auth=need
## ssl end

4.2.4、ssl客户端配置

客户端restTemplate底层实现选用httpClient,

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.5</version>
</dependency>

构造httpClient的sslContext,

@Configuration
@Slf4j
public class RestTemplateConfig {

    @Value("${server.ssl.pure-key-store}")
    public String keyStore;
    @Value("${server.ssl.key-store-password}")
    public String keyStorePassword;
    @Value("${server.ssl.pure-trust-store}")
    public String trustStore;
    @Value("${server.ssl.trust-store-password}")
    public String trustStorePassword;

    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory httpComponentsClientHttpRequestFactory) {
        RestTemplate restTemplate = new RestTemplate(httpComponentsClientHttpRequestFactory);
        restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        log.info("loading restTemplate");
        return restTemplate;
    }

    @Bean("httpComponentsClientHttpRequestFactory")
    public ClientHttpRequestFactory httpComponentsClientHttpRequestFactory() throws IOException, UnrecoverableKeyException,
            CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
        SSLContext sslContext = SSLContextBuilder
                .create()
                // 作为client,load自己密钥库;理论上当server的server.ssl.client-auth=need时,通过它可以获取client的公钥证书传递给server来验证
                .loadKeyMaterial(new ClassPathResource(keyStore).getURL(),
                        trustStorePassword.toCharArray(), keyStorePassword.toCharArray())
                // 作为client,load自己的信任库;在请求server之前,client先通过它判断server是否受信
                .loadTrustMaterial(new ClassPathResource(trustStore).getURL(), trustStorePassword.toCharArray())
                .build();
        HttpClient client = HttpClients.custom()
                .setSSLContext(sslContext)
                .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) // 不需要主机验证
                .build();
        HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(client);
        return requestFactory;
    }
}

4.3、同时开启http

自定义一个http端口,

server.http-port=8288

配置web容器也开启http端口,

@Component
public class TomcatServerCustomer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
    @Value("${server.http-port}")
    public Integer httpPort;

    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("http");
        connector.setPort(httpPort);
        factory.addAdditionalTomcatConnectors(connector);
    }
}

服务启动后会打印https和http端口信息,

Tomcat started on port(s): 28288 (https) 8288 (http) with context path ''

4.4、测试

http调用peer,触发内部https调用另一peer,成功返回,

curl http://127.0.0.1:8688/api/doSomething
hello, peer2
curl http://127.0.0.1:8288/api/doSomething
hello, peer1

当然也可以在postman中模拟一个客户端peer2,直接请求https接口https://peer1/interact/reply/hello,只不过需要在Settings-Certificates-Client Certificates导入peer2的证书即可。

从peer2的jks格式秘钥库中获取p12证书,

keytool -importkeystore -srckeystore peer2.jks -destkeystore peer2.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass passwd2 -deststorepass passwd2 -srckeypass passwd2 -destkeypass passwd2 -srcalias peer2 -destalias peer2 -noprompt

除此之外,还有其他命令,

查看帮助命令:

keytool -command_name -help
keytool -genkeypair -help

查看密钥库里面的信息:

keytool -list -keystore peer1.jks -v

查看指定证书文件的信息:

keytool -printcert -file peer1.cer -v

cer证书转为pem:

openssl x509 -in ca.cer -inform DER -out ca.pem -outform PEM

五、其他

5.1、ssl单向认证

是指peer节点作为服务端,不用去认证客户端,这样每个客户端也不用发送自己的公钥证书给服务端了。对应设置server.ssl.client-auth=none,再注释掉下面不需要的这行,

SSLContext sslContext = SSLContextBuilder
    .create()
    // 注释这行
    /*.loadKeyMaterial(new ClassPathResource(keyStore).getURL(), trustStorePassword.toCharArray(), keyStorePassword.toCharArray())*/
    ...

ssl单向认证
可以看出来,单向认证只是客户端校验服务端证书是否合法。
ssl双向认证
双向认证还需要服务端校验客户端证书。
以上插图来自这里

5.2、jdk版本兼容问题

真实场景中使用,需要多测试不同jdk版本下的keytool生成的秘钥证书,在不同版本jvm下的表现。比如jdk11下生成的jks,在jdk8中运行可能报错“Invalid keytool format”。

5.3、CA方式单向认证在postman的测试

这种情况下,因为服务端不再需要验证客户端证书,所以postman作为客户端也不需要导入Settings-Certificates-Client Certificates。此时已经可以直接访问https接口了,只是postman里会标记提示“Unable to get local issuer certificate”,如下,
在这里插入图片描述
同样,直接在浏览器敲“https://127.0.0.1:28288/interact/reply/hello?name=browser”,也会先提示“NET::ERR_CERT_AUTHORITY_INVALID”,代表服务端的证书可能是不安全的,需要手动继续访问。
那如果强行开启postman客户端的ssl认证,首先需要如下操作,
在这里插入图片描述

此时访问,则报错SSL Error: Unable to get local issuer certificate
在这里插入图片描述

所以还需要在Settings-Certificates-CA Certificates导入ca.pem,此时再访问,可能报错SSL Error: Hostname/IP does not match certificate's altnames
在这里插入图片描述
这是因为生成节点证书时,通过-ext "san=dns:localhost"指定了只允许通过serverName别名localhost来访问,127.0.0.1不能访问,所以要么请求ip改为localhost,要么在生成证书时,同时指定ip,就可以通过ip访问:-ext "san=dns:localhost,ip:127.0.0.1"

5.4、多级CA

上面是ca->peer,只有一级。多级root->ca->peer可以这样,

5.4.1、root

## [root] generate root.jks
keytool -genkeypair -alias root -dname "cn=root" -validity 10000 -keyalg RSA -keysize 2048 -ext bc:c -keystore root.jks -keypass passwdroot -storepass passwdroot

## [root] self sign and generate root.pem
keytool -exportcert -rfc -keystore root.jks -alias root -storepass passwdroot > root.pem

5.4.2、ca

## [ca] generate ca.jks
keytool -genkeypair -alias ca -dname "cn=ca" -validity 10000 -keyalg RSA -keysize 2048 -ext bc:c -keystore ca.jks -keypass passwdca -storepass passwdca
## [ca] generate ca.csr
keytool -keystore ca.jks -storepass passwdca -certreq -alias ca -file ca.csr
## [root] root sign and generate ca.pem (root -> ca)
keytool -keystore root.jks -storepass passwdroot -gencert -alias root -ext bc=0 -ext san=dns:ca -rfc -infile ca.csr > ca.pem

## [ca] import root.pem and ca.pem into ca.jks
keytool -keystore ca.jks -storepass passwdca -importcert -trustcacerts -noprompt -alias root -file root.pem
keytool -keystore ca.jks -storepass passwdca -importcert -alias ca -file ca.pem

5.4.3、peer1

## [peer1] generate peer1.jks
keytool -genkeypair -alias peer1 -dname cn=peer1 -validity 10000 -keyalg RSA -keysize 2048 -keystore peer1.jks -keypass passwd1 -storepass passwd1
## [peer1] generate peer1.csr
keytool -keystore peer1.jks -storepass passwd1 -certreq -alias peer1 -file peer1.csr
## [ca] ca sign and generate peer1.pem (root -> ca -> peer1)
keytool -keystore ca.jks -storepass passwdca -gencert -alias ca -ext ku:c=dig,keyEnc -ext "san=dns:localhost,ip:127.0.0.1" -ext eku=sa,ca -rfc -infile peer1.csr > peer1.pem

## [peer1] import root.pem and ca.pem and peer1.pem into peer1.jks
keytool -keystore peer1.jks -storepass passwd1 -importcert -trustcacerts -noprompt -alias root -file root.pem
keytool -keystore peer1.jks -storepass passwd1 -importcert -alias ca -file ca.pem
keytool -keystore peer1.jks -storepass passwd1 -importcert -alias peer1 -file peer1.pem

## [peer1] import root.pem and ca.pem and peer1.pem into peer1Trust.jks
keytool -keystore peer1Trust.jks -storepass passwd1 -importcert -trustcacerts -noprompt -alias root -file root.pem
keytool -keystore peer1Trust.jks -storepass passwd1 -importcert -alias ca -file ca.pem
keytool -keystore peer1Trust.jks -storepass passwd1 -importcert -alias peer1 -file peer1.pem

## [peer1] peer1.jks -> peer1.p12
keytool -importkeystore -srckeystore peer1.jks -destkeystore peer1.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass passwd1 -deststorepass passwd1 -srckeypass passwd1 -destkeypass passwd1 -srcalias peer1 -destalias peer1 -noprompt

5.4.4、peer2

## [peer2] generate peer2.jks
keytool -genkeypair -alias peer2 -dname cn=peer2 -validity 10000 -keyalg RSA -keysize 2048 -keystore peer2.jks -keypass passwd2 -storepass passwd2
## [peer2] generate peer2.csr
keytool -keystore peer2.jks -storepass passwd2 -certreq -alias peer2 -file peer2.csr
## [ca] ca sign and generate peer2.pem (root -> ca -> peer2)
keytool -keystore ca.jks -storepass passwdca -gencert -alias ca -ext ku:c=dig,keyEnc -ext "san=dns:localhost,ip:127.0.0.1" -ext eku=sa,ca -rfc -infile peer2.csr > peer2.pem

## [peer2] import root.pem and ca.pem and peer2.pem into peer2.jks
keytool -keystore peer2.jks -storepass passwd2 -importcert -trustcacerts -noprompt -alias root -file root.pem
keytool -keystore peer2.jks -storepass passwd2 -importcert -alias ca -file ca.pem
keytool -keystore peer2.jks -storepass passwd2 -importcert -alias peer2 -file peer2.pem

## [peer2] import root.pem and ca.pem and peer2.pem into peer2Trust.jks
keytool -keystore peer2Trust.jks -storepass passwd2 -importcert -trustcacerts -noprompt -alias root -file root.pem
keytool -keystore peer2Trust.jks -storepass passwd2 -importcert -alias ca -file ca.pem
keytool -keystore peer2Trust.jks -storepass passwd2 -importcert -alias peer2 -file peer2.pem

## [peer2] peer2.jks -> peer2.p12
keytool -importkeystore -srckeystore peer2.jks -destkeystore peer2.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass passwd2 -deststorepass passwd2 -srckeypass passwd2 -destkeypass passwd2 -srcalias peer2 -destalias peer2 -noprompt

猜你喜欢

转载自blog.csdn.net/songzehao/article/details/127780689