服务端
Tomcat配置
<Connector port="9443" 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="222222"
keystoreType="PKCS12"
clientAuth="true"
truststoreFile="conf/root.p12"
truststorePass="333333"
truststoreType="PKCS12"
ciphers="SSL_ECDHE_RSA_WITH_AES_256_GCM_SHA384,SSL_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,SSL_ECDHE_RSA_WITH_AES_128_GCM_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.1"/>
SpringBoot配置
server:
ssl:
enabled: true
key-store: /cert/server.p12
key-store-password: 222222
key-store-type: PKCS12
trust-store: /cert/root.p12
trust-store-password: 333333
trust-store-type: PKCS12
client-auth: need
客户端
客户端RestTemplate
maven依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.sun.jersey.contribs</groupId>
<artifactId>jersey-apache-client4</artifactId>
<version>1.19.1</version>
</dependency>
<dependency>
<groupId>com.netflix.eureka</groupId>
<artifactId>eureka-client</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
HttpClientProperties类
@Component
@ConfigurationProperties(prefix = "dc.security.https.httpclient")
public class HttpClientProperties {
/**
* 是否开启服务端证书校验
*/
private boolean enabled = true;
/**
* 是否开启客户端证书发送
*/
private boolean clientCert = false;
/**
* 是否支持客户端负载均衡
*/
private boolean loadBalanced = true;
/**
* 是否支持eureka注册发现
*/
private boolean eureka = true;
/**
* CA根证书密钥库文件
*/
private String caRootCertKeyStore;
/**
* CA根证书密钥库密码
*/
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;
/**
* 连接保持活跃的时间(Keep-Alive)
*/
private int defaultKeepAliveTimeMillis = 20000;
/**
* 空闲连接的生存时间
*/
private int closeIdleConnectionWaitTimeSecs = 30;
}
X509Util工具类
public class X509Util {
public static X509TrustManager getX509TrustManager(HttpClientProperties properties) {
try (FileInputStream rootKeyStore = new FileInputStream(properties.getCaRootCertKeyStore())){
// 加载服务端信任根证书库
KeyStore trustKeyStore = KeyStore.getInstance("PKCS12");
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);
}
}
public static KeyManager[] getX509KeyManagers(HttpClientProperties properties) {
try (FileInputStream clientKeystore = new FileInputStream(properties.getClientCertKeyStore())) {
// 加载客户端证书库
KeyStore clientKeyStore = KeyStore.getInstance("PKCS12");
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);
}
}
}
ICrlService服务类
public interface ICrlService {
/**
* 实现此方法查询证书吊销列表
*
* @return 证书吊销序列号集合
*/
Set<String> getCrlList();
}
AbstractUserInfoInterceptor
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());
}
/**
* 加载用户信息
*
* @return 用户信息
*/
protected abstract String loadUserInfo();
}
AbstractClientIPInterceptor
public abstract class AbstractClientIPInterceptor implements HttpRequestInterceptor {
private static final Logger _logger = LoggerFactory.getLogger(AbstractClientIPInterceptor.class);
/**
* 内部微服务之间调用增加的IP地址头
*/
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());
}
/**
* 加载用户真实IP地址
*
* @return 用户真实IP地址
*/
protected abstract String loadRemoteClientIp();
}
SecurityEurekaClientConfig
@Configuration
@EnableConfigurationProperties({HttpClientProperties.class})
@ConditionalOnProperty(value = "dc.security.https.httpclient.eureka", havingValue = "true", matchIfMissing = true)
public class SecurityEurekaClientConfig {
private static final Logger logger = LoggerFactory.getLogger(SecurityEurekaClientConfig.class);
@Autowired
private HttpClientProperties properties;
@Bean
public DiscoveryClient.DiscoveryClientOptionalArgs discoveryClientOptionalArgs() throws Exception {
logger.info("DiscoveryClient.DiscoveryClientOptionalArgs init ...");
EurekaJerseyClientImpl.EurekaJerseyClientBuilder builder = new EurekaJerseyClientImpl.EurekaJerseyClientBuilder();
builder.withClientName("eureka-client");
builder.withCustomSSL(sslContextEureka());
builder.withMaxTotalConnections(10);
builder.withMaxConnectionsPerHost(10);
DiscoveryClient.DiscoveryClientOptionalArgs args = new DiscoveryClient.DiscoveryClientOptionalArgs();
args.setEurekaJerseyClient(builder.build());
return args;
}
private SSLContext sslContextEureka() throws Exception {
// 加载服务端信任Keystore
X509TrustManager origTrustmanager = getX509TrustManager(properties);
TrustManager[] wrappedTrustManagers = new TrustManager[]{
new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
logger.info(">>>>>>>>>>>>>> sslContextEureka getAcceptedIssuers 00000000000000000 start ...");
return origTrustmanager.getAcceptedIssuers();
}
@Override
public void checkClientTrusted(X509Certificate[] certs, String authType) throws CertificateException {
logger.info(">>>>>>>>>>>>>> sslContextEureka checkClientTrusted 111111111111 start ...");
}
@Override
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
logger.info(">>>>>>>>>>>>>> sslContextEureka checkServerTrusted 222222222222222 start ...");
}
}
};
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, wrappedTrustManagers, new java.security.SecureRandom());
return sslContext;
}
}
主配置类SecurityHttpClientConfig
@Configuration
@ConditionalOnProperty(value = "dc.security.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
private ICrlService crlService;
@Autowired
protected ApplicationContext context;
@Bean
@ConditionalOnMissingBean(RestTemplate.class)
@ConditionalOnProperty(value = "dc.security.https.httpclient.load-balanced", havingValue = "false")
public RestTemplate restTemplateSimple(RestTemplateBuilder restTemplateBuilder) {
logger.info("simple RestTemplate");
return restTemplateBuilder.build();
}
@Bean
@LoadBalanced
@ConditionalOnMissingBean(RestTemplate.class)
@ConditionalOnProperty(value = "dc.security.https.httpclient.load-balanced", havingValue = "true", matchIfMissing = true)
public RestTemplate restTemplateLoadBalanced(RestTemplateBuilder restTemplateBuilder) {
logger.info("loadBalanced RestTemplate");
return restTemplateBuilder.build();
}
@Bean
@DependsOn(value = {"customRestTemplateCustomizer"})
public RestTemplateBuilder restTemplateBuilder(MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) {
RestTemplateBuilder builder = new RestTemplateBuilder(customRestTemplateCustomizer());
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() {
return restTemplate -> {
HttpComponentsClientHttpRequestFactory rf = new HttpComponentsClientHttpRequestFactory();
rf.setHttpClient(httpClient(sslContext()));
restTemplate.setRequestFactory(rf);
};
}
private SSLContext sslContext() {
// 加载服务端信任Keystore
X509TrustManager origTrustmanager = getX509TrustManager(properties);
TrustManager[] wrappedTrustManagers = new TrustManager[]{
new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
logger.info(">>>>>>>>>>>>>> getAcceptedIssuers 00000000000000000 start ...");
return origTrustmanager.getAcceptedIssuers();
}
@Override
public void checkClientTrusted(X509Certificate[] certs, String authType) throws CertificateException {
logger.info(">>>>>>>>>>>>>> checkClientTrusted 111111111111 start ...");
origTrustmanager.checkClientTrusted(certs, authType);
}
@Override
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
logger.info(">>>>>>>>>>>>>> checkServerTrusted 222222222222222 start ...");
try {
//Original trust checking
origTrustmanager.checkServerTrusted(certs, authType);
//Check revocation with each cert
//from docs: CertificateException - if the certificate chain is not trusted by this TrustManager.
for (X509Certificate cert : certs) {
String certSerial = cert.getSerialNumber().toString(16).toUpperCase();
if (crlService.getCrlList().contains(certSerial)) {
logger.error("cert serial={} is in crl list, bad", certSerial);
throw new CertificateException();
}
}
} catch (CertificateExpiredException e) {
logger.error("cert expired error");
throw new CertificateExpiredException();
}
}
}
};
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
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");
}
}
private 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, 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;
}
private 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))
.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();
}
/**
* You can't set an idle connection timeout in the config for Apache HTTP Client.
* The reason is that there is a performance overhead in doing so.
* <properties>
* The documentation clearly states why, and gives an example of an idle connection monitor implementation you can
* copy.
* Essentially this is another thread that you run to periodically call closeIdleConnections on
* HttpClientConnectionManager
* <properties>
* http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html
*
* @return 线程
*/
@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. msg={}, e={}", e.getMessage(), e);
}
}
};
}
@Bean
@ConditionalOnMissingBean(TaskScheduler.class)
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setThreadNamePrefix("poolScheduler");
scheduler.setPoolSize(10);
return scheduler;
}
@Bean
@ConditionalOnMissingBean(ICrlService.class)
public ICrlService crlService() {
return HashSet::new;
}
}
客户端配置
dc:
security:
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 #客户端证书库密码