一 简介:
HTTPS(Hyper Text Transfer Protocol Secure),是一种基于SSL/TLS的HTTP,所有的HTTP数据都是在SSL/TLS协议封装之上进行传输的。HTTPS协议是在HTTP协议的基础上,添加了SSL/TLS握手以及数据加密传输,也属于应用层协议。所以,研究HTTPS协议原理,最终其实就是研究SSL/TLS协议。
SSL/TLS协议作用
不使用SSL/TLS的HTTP通信,就是不加密的通信,所有的信息明文传播,带来了三大风险:
窃听风险:第三方可以获知通信内容。
篡改风险:第三方可以修改通知内容。
冒充风险:第三方可以冒充他人身份参与通信。
SSL/TLS协议是为了解决这三大风险而设计的,希望达到:
所有信息都是加密传输,第三方无法窃听。
具有校验机制,一旦被篡改,通信双方都会立刻发现。
配备身份证书,防止身份被冒充。
基本的运行过程
SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密,这就是非对称加密。但是这里需要了解两个问题的解决方案。
如何保证公钥不被篡改?
解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。
公钥加密计算量太大,如何减少耗用的时间?
解决方法:每一次对话(session),客户端和服务器端都生成一个“对话密钥”(session key),用它来加密信息。由于“对话密钥”是对称加密,所以运算速度非常快,而服务器公钥只用于加密“对话密钥”本身,这样就减少了加密运算的消耗时间。
因此,SSL/TLS协议的基本过程是这样的:
客户端向服务器端索要并验证公钥。
双方协商生成“对话密钥”。
双方采用“对话密钥”进行加密通信。
上面过程的前两布,又称为“握手阶段”。
代入场景
假设现在 A 要与远端的 B 建立安全的连接进行通信。
直接使用对称加密通信,那么密钥无法安全的送给 B 。
直接使用非对称加密,B 使用 A 的公钥加密,A 使用私钥解密。但是因为B无法确保拿到的公钥就是A的公钥,因此也不能防止中间人攻击。
CA
为了解决上述问题,引入了一个第三方,也就是上面所说的 CA(Certificate Authority)。
CA 用自己的私钥签发数字证书,数字证书中包含A的公钥。然后 B 可以用 CA 的根证书中的公钥来解密 CA 签发的证书,从而拿到合法的公钥。那么又引入了一个问题,如何保证 CA 的公钥是合法的呢。答案就是现代主流的浏览器会内置 CA 的证书。
但是CA是收费的,个人或者对安全要求不是很高的可以使用自签名证书,12306就是自签名证书;
中间证书
当然,现在大多数CA不直接签署服务器证书,而是签署中间CA,然后用中间CA来签署服务器证书。这样根证书可以离线存储来确保安全,即使中间证书出了问题,可以用根证书重新签署中间证书。
二 总共四次握手:
2.1:client ->service 请求服务器
客户端请求服务端,以明文的方式请求,发送一下信息
• 支持的最高TSL协议版本version,从低到高依次 SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2,当前基本不再使用低于 TLSv1 的版本;
• 客户端支持的加密套件 cipher suites 列表, 每个加密套件对应前面 TLS 原理中的四个功能的组合:认证算法 Au (身份验证)、密钥交换算法 KeyExchange(密钥协商)、对称加密算法 Enc (信息加密)和信息摘要 Mac(完整性校验);
• 支持的压缩算法 compression methods 列表,用于后续的信息压缩传输;
• 随机数 random_C,用于后续的密钥的生成;
• 扩展字段 extensions,支持协议与算法的相关参数以及其它辅助信息等,常见的 SNI 就属于扩展字段,后续单独讨论该字段作用
2.2 service->client
• server_hello, 服务端返回协商的信息结果,包括选择使用的协议版本 version,选择的加密套件 cipher suite,选择的压缩算法 compression method、随机数 random_S 等,其中随机数用于后续的密钥协商;
• server_certificates, 服务器端配置对应的证书链,用于身份验证与密钥交换;
• server_hello_done,通知客户端 server_hello 信息发送结束;
2.3 client->service
客户端首先要对返回的证书进行验证,证书包含以下信息:申请者公钥、申请者的组织信息和个人信息、签发机构 CA的信息、有效时间、证书序列号等信息的明文,同时包含一个签名;签名的产生算法:首先,使用散列函数计算公开的明文信息的信息摘要,然后,采用 CA的私钥对信息摘要进行加密,密文即签名;
读取证书中的相关的明文信息,采用相同的散列函数计算得到信息摘要,然后,利用对应 CA的公钥解密签名数据,对比证书的信息摘要,如果一致,则可以确认证书的合法性,即公钥合法;
比如:使用HttpsURLConnection去请求百度,百度返回的是AC证书是被信任的,所以能成功;但是你去请求12306就是失败的,因为他是自签名的证书,android不信任;必须要加入到信任锚点中才可以;下面会讲到;
在携带以下信息访问服务器
(a) client_key_exchange,合法性验证通过之后,客户端计算产生随机数字 Pre-master,并用证书公钥加密,发送给服务器;
(b) 此时客户端已经获取全部的计算协商密钥需要的信息:两个明文随机数 random_C 和 random_S 与自己计算产生的 Pre-master,计算得到协商密钥;
enc_key=Fuc(random_C, random_S, Pre-Master)
(c) change_cipher_spec,客户端通知服务器后续的通信都采用协商的通信密钥和加密算法进行加密通信;
(d) encrypted_handshake_message,结合之前所有通信参数的 hash 值与其它相关信息生成一段数据,采用协商密钥 session secret 与算法进行加密,然后发送给服务器用于数据与握手验证;
2.4 service->client
(a) 服务器用私钥解密加密的 Pre-master 数据,基于之前交换的两个明文随机数 random_C 和 random_S,计算得到协商密钥:enc_key=Fuc(random_C, random_S, Pre-Master);
(b) 计算之前所有接收信息的 hash 值,然后解密客户端发送的 encrypted_handshake_message,验证数据和密钥正确性;
(c) change_cipher_spec, 验证通过之后,服务器同样发送 change_cipher_spec 以告知客户端后续的通信都采用协商的密钥与算法进行加密通信;
(d) encrypted_handshake_message, 服务器也结合所有当前的通信参数信息生成一段数据并采用协商密钥 session secret 与算法加密并发送到客户端;
三 配置代码
当然还有就是对证书不做验证,直接信任处理,这种方式很不安全,采用中间人功能模式就可以获取信息就该信息,实际开发中这种是不会采用的;具体不做证书验证的方法自行百度,很多的;这里不做说明,下面重要将android的httpsURlConnetion怎样使用https协议:
这里使用12306的自签名证书:可以在12306上去下载
public void httpsbaidu() {
HttpsURLConnection httpsURLConnection = null;
BufferedReader reader = null;
try {
SSLContext sc = createTrustManager();
if (sc == null) {
Log.e("TAG", "tmf create failed!");
return ;
}
URL url = new URL("https://kyfw.12306.cn/otn/");
httpsURLConnection = (HttpsURLConnection) url.openConnection();
httpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
httpsURLConnection.setConnectTimeout(5000);
httpsURLConnection.setDoInput(true);
httpsURLConnection.setUseCaches(false);
httpsURLConnection.connect();
reader = new BufferedReader(new InputStreamReader(httpsURLConnection.getInputStream()));
StringBuilder sBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sBuilder.append(line);
}
Log.e("TAG", "Wiki content=" + sBuilder.toString());
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (httpsURLConnection != null) {
httpsURLConnection.disconnect();
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private SSLContext createTrustManager() {
BufferedInputStream cerInputStream = null;
try {
// 获取客户端存放的服务器公钥证书
cerInputStream = new BufferedInputStream(getAssets().open("srca.cer"));
// 根据公钥证书生成Certificate对象
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Certificate ca = cf.generateCertificate(cerInputStream);
// 创建 Keystore 包含我们的证书
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null,null);
keyStore.setCertificateEntry("ca", ca);
// 使用包含指定CA证书的keystore生成TrustManager[]数组
//创建一个 TrustManager 仅把 Keystore 中的证书 作为信任的锚点
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
TrustManager[] trustManagers = tmf.getTrustManagers();
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustManagers, null);
return sc;
} catch (CertificateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
} finally {
if (cerInputStream != null) {
try {
cerInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
上面是android自身api的配置https的,下面代码上OKhttp的配置代码,网上找的,但是死活不成功,不能访问。而且okhttp是默认是信任所有证书的,不对证书进行验证,默认情况是可以访问https的,但是这样很不安全;先看看代码,有懂的小伙伴留言告知我为什么不能成功
SSLContext trustManager = createTrustManager();
OkHttpClient okHttpClient=new OkHttpClient.Builder()
.sslSocketFactory(trustManager.getSocketFactory())
.build();
Request request=new Request.Builder().url("https://kyfw.12306.cn/otn/").build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e("hahha",e.toString());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.e("hahha",response.body().string());
}
});
private SSLContext createTrustManager() {
InputStream open = null;
try {
// 获取客户端存放的服务器公钥证书
// 根据公钥证书生成Certificate对象
CertificateFactory cf = CertificateFactory.getInstance("X.509");
open = getAssets().open("srca.cer");
// Certificate ca = cf.generateCertificate(cerInputStream);
// 创建 Keystore 包含我们的证书
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
String certificateAlias = Integer.toString(0);
keyStore.setCertificateEntry(certificateAlias,cf.generateCertificate(open) );//拷贝好的证书
// 使用包含指定CA证书的keystore生成TrustManager[]数组
//创建一个 TrustManager 仅把 Keystore 中的证书 作为信任的锚点
TrustManagerFactory tmf =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, tmf.getTrustManagers(), null);
return sc;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (open != null) {
try {
open.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
接下来介绍,如何去生成证书以及在tomcat服务器下使用自签名证书部署服务。如果大家没这方面需要可以简单了解下。
四、tomcat下使用自签名证书部署服务
下面这些内容直接是从别人博客复制过来的,便于以后自己查看,感谢这位作者;
首先自行下载个tomcat的压缩包。
既然我们要支持https,那么肯定需要个证书,如何生成证书呢?使用keytool非常简单。
zhydeMacBook-Pro:temp zhy$ keytool -genkey -alias zhy_server -keyalg RSA -keystore zhy_server.jks -validity 3600 -storepass 123456
您的名字与姓氏是什么?
[Unknown]: zhang
您的组织单位名称是什么?
[Unknown]: zhang
您的组织名称是什么?
[Unknown]: zhang
您所在的城市或区域名称是什么?
[Unknown]: xian
您所在的省/市/自治区名称是什么?
[Unknown]: shanxi
该单位的双字母国家/地区代码是什么?
[Unknown]: cn
CN=zhang, OU=zhang, O=zhang, L=xian, ST=shanxi, C=cn是否正确?
[否]: y
输入 <zhy_server> 的密钥口令
(如果和密钥库口令相同, 按回车):
使用以上命令即可生成一个证书请求文件zhy_server.jks
,注意密钥库口令为:123456
.
接下来利用zhy_server.jks
来签发证书:
zhydeMacBook-Pro:temp zhy$ keytool -export -alias zhy_server
-file zhy_server.cer
-keystore zhy_server.jks
-storepass 123456
即可生成包含公钥的证书zhy_server.cer
。
找到tomcat/conf/sever.xml
文件,并以文本形式打开。
在Service标签中,加入:
<Connector SSLEnabled="true" acceptCount="100" clientAuth="false"
disableUploadTimeout="true" enableLookups="true" keystoreFile="" keystorePass="123456" maxSpareThreads="75"
maxThreads="200" minSpareThreads="5" port="8443"
protocol="org.apache.coyote.http11.Http11NioProtocol" scheme="https"
secure="true" sslProtocol="TLS"
/>
注意keystoreFile的值为我们刚才生成的jks文件的路径:/Users/zhy/
(填写你的路径).keystorePass值为密钥库密码:
temp/zhy_server.jks123456
。
然后启动即可,对于命令行启动,依赖环境变量JAVA_HOME;如果在MyEclispe等IDE下启动就比较随意了。
启动成功以后,打开浏览器输入url:https://localhost:8443/
即可看到证书不可信任的警告了。选择打死也要进入,即可进入tomcat默认的主页:
如果你在此tomcat中部署了项目,即可按照如下url方式访问: https://192.168.1.103:8443/项目名/path
,没有部署也没关系,直接拿默认的主页进行测试了,拿它的html字符串。
对于访问,还需要说么,我们刚才已经生成了zhy_server.cer
证书。你可以选择copy到assets,或者通过命令拿到内部包含的字符串。我们这里选择copy。
依然选择在Application中设置信任证书:
public class MyApplication extends Application
{
private String CER_12306 = "省略...";
@Override
public void onCreate()
{
super.onCreate();
try
{
OkHttpClientManager.getInstance()
.setCertificates(
new Buffer()
.writeUtf8(CER_12306).inputStream(),
getAssets().open("zhy_server.cer")
);
} catch (IOException e)
{
e.printStackTrace();
}
}
}
ok,这样就能正常访问你部署的https项目中的服务了,没有部署项目的尝试拿https://服务端ip:8443/
测试即可。
注意:不要使用localhost,真机测试保证手机和服务器在同一局域网段内。
ok,到此我们介绍完了如果搭建https服务和如何访问,基本上可以应付极大部分的需求了。当然还是极少数的应用需要双向证书验证,比如银行、金融类app,我们一起来了解下。
五、双向证书验证
首先对于双向证书验证,也就是说,客户端也会有个“kjs文件”,服务器那边会同时有个“cer文件”与之对应。
我们已经生成了zhy_server.kjs
和zhy_server.cer
文件。
接下来按照生成证书的方式,再生成一对这样的文件,我们命名为:zhy_client.kjs
,zhy_client.cer
.
首先我们配置服务端:
服务端的配置比较简单,依然是刚才的Connector标签,不过需要添加些属性。
<Connector 其他属性与前面一致
clientAuth="true"
truststoreFile="/Users/zhy/temp/zhy_client.cer"
/>
将clientAuth
设置为true,并且多添加一个属性truststoreFile,理论上值为我们的cer文件。这么加入以后,尝试启动服务器,会发生错误:Invalid keystore format
。说keystore的格式不合法。
我们需要对zhy_client.cer执行以下步骤,将证书添加到kjs文件中。
keytool -import -alias zhy_client
-file zhy_client.cer -keystore zhy_client_for_sever.jks
接下里修改server.xml为:
<Connector 其他属性与前面一致
clientAuth="true"
truststoreFile="/Users/zhy/temp/zhy_client_for_sever.jks"
/>
此时启动即可。
此时再拿浏览器已经无法访问到我们的服务了,会显示基于证书的身份验证失败
。
我们将目标来到客户端,即我们的Android端,我们的Android端,如何设置kjs文件呢。
目前我们app端依靠的应该是zhy_client.kjs
。
ok,大家还记得,我们在支持https的时候调用了这么俩行代码:
sslContext.init(null, trustManagerFactory.getTrustManagers(),
new SecureRandom());
mOkHttpClient.setSslSocketFactory(sslContext.getSocketFactory());
- 1
- 2
- 3
注意sslContext.init的第一个参数我们传入的是null,第一个参数的类型实际上是KeyManager[] km
,主要就用于管理我们客户端的key。
于是代码可以这么写:
public void setCertificates(InputStream... certificates)
{
try
{
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
int index = 0;
for (InputStream certificate : certificates)
{
String certificateAlias = Integer.toString(index++);
keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));
try
{
if (certificate != null)
certificate.close();
} catch (IOException e)
{
}
}
SSLContext sslContext = SSLContext.getInstance("TLS");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.
getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
//初始化keystore
KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
clientKeyStore.load(mContext.getAssets().open("zhy_client.jks"), "123456".toCharArray());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(clientKeyStore, "123456".toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
mOkHttpClient.setSslSocketFactory(sslContext.getSocketFactory());
} catch (Exception e)
{
e.printStackTrace();
}
}
核心代码其实就是:
//初始化keystore
KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
clientKeyStore.load(mContext.getAssets().open("zhy_client.jks"), "123456".toCharArray());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(clientKeyStore, "123456".toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
然而此时启动会报错:java.io.IOException: Wrong version of key store.
为什么呢?
因为:Java平台默认识别jks格式的证书文件,但是android平台只识别bks格式的证书文件。
这么就纠结了,我们需要将我们的jks文件转化为bks文件,怎么转化呢?
这里的方式可能比较多,大家可以百度,我推荐一种方式:
–
去Portecle下载Download portecle-1.9.zip (3.4 MB)。
解压后,里面包含bcprov.jar
文件,使用jave -jar bcprov.jar即可打开GUI界面。
按照上图即可将zhy_client.jks
转化为zhy_client.bks
。
然后将zhy_client.bks
拷贝到assets目录下,修改代码为:
//初始化keystore
KeyStore clientKeyStore = KeyStore.getInstance("BKS");
clientKeyStore.load(mContext.getAssets().open("zhy_client.bks"), "123456".toCharArray());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(clientKeyStore, "123456".toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
再次运行即可。然后就成功的做到了双向的验证,关于双向这块大家了解下即可。