公司一个SpringBoot系统需要HTTPS改造,要求HHTP、HTTPS单向、HTTPS双向都是可配置的,它们是由四个系统构成的,相互之间通过WebClient和RestTemplate进行请求。所以首先我们可以明确配置文件的内容:
server:
port: 8080
ssl:
enabled: true
key-store: server.jks
key-alias: localhost
key-store-password: 123456
key-store-type: JKS
trust-store-provider: SUN
trust-store-type: JKS
trust-store: server.jks
trust-store-password: 123456
client-auth: need
ssl-client:
client-file: client.jks
client-type: JKS
client-password: 123456
其中:
enabled: 是否开启https,默认为False,则是正常的HTTP
key-store-type: 服务端证书的类型 JKS / pcks12
key-store: 服务端证书文件的位置
key-store-password: 服务端证书的密码
key-alias: 服务端证书的别名
trust-store: 信任证书的文件位置,与服务端相同即可
trust-store-password:证书的密码
trust-store-provider: SUN
trust-store-type: 证书类型
client-auth: 是否需要验证客户端证书、默认值为none;need代
表开启双向认证、want尝试认证,成功与否都能建立
连接、none不进行认证
client-file: 客户端证书文件
client-type: 客户端证书类型
client-password: 客户端证书密码
而后在系统初始化的时候,根据配置文件的内容选择什么样的WebClient和RestTemplate
1. 当enabled为flase
则WebClient 为 WebClient.create()
则RestTemplate为:
int POOL_SIZE = 200;
int TIMEOUT = 10000;
RequestConfig defaultRequestConfig = RequestConfig.custom().setSocketTimeout(TIMEOUT).setConnectTimeout(TIMEOUT)
.setConnectionRequestTimeout(TIMEOUT).build();
PoolingHttpClientConnectionManager connMgr = new PoolingHttpClientConnectionManager();
connMgr.setMaxTotal(POOL_SIZE + 1);
connMgr.setDefaultMaxPerRoute(POOL_SIZE);
CloseableHttpClient httpClient =
HttpClients.custom().setConnectionManager(connMgr).setDefaultRequestConfig(defaultRequestConfig).build();
RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient));
List<HttpMessageConverter<?>> converters = new ArrayList<>();
FastJsonHttpMessageConverter fastjson = new FastJsonHttpMessageConverter();
converters.add(fastjson);
template.setMessageConverters(converters);
return template;
2. 当client-auth等于none或者是want,表示单向认证
则WebClient 为:
reactor.netty.http.client.HttpClient secure = HttpClient.create()
.secure(t -> t.sslContext(SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE)));
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(secure))
.build();
则RestTemplate为:
TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;
SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy)
.build();
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(csf).build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
return new RestTemplate(requestFactory);
3. 否则为双向认证,即client-auth=need
注意:以下代码里的常量需替换为自己获取到的值,如类型,文件路径,密码
则WebClient 为:
private static WebClient createWebClient() {
File clientFile = new File(Constant.HTTP_SSL_FILE_PATH);
if (!clientFile.exists()) {
throw new PaException("请上传客户端证书: " + Constant.HTTP_SSL_FILE_PATH);
}
InputStream is = new FileInputStream(clientFile);
KeyStore ks = KeyStore.getInstance(Constant.HTTP_SSL_FILE_TYPE);
ks.load(is, Constant.HTTP_SSL_PASSWORD.toCharArray());
log.info("管理平台为HTTPS双向验证, 证书为: " + Constant.HTTP_SSL_FILE_PATH);
return WebClient.builder().clientConnector(getClientHttpConnector(ks, Constant.HTTP_SSL_PASSWORD, ks)).build();
}
private static ClientHttpConnector getClientHttpConnector(KeyStore keyStore, String keyStorePassword, KeyStore trustStore) throws Exception {
SslContextBuilder builder = SslContextBuilder.forClient();
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());
builder.keyManager(keyManagerFactory);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
trustManagerFactory.init(trustStore);
builder.trustManager(trustManagerFactory);
SslContext sslContext = builder.build();
HttpClient httpClient = HttpClient.create().secure(t -> t.sslContext(sslContext).handlerConfigurator(handler -> {
SSLEngine engine = handler.engine();
List<SNIMatcher> matchers = new LinkedList<>();
SNIMatcher matcher = new SNIMatcher(0) {
@Override
public boolean matches(SNIServerName serverName) {
return true;
}
};
matchers.add(matcher);
SSLParameters params = new SSLParameters();
params.setSNIMatchers(matchers);
engine.setSSLParameters(params);
}));
return new ReactorClientHttpConnector(httpClient);
}
则RestTemplate为:
private static String pwd;
private static String clientNeed;
private static String clientFile;
private static String clientType;
@Value("${server.ssl-client.client-password:null}")
public void setPwd(String pwd) {
MafitRestTemplateConfig.pwd = pwd;
}
@Value("${server.ssl.client-auth:none}")
public void setClientNeed(String clientNeed) {
MafitRestTemplateConfig.clientNeed = clientNeed;
}
@Value("${server.ssl-client.client-file:null}")
public void setClientFile(String clientFile) {
MafitRestTemplateConfig.clientFile = clientFile;
}
@Value("${server.ssl-client.client-type:null}")
public void setClientType(String clientType) {
MafitRestTemplateConfig.clientType = clientType;
}
public static RestTemplate restTemplateSsl(String password) throws Exception {
FastJsonHttpMessageConverter httpMessageConverter = new FastJsonHttpMessageConverter();
HttpComponentsClientHttpRequestFactory factory = new
HttpComponentsClientHttpRequestFactory();
factory.setConnectionRequestTimeout(5 * 60 * 1000);
factory.setConnectTimeout(5 * 60 * 1000);
factory.setReadTimeout(5 * 60 * 1000);
SSLContextBuilder builder = new SSLContextBuilder();
KeyStore keyStore = KeyStore.getInstance(clientType);
File file = new File(clientFile);
if (!file.exists()) {
throw new PaException("未加载到客户端文件" + clientFile);
}
InputStream inputStream = new FileInputStream(clientFile);
keyStore.load(inputStream, password.toCharArray());
builder.loadKeyMaterial(keyStore, password.toCharArray());
builder.loadTrustMaterial(keyStore, null);
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(builder.build(), NoopHostnameVerifier.INSTANCE);
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", new PlainConnectionSocketFactory())
.register("https", socketFactory).build();
PoolingHttpClientConnectionManager phccm = new PoolingHttpClientConnectionManager(registry);
phccm.setMaxTotal(200);
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory).setConnectionManager(phccm).setConnectionManagerShared(true).build();
factory.setHttpClient(httpClient);
RestTemplate restTemplate = new RestTemplate(factory);
List<HttpMessageConverter<?>> converters = restTemplate.getMessageConverters();
ArrayList<HttpMessageConverter<?>> convertersValid = new ArrayList<>();
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof MappingJackson2HttpMessageConverter ||
converter instanceof MappingJackson2XmlHttpMessageConverter) {
continue;
}
convertersValid.add(converter);
}
convertersValid.add(httpMessageConverter);
restTemplate.setMessageConverters(convertersValid);
inputStream.close();
return restTemplate;
}
# 直接 return restTemplateSsl(pwd);即可 在创建之前,先执行判断语句,来验证参数的正确性
private static void setLoadHttpTypeInit() {
LoadHttpType.SSL_HTTP_TYPE = LoadHttpType.HTTP_TYPE_DEFAULT;
if (mgrUrl.startsWith(LoadHttpType.HTTPS_TYPE_DEFAULT)) {
LoadHttpType.SSL_HTTP_TYPE = LoadHttpType.HTTPS_TYPE_DEFAULT;
LoadHttpType.SSL_IS_NEED_CLIENT = "need".equals(clientNeed);
if (LoadHttpType.SSL_IS_NEED_CLIENT) {
if (nullMsg.equals(clientType) || nullMsg.equals(pwd) || nullMsg.equals(clientFile)) {
throw new PaException("客户端证书的信息配置缺失, 请检查server.ssl-client中的配置信息");
}
}
}
}
生成证书方法参考
步骤1:在windows下执行,其中:
Localhost: 证书的别名 localhost.jks: 生成的证书的名字,可以不修改,生成完毕之后再重命名 CN=127.0.0.1和 san=ip:127.0.0.1 : 若是本地测试,则填写127.0.0.1,若是放在服务器上运行,则填写服务器的ip,系统运行在哪台服务器,就填写哪个ip 123456:密码
keytool -genkey -alias localhost -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -keystore localhost.jks -dname CN=127.0.0.1,OU=Test,O=pkslow,L=Guangzhou,C=CN -ext san=ip:127.0.0.1 -validity 36000 -storepass 123456 -keypass 123456
步骤2:在windows下执行
keytool -export -alias localhost -file localhost.cer -keystore localhost.jks
步骤3:在windows下执行
keytool -genkey -alias client -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -keystore client.jks -dname CN=client,OU=Test,O=pkslow,L=Guangzhou,C=CN -validity 731 -storepass 123456 -keypass 123456
步骤4:在windows下执行
keytool -export -alias client -file client.cer -keystore client.jks
步骤5:将生成的证书放入服务器的目录下,在服务器中执行以下代码
keytool -import -alias client -file client.cer -keystore localhost.jks
会询问是否添加到信任库中,填写y回车即可
步骤6:在服务器中执行以下代码
keytool -import -alias localhost -file localhost.cer -keystore client.jks
步骤7:生成完毕,需要用到的文件为:localhost.jks 和 client.jks , 可以将localhost.jks重命名为server.jks
打开浏览器 -> 设置 -> 安全 -> 管理证书 ->导入 -> 浏览器 点击所有文件->选择client.jks->下一步输入密码…即可将证书导入服务器中
- 问:为什么要分在windows下执行 和 linux下执行代码?
答:若是全部在linux下执行,是可以执行成功的,但是发现了一个问题,就是从服务器中拿到client.jks证书,再导入进浏览器的时候会提示以下信息:
而代码中使用这个证书进行请求,却是正常的。不知道别人会不会出现,总之为了防止这个事情发生,故生成证书放在客户端来执行,而将证书添加到信任库中,则是在服务端执行。
- 问:为什么要在linux上执行那两个代码?
答:为了将client添加进服务器的信任库中。否则请求会出现No trusted certificate found错误
- 问:如何进行测试,知道自己的配置是否正确?
答:我们要求各个系统的证书是设置为一样的,因为不同系统会同时请求其它系统,若是需要的客户端证书不一致,则无法区分开具体使用哪个client文件。
执行以下Test.java代码请求mgr,测试证书是否正确
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import javax.net.ssl.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class Test {
static String url = "https://IP:端口/xxxx";
static String clickFile = "client.jks";
static String password = "123456";
static String alias = "JKS";
public static void main(String[] args) throws Exception {
File clientFile = new File(clickFile);
if (!clientFile.exists()) {
throw new Exception("请上传客户端证书");
}
InputStream is = new FileInputStream(clientFile);
KeyStore ks = KeyStore.getInstance(alias);
ks.load(is, password.toCharArray());
WebClient webClient = WebClient.builder().clientConnector(getClientHttpConnector(ks, password, ks)).build();
System.out.println(webClient.get().uri(
url
).retrieve().bodyToMono(String.class).block());
RestTemplate restTemplate = getRestTemplate();
System.out.println(restTemplate.getForObject(url, String.class));
}
private static ClientHttpConnector getClientHttpConnector(KeyStore keyStore, String keyStorePassword, KeyStore trustStore) throws Exception {
SslContextBuilder builder = SslContextBuilder.forClient();
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());
builder.keyManager(keyManagerFactory);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
trustManagerFactory.init(trustStore);
builder.trustManager(trustManagerFactory);
SslContext sslContext = builder.build();
HttpClient httpClient = HttpClient.create().secure(t -> t.sslContext(sslContext).handlerConfigurator(handler -> {
SSLEngine engine = handler.engine();
List<SNIMatcher> matchers = new LinkedList<>();
SNIMatcher matcher = new SNIMatcher(0) {
@Override
public boolean matches(SNIServerName serverName) {
return true;
}
};
matchers.add(matcher);
SSLParameters params = new SSLParameters();
params.setSNIMatchers(matchers);
engine.setSSLParameters(params);
}));
return new ReactorClientHttpConnector(httpClient);
}
public static RestTemplate getRestTemplate() throws Exception {
FastJsonHttpMessageConverter httpMessageConverter = new FastJsonHttpMessageConverter();
HttpComponentsClientHttpRequestFactory factory = new
HttpComponentsClientHttpRequestFactory();
factory.setConnectionRequestTimeout(5 * 60 * 1000);
factory.setConnectTimeout(5 * 60 * 1000);
factory.setReadTimeout(5 * 60 * 1000);
SSLContextBuilder builder = new SSLContextBuilder();
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
File clientFile = new File(clickFile);
InputStream inputStream = new FileInputStream(clientFile);
keyStore.load(inputStream, password.toCharArray());
builder.loadKeyMaterial(keyStore, password.toCharArray());
builder.loadTrustMaterial(keyStore,null);
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(builder.build(), NoopHostnameVerifier.INSTANCE);
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", new PlainConnectionSocketFactory())
.register("https", socketFactory).build();
PoolingHttpClientConnectionManager phccm = new PoolingHttpClientConnectionManager(registry);
phccm.setMaxTotal(200);
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory).setConnectionManager(phccm).setConnectionManagerShared(true).build();
factory.setHttpClient(httpClient);
RestTemplate restTemplate = new RestTemplate(factory);
List<HttpMessageConverter<?>> converters = restTemplate.getMessageConverters();
ArrayList<HttpMessageConverter<?>> convertersValid = new ArrayList<>();
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof MappingJackson2HttpMessageConverter ||
converter instanceof MappingJackson2XmlHttpMessageConverter) {
continue;
}
convertersValid.add(converter);
}
convertersValid.add(httpMessageConverter);
restTemplate.setMessageConverters(convertersValid);
inputStream.close();
return restTemplate;
}
}
注: 生成证书借鉴了博客《Https双向验证与Springboot整合测试-人来人往我只认你》 编写请求部分内容借鉴了博客《Java调用Http/Https接口(7)–WebClient调用Http/Https接口》
|