本文通过构建自签名证书,实现浏览器和代码发送https请求的单向认证,双向认证和代码层绕过证书校验。
keytool -genkey -v -alias clientKey -keyalg RSA -storetype PKCS12 -keystore client.key.p12
得到文件client.key.p12,密码为:123456,alias为clientKey
keytool -genkey -v -alias serverKey -keyalg RSA -keystore server.keystore -validity 365
装换格式
keytool -importkeystore -srckeystore server.keystore -destkeystore server.keystore -deststoretype pkcs12
得到文件:server.keystore,密码为:123456,别名serverKey
tomcat的webapps目录下创建ssl目录,并将index.jsp文件放到该目录下
<%@ page language="java" contentType="text/html;charset=utf-8" pageEncoding="UTF-8" %>
<%@ page import="java.util.Enumeration" %>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<title>testtitle>
head>
<body>
<p>request属性信息p>
<pre>
<%
for(Enumeration en = request.getAttributeNames();en.hasMoreElements();){
String name = (String) en.nextElement();
out.println(name);
out.println(" = " + request.getAttribute(name));
out.println();
}
%>
pre>
body>
html>
ssl目录下创建WEB-INF目录,WEB-INF目录下创建web.xml文件
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>ssldisplay-name>
<welcome-file-list>
<welcome-file>index.jspwelcome-file>
welcome-file-list>
web-app>
tomcat的server.xml配置文件如下:
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true" scheme="https" secure="true" sslProtocol="TLS"
keystoreFile="conf/server.keystore"
keystorePass="123456"
/>
将上面生成的server.keystore文件放到server…xml的同级目录,启动服务
使用浏览器访问:https://localhost:8443/ssl/,会出现提醒,点击继续访问就可看到返回的内容
客户端代码要验证服务器的证书,就是在客户端的密钥库中添加服务的的证书。我们可以通过浏览器获得服务器返回的证书,再将其导入到本地的密钥库中。
按照以上步骤得到文件server.cer,将该证书导入密钥库client.key.p12
keytool -importcert -trustcacerts -alias serverKey -file server.cer -keystore client.key.p12
可以通过查看命令
keytool -importcert -trustcacerts -alias serverKey -file server.cer -keystore client.key.p12
看到client.key.p12中有两个条目,一个类型为:PrivateKeyEntry,一个类型为:trustedCertEntry
代码测试:
获取SSLSocketFactory
public class HttpConfig {
public static final String PROTOCOL = "TLS";
/**
* 获取keystore
*
* @param keystorePath keystore路径
* @param password 密码
* @return 密钥库
* @throws Exception Exception
*/
private static KeyStore getKeyStore(String keystorePath, String password) throws Exception {
KeyStore keystore = KeyStore.getInstance("PKCS12");
// KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
try (FileInputStream in = new FileInputStream(keystorePath);) {
keystore.load(in, password.toCharArray());
return keystore;
}
}
/**
* 获取 SSLSocketFactory
* @param keyManagerFactory 密钥库工厂
* @param trustFactory 信任库工厂
* @return SSLSocketFactory
* @throws Exception Exception
*/
public static SSLSocketFactory getSSLSocketFactory(KeyManagerFactory keyManagerFactory,
TrustManagerFactory trustFactory) throws Exception {
// 实例化SSL上下文
SSLContext context = SSLContext.getInstance(PROTOCOL);
KeyManager[] keyManagers = Optional.ofNullable(keyManagerFactory)
.map(KeyManagerFactory::getKeyManagers).orElse(null);
TrustManager[] trustManagers = Optional.ofNullable(trustFactory)
.map(TrustManagerFactory::getTrustManagers).orElse(null);
context.init(keyManagers, trustManagers, new SecureRandom());
return context.getSocketFactory();
}
public static TrustManagerFactory getTrustManagersFactory(String trustStorePath, String password) throws Exception {
// 实例化信任库
TrustManagerFactory trustFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
KeyStore trustStore = getKeyStore(trustStorePath, password);
// 初始化信任库
trustFactory.init(trustStore);
return trustFactory;
}
public static KeyManagerFactory getKeyManagerFactory(String keystorePath, String password) throws Exception {
KeyManagerFactory factory = KeyManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
// 获取密钥库
KeyStore keyStore = getKeyStore(keystorePath, password);
// 初始化密钥工厂
factory.init(keyStore, password.toCharArray());
return factory;
}
}
测试
public class HttpsRequestTest {
private String password = "123456";
private String trustStorePath = "D:\\ssl\\client.key.p12";
//笔者这里用localhost会报一个签名不匹配问题
private String httpUrl = "https://www.xuecheng.com:8443/ssl/";
@Test
public void oneWayAuthentication() throws Exception {
URL url = new URL(httpUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
// 打开输入输出流
conn.setDoInput(true);
//域名校验
conn.setHostnameVerifier((k, t) -> true);
// 单向认证
TrustManagerFactory trustManagersFactory =
HttpConfig.getTrustManagersFactory(trustStorePath, password);
SSLSocketFactory sslSocketFactory = HttpConfig.getSSLSocketFactory(null, trustManagersFactory);
conn.setSSLSocketFactory(sslSocketFactory);
conn.connect();
receiveData(conn);
conn.disconnect();
}
private void receiveData(HttpsURLConnection conn) throws IOException {
int length = conn.getContentLength();
byte[] data = null;
if (length != -1) {
DataInputStream input = new DataInputStream(conn.getInputStream());
data = new byte[length];
input.readFully(data);
input.close();
System.out.println(new String(data));
}
}
}
双向认证就是让服务器信任客户端证书的证书。就需要将客户端的证书导入到服务器中。
因不能直接将PKCS12格式的证书库导入服务器证书库,需要将客户端证书导出为一个单独的CER文件
keytool -export -alias clientKey -keystore client.key.p12 -storetype PKCS12 -file client.cer -rfc
这样就得到一个client.cer证书,该证书发给服务器的伙伴,服务器端的伙伴需要将此证书导入到服务器的密钥库中
通过命令:
keytool -list -v -keystore server.keystore
同样可以看到服务器端的密钥库中有两个条目:一个类型为trustedCertEntry,一个为 PrivateKeyEntry
配置tomcat的server.xml
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true" scheme="https" secure="true" sslProtocol="TLS"
keystoreFile="conf/server.keystore"
keystorePass="123456"
keystoreType="PKCS12"
truststoreFile="conf/server.keystore"
truststorePass="123456"
truststoreType="PKCS12"
clientAuth="true"
/>
服务器启动后,访问出现以下页面
双击client.key.p12文件进行证书安装
安装成功后就可以继续访问
public class HttpsRequestTest {
private String password = "123456";
private String trustStorePath = "D:\\ssl\\client.key.p12";
private String keyStorePath = "D:\\ssl\\client.key.p12";
// 服务器服务地址(注意:笔者这里用localhost会报一个签名不匹配问题)
private String httpUrl = "https://www.xuecheng.com:8443/ssl/";
@Test
public void twoWayAuthentication() throws Exception {
URL url = new URL(httpUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
// 打开输入输出流
conn.setDoInput(true);
//域名校验
conn.setHostnameVerifier((k, t) -> true);
// 双向认证
TrustManagerFactory trustManagersFactory =
HttpConfig.getTrustManagersFactory(trustStorePath, password);
KeyManagerFactory keyManagerFactory = HttpConfig
.getKeyManagerFactory(keyStorePath, password);
SSLSocketFactory sslSocketFactory = HttpConfig
.getSSLSocketFactory(keyManagerFactory, trustManagersFactory);
conn.setSSLSocketFactory(sslSocketFactory);
conn.connect();
receiveData(conn);
conn.disconnect();
}
private void receiveData(HttpsURLConnection conn) throws IOException {
int length = conn.getContentLength();
byte[] data = null;
if (length != -1) {
DataInputStream input = new DataInputStream(conn.getInputStream());
data = new byte[length];
input.readFully(data);
input.close();
System.out.println(new String(data));
}
}
}
本篇代码访问方面虽然没有集成主流框架httpclient,但弄清楚了认证的流程和原理后,可以在使用httpclient发送请求时做到单向认证,双向认证和绕过证书认证。