SSL/TLS 证书是用于用户浏览器和网站服务器之间的数据传输加密,实现互联网传输安全保护,大多数情况下指的是服务器证书。
服务器证书是用于向浏览器客户端验证服务器,这种是属于单向认证的SSL证书。但是,如果服务器需要对客户端进行身份验证,
该怎么办?这就需要双向认证证书。
为什么需要另一种认证方式的证书?因为当同时使用两种认证方式的证书时,有助于双方(即客户端和服务器端)之间的相互认证。
另外,与标准SSL证书不同的是,双向认证的SSL证书实际上被称作为个人认证证书(PAC)。
双向认证流程图如下:
客户端发起建立HTTPS连接请求,将SSL协议版本的信息发送给服务端;
服务器端将本机的公钥证书(server.crt)发送给客户端;
客户端读取公钥证书(server.crt),取出了服务端公钥;
客户端将客户端公钥证书(client.crt)发送给服务器端;
服务器端使用根证书(root.crt)解密客户端公钥证书,拿到客户端公钥;
客户端发送自己支持的加密方案给服务器端;
服务器端根据自己和客户端的能力,选择一个双方都能接受的加密方案,使用客户端的公钥加密后发送给客户端;
客户端使用自己的私钥解密加密方案,生成一个随机数R,使用服务器公钥加密后传给服务器端;
服务端用自己的私钥去解密这个密文,得到了密钥R
服务端和客户端在后续通讯过程中就使用这个密钥R进行通信了。
本篇直接通过代码演示如何在Java实现证书的双向认证。
HttpClientProperties类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 @Component @ConfigurationProperties(prefix = "com.xncoding.https.httpclient") public class HttpClientProperties { private boolean enabled = true ; private boolean clientCert = false ; private String keystoreType = "PKCS12" ; private String caRootCertKeyStore; private String caRootCertPassword; private String clientCertKeyStore; private String clientCertPassword; private int connectTimeout = 20000 ; private int requestTimeout = 20000 ; private int socketTimeout = 30000 ; private int defaultMaxPerRoute = 100 ; private int maxTotalConnections = 300 ; private int defaultKeepAliveTimeMillis = 20000 ; private int closeIdleConnectionWaitTimeSecs = 30 ; }
X509Util工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 public class X509Util { private static final Logger logger = LoggerFactory.getLogger(X509Util.class); public static SSLContext initSslContext (HttpClientProperties properties) { X509TrustManager origTrustmanager = getX509TrustManager(properties); TrustManager[] wrappedTrustManagers = new TrustManager []{ new X509TrustManager () { @Override public X509Certificate[] getAcceptedIssuers() { logger.debug(">>>>>>>>>>>>>> getAcceptedIssuers 00000000000000000 start ..." ); return origTrustmanager.getAcceptedIssuers(); } @Override public void checkClientTrusted (X509Certificate[] certs, String authType) throws CertificateException { logger.debug(">>>>>>>>>>>>>> checkClientTrusted 111111111111 start ..." ); origTrustmanager.checkClientTrusted(certs, authType); } @Override public void checkServerTrusted (X509Certificate[] certs, String authType) throws CertificateException { logger.debug(">>>>>>>>>>>>>> checkServerTrusted 222222222222222 start ..." ); origTrustmanager.checkServerTrusted(certs, authType); for (X509Certificate cert : certs) { cert.checkValidity(); } if (certs.length > 1 ) { for (int i = 1 ; i < certs.length; i++) { X509Certificate ca = certs[i]; if (ca.getBasicConstraints() == -1 ) { logger.error("check CA certificate error" ); throw new CertificateException ("not a CA certificate" ); } boolean [] keyUsages = ca.getKeyUsage(); if (!keyUsages[5 ]) { logger.error("check keyusage keyCertSign error" ); throw new CertificateException ("keyusage has not value keyCertSign" ); } } } } } }; try { SSLContext sslContext = SSLContext.getInstance("TLSv1.2" ); if (properties.isClientCert()) { sslContext.init(getX509KeyManagers(properties), wrappedTrustManagers, new java .security.SecureRandom()); } else { sslContext.init(null , wrappedTrustManagers, new java .security.SecureRandom()); } return sslContext; } catch (NoSuchAlgorithmException | KeyManagementException e) { throw new RuntimeException ("init sslContext error" ); } } public static X509TrustManager getX509TrustManager (HttpClientProperties properties) { String caRootCertKeyStore = properties.getCaRootCertKeyStore(); try { PathUtils.checkRegularAndSecure(caRootCertKeyStore); } catch (SecurityException e) { throw new RuntimeException ("init trustManager, check regular and secure error" , e); } try (FileInputStream rootKeyStore = new FileInputStream (caRootCertKeyStore)) { KeyStore trustKeyStore = KeyStore.getInstance(properties.getKeystoreType()); trustKeyStore.load(rootKeyStore, properties.getCaRootCertPassword().toCharArray()); TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(trustKeyStore); TrustManager[] trustManagers = tmf.getTrustManagers(); return (X509TrustManager) trustManagers[0 ]; } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) { throw new RuntimeException ("init trustManager error" , e); } } private static KeyManager[] getX509KeyManagers(HttpClientProperties properties) { String clientCertKeyStore = properties.getClientCertKeyStore(); try { PathUtils.checkRegularAndSecure(clientCertKeyStore); } catch (SecurityException e) { throw new RuntimeException ("init KeyManager, check regular and secure error" , e); } try (FileInputStream clientKeystore = new FileInputStream (clientCertKeyStore)) { KeyStore clientKeyStore = KeyStore.getInstance(properties.getKeystoreType()); clientKeyStore.load(clientKeystore, properties.getClientCertPassword().toCharArray()); KeyManagerFactory keyManagerFactory = KeyManagerFactory .getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(clientKeyStore, properties.getClientCertPassword().toCharArray()); return keyManagerFactory.getKeyManagers(); } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException e) { throw new RuntimeException ("init keyManagers error" , e); } } }
AbstractUserInfoInterceptor 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public abstract class AbstractUserInfoInterceptor implements HttpRequestInterceptor { private static final Logger _logger = LoggerFactory.getLogger(AbstractUserInfoInterceptor.class); private static final String HEADER_ACCESS_USER = "access-user" ; public void process (HttpRequest request, HttpContext context) { _logger.debug("ClientIPInterceptor start to handle..." ); request.addHeader(HEADER_ACCESS_USER, loadUserInfo()); } protected abstract String loadUserInfo () ; }
AbstractClientIPInterceptor 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public abstract class AbstractClientIPInterceptor implements HttpRequestInterceptor { private static final Logger _logger = LoggerFactory.getLogger(AbstractClientIPInterceptor.class); private static final String HEADER_X_REMOTE_USER_IP = "X-Remote-User-IP" ; public void process (HttpRequest request, HttpContext context) { _logger.debug("ClientIPInterceptor start to handle..." ); request.addHeader(HEADER_X_REMOTE_USER_IP, loadRemoteClientIp()); } protected abstract String loadRemoteClientIp () ; }
主配置类SecurityHttpClientConfig 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 @Configuration @ConditionalOnProperty(value = "com.xncoding.https.httpclient.enabled", havingValue = "true") @EnableScheduling @EnableConfigurationProperties({HttpClientProperties.class}) public class SecurityHttpClientConfig { private static final Logger logger = LoggerFactory.getLogger(SecurityHttpClientConfig.class); @Autowired private HttpClientProperties properties; @Autowired protected ApplicationContext context; @Bean(name = "restTemplate") public RestTemplate restTemplate (RestTemplateBuilder restTemplateBuilder) { logger.info("loadBalanced RestTemplate initialize" ); return restTemplateBuilder.build(); } @Bean @DependsOn(value = {"customRestTemplateCustomizer"}) public RestTemplateBuilder restTemplateBuilder ( MappingJackson2HttpMessageConverter jackson2HttpMessageConverter, SSLContext sslContext) { RestTemplateBuilder builder = new RestTemplateBuilder (customRestTemplateCustomizer(sslContext)); List<HttpMessageConverter<?>> messageConverters = new ArrayList <>(); StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter (StandardCharsets.UTF_8); FormHttpMessageConverter formMessageConverter = new FormHttpMessageConverter (); messageConverters.add(stringHttpMessageConverter); messageConverters.add(jackson2HttpMessageConverter); messageConverters.add(formMessageConverter); builder.messageConverters(messageConverters); return builder; } @Bean public RestTemplateCustomizer customRestTemplateCustomizer (SSLContext sslContext) { return restTemplate -> { HttpComponentsClientHttpRequestFactory rf = new HttpComponentsClientHttpRequestFactory (); rf.setHttpClient(httpClient(sslContext)); restTemplate.setRequestFactory(rf); }; } @Bean public SSLContext sslContext () { logger.info("SecurityHttpClientConfig init sslContext..." ); return X509Util.initSslContext(properties); } @Bean public PoolingHttpClientConnectionManager poolingConnectionManager (SSLContext sslContext) { SSLConnectionSocketFactory sslsf; try { HostnameVerifier hostnameVerifier = (s, sslSession) -> { try { Certificate[] certs = sslSession.getPeerCertificates(); X509Certificate x509 = (X509Certificate) certs[0 ]; } catch (SSLPeerUnverifiedException e) { logger.error("hostnameVerifier error" , e); return false ; } return true ; }; sslsf = new SSLConnectionSocketFactory (sslContext, new String []{"TLSv1.2" }, null , hostnameVerifier); } catch (Exception e) { logger.error("Pooling Connection Manager Initialisation failure" ); throw new RuntimeException ("Pooling Connection Manager Initialisation failure" , e); } Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder .<ConnectionSocketFactory>create() .register("https" , sslsf) .register("http" , new PlainConnectionSocketFactory ()) .build(); PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager (socketFactoryRegistry); poolingConnectionManager.setMaxTotal(properties.getMaxTotalConnections()); poolingConnectionManager.setDefaultMaxPerRoute(properties.getDefaultMaxPerRoute()); return poolingConnectionManager; } @Bean public ConnectionKeepAliveStrategy connectionKeepAliveStrategy () { return (response, httpContext) -> { HeaderElementIterator it = new BasicHeaderElementIterator (response.headerIterator(HTTP.CONN_KEEP_ALIVE)); while (it.hasNext()) { HeaderElement he = it.nextElement(); String param = he.getName(); String value = he.getValue(); if (value != null && param.equalsIgnoreCase("timeout" )) { return Long.parseLong(value) * 1000 ; } } return properties.getDefaultKeepAliveTimeMillis(); }; } private CloseableHttpClient httpClient (SSLContext sslContext) { RequestConfig requestConfig = RequestConfig.custom() .setConnectionRequestTimeout(properties.getRequestTimeout()) .setConnectTimeout(properties.getConnectTimeout()) .setSocketTimeout(properties.getSocketTimeout()).build(); HttpClientBuilder httpClientBuilder = HttpClients.custom() .setDefaultRequestConfig(requestConfig) .setConnectionManager(poolingConnectionManager(sslContext)) .setConnectionManagerShared(true ) .setKeepAliveStrategy(connectionKeepAliveStrategy()) .setRetryHandler(new DefaultHttpRequestRetryHandler (3 , true )); Map<String, HttpRequestInterceptor> interceptorMap = context.getBeansOfType(HttpRequestInterceptor.class); if (interceptorMap.size() > 0 ) { for (HttpRequestInterceptor interceptor : interceptorMap.values()) { httpClientBuilder.addInterceptorLast(interceptor); } } return httpClientBuilder.build(); } @Bean public Runnable idleConnectionMonitor () { return new Runnable () { @Override @Scheduled(fixedDelay = 10000) public void run () { try { PoolingHttpClientConnectionManager connectionManager = poolingConnectionManager(sslContext()); logger.trace("run IdleConnectionMonitor - Closing expired and idle connections..." ); connectionManager.closeExpiredConnections(); connectionManager .closeIdleConnections(properties.getCloseIdleConnectionWaitTimeSecs(), TimeUnit.SECONDS); } catch (Exception e) { logger.error("run IdleConnectionMonitor - Exception occurred." , e); } } }; } @Bean @ConditionalOnMissingBean(TaskScheduler.class) public TaskScheduler taskScheduler () { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler (); scheduler.setThreadNamePrefix("poolScheduler" ); scheduler.setPoolSize(10 ); return scheduler; } }
客户端使用配置 1 2 3 4 5 6 7 8 9 10 com: xncoding: https: httpclient: enabled: true ca-root-cert-key-store: /cert/root.p12 ca-root-cert-password: 333333 client-cert: true client-cert-key-store: /cert/server.p12 client-cert-password: 222222
然后在Service中直接注入RestTemplate
类即可。
1 2 @Autowired private RestTemplate restTemplate;
后面该咋调用还是咋调用,客户端使用者不感知。
Tomcat证书双向认证 上面的是Java客户端的代码,双向认证客户端和服务端都要配置自己的证书和信任证书,以下是服务端Tomcat8的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <Connector port ="9443" protocol ="org.apache.coyote.http11.Http11NioProtocol" maxThreads ="150" SSLEnabled ="true" > <UpgradeProtocol className ="org.apache.coyote.http2.Http2Protocol" /> <SSLHostConfig truststoreFile ="conf/root.p12" truststorePassword ="222222" truststoreType ="PKCS12" certificateVerification ="required" truststoreAlgorithm ="SunX509" sslProtocol ="TLS" > <Certificate certificateKeystoreFile ="conf/server.p12" certificateKeystorePassword ="222222" certificateKeystoreType ="PKCS12" type ="RSA" /> </SSLHostConfig > </Connector >
或者是下面的配置:
1 2 3 4 5 6 7 8 <Connector port ="8443" protocol ="org.apache.coyote.http11.Http11NioProtocol" SSLEnabled ="true" sslProtocol ="TLS" maxThreads ="150" acceptCount ="100" maxHttpHeaderSize ="8192" maxKeepAliveRequests ="100" scheme ="https" secure ="true" keystoreFile ="conf/server.p12" keystorePass ="333333" keystoreType ="PKCS12" clientAuth ="true" truststoreFile ="root.p12" truststorePass ="66666" truststoreType ="PKCS12" truststoreAlgorithm ="SunX509" ciphers ="TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" maxPostSize ="10240" connectionTimeout ="20000" allowTrace ="false" xpoweredBy ="false" server ="WebServer" URIEncoding ="UTF-8" sslEnabledProtocols ="TLSv1.2,TLSv1.3" />