参考学习并不限于:
http://www.jb51.net/article/70419.htm
http://blog.youkuaiyun.com/lmj623565791/article/details/48129405
http://blog.youkuaiyun.com/dd864140130/article/details/52625666
HTTPS原理
HTTPS相当于HTTP的安全版本
因为它在HTTP加入了SSL (Secure Socket Layer),安全的基础就靠这个SSL了。
SSL位于TCP/IP和HTTP协议之间,它能够:
认证用户和服务器,确保数据发送到正确的客户机和服务器;(验证证书)
加密数据以防止数据中途被窃取;(加密)
维护数据的完整性,确保数据在传输过程中不被改变。(摘要算法)
它能够:
HTTPS(Hyper Text Transfer Protocol Secure),是一种基于SSL/TLS的HTTP,所有的HTTP数据都是在SSL/TLS协议封装之上进行传输的。HTTPS协议是在HTTP协议的基础上,添加了SSL/TLS握手以及数据加密传输,也属于应用层协议。所以,研究HTTPS协议原理,最终就是研究SSL/TLS协议。
SSL/TLS协议作用
不使用SSL/TLS的HTTP通信,就是不加密的通信,所有的信息明文传播,带来了三大风险:
1. 窃听风险:第三方可以获知通信内容。
2. 篡改风险:第三方可以修改通知内容。
3. 冒充风险:第三方可以冒充他人身份参与通信。
SSL/TLS协议是为了解决这三大风险而设计的,希望达到:
1. 所有信息都是加密传输,第三方无法窃听。
2. 具有校验机制,一旦被篡改,通信双方都会立刻发现。
3. 配备身份证书,防止身份被冒充。
基本的运行过程
SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。但是这里需要了解两个问题的解决方案。
1. 如何保证公钥不被篡改?
解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。
2. 公钥加密计算量太大,如何减少耗用的时间?
解决方法:每一次对话(session),客户端和服务器端都生成一个“对话密钥”(session key),用它来加密信息。由于“对话密钥”是对称加密,所以运算速度非常快,而服务器公钥只用于加密“对话密钥”本身,这样就减少了加密运算的消耗时间。
因此,SSL/TLS协议的基本过程是这样的:
1. 客户端向服务器端索要并验证公钥。
2. 双方协商生成“对话密钥”。
3. 双方采用“对话密钥”进行加密通信。
上面过程的前两步,又称为“握手阶段”。
握手阶段”涉及四次通信,需要注意的是,“握手阶段”的所有通信都是明文的。
客户端发出请求(ClientHello)
首先,客户端(通常是浏览器)先向服务器发出加密通信的请求,这被叫做ClientHello请求。在这一步中,客户端主要向服务器提供以下信息:
1. 支持的协议版本,比如TLS 1.0版
2. 一个客户端生成的随机数,稍后用于生成“对话密钥”。
3. 支持的加密方法,比如RSA公钥加密。
4. 支持的压缩方法。
这里需要注意的是,客户端发送的信息之中不包括服务器的域名。也就是说,理论上服务器只能包含一个网站,否则会分不清应用向客户端提供哪一个网站的数字证书。这就是为什么通常一台服务器只能有一张数字证书的原因。
服务器回应(ServerHello)
服务器收到客户端请求后,向客户端发出回应,这叫做ServerHello。服务器的回应包含以下内容:
1. 确认使用的加密通信协议版本,比如TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。
2. 一个服务器生成的随机数,稍后用于生成“对话密钥”。
3. 确认使用的加密方法,比如RSA公钥加密。
4. 服务器证书。
除了上面这些信息,如果服务器需要确认客户端的身份,就会再包含一项请求,要求客户端提供“客户端证书”。比如,金融机构往往只允许认证客户连入自己的网络,就会向正式客户提供USB密钥,里面就包含了一张客户端证书。
客户端回应
客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁发,或者证书中的域名与实际域名不一致,或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。
如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项消息。
1. 一个随机数。该随机数用服务器公钥加密,防止被窃听。
2. 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
3. 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项通常也是前面发送的所有内容的hash值,用来供服务器校验。
上面第一项随机数,是整个握手阶段出现的第三个随机数,又称“pre-master key”。有了它以后,客户端和服务器就同时有了三个随机数,接着双方就用事先商定的加密方法,各自生成本次会话所用的同一把“会话密钥”。
服务器的最后回应
服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的“会话密钥”。然后,向客户端最后发送下面信息。
1. 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
2. 服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发生的所有内容的hash值,用来供客户端校验。
握手结束
至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的HTTP协议,只不过用“会话密钥”加密内容。
服务器基于Nginx搭建HTTPS虚拟站点
之前一篇文章详细介绍了在服务器端如何生成SSL证书,并基于Nginx搭建HTTPS服务器,链接:Nginx搭建HTTPS服务器
Android实现HTTPS通信
由于各种原因吧,这里使用HttpClicent类讲解一下Android如何建立HTTPS连接。代码demo如下。
MainActivity.java
package com.example.photocrop;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.AsyncTask.Status;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends Activity {
private Button httpsButton;
private TextView conTextView;
private CreateHttpsConnTask httpsTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
httpsButton = (Button) findViewById(R.id.create_https_button);
httpsButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
runHttpsConnection(); //异步请求
}
});
conTextView = (TextView) findViewById(R.id.content_textview);
conTextView.setText("初始为空");
}
private void runHttpsConnection() {
if (httpsTask == null || httpsTask.getStatus()
== Status.FINISHED) {
httpsTask = new CreateHttpsConnTask();
httpsTask.execute();
}
}
private class CreateHttpsConnTask extends
AsyncTask<Void, Void, Void> {
private static final String HTTPS_EXAMPLE_URL = "自定义";
private StringBuffer sBuffer = new StringBuffer();
@Override
protected Void doInBackground(Void... params) {
HttpUriRequest request = new HttpPost(HTTPS_EXAMPLE_URL);
HttpClient httpClient = HttpUtils.getHttpsClient();
try {
HttpResponse httpResponse = httpClient.execute(request);
if (httpResponse != null) {
StatusLine statusLine = httpResponse.getStatusLine();
if (statusLine != null
&& statusLine.getStatusCode() == HttpStatus.SC_OK) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(
httpResponse.getEntity().getContent(),
"UTF-8"));
String line = null;
while ((line = reader.readLine()) != null) {
sBuffer.append(line);
}
} catch (Exception e) {
Log.e("https", e.getMessage());
} finally {
if (reader != null) {
reader.close();
reader = null;
}
}
}
}
} catch (Exception e) {
Log.e("https", e.getMessage());
} finally {
}
return null;
}
@Override
protected void onPostExecute(Void result) {
if (!TextUtils.isEmpty(sBuffer.toString())) {
conTextView.setText(sBuffer.toString()); //更新UI
}
}
}
}
HttpUtils.java
package com.example.photocrop;
import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;
import android.content.Context;
public class HttpUtils {
public static HttpClient getHttpsClient() {
BasicHttpParams params = new BasicHttpParams();
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params,
HTTP.DEFAULT_CONTENT_CHARSET);
HttpProtocolParams.setUseExpectContinue(params, true);
SchemeRegistry schReg = new SchemeRegistry();
schReg.register(new Scheme("http",
PlainSocketFactory.getSocketFactory(), 80));
schReg.register(new Scheme("https",
SSLSocketFactory.getSocketFactory(), 443));
ClientConnectionManager connMgr =
new ThreadSafeClientConnManager(params, schReg);
return new DefaultHttpClient(connMgr, params);
}
public static HttpClient getCustomClient() {
BasicHttpParams params = new BasicHttpParams();
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params,
HTTP.DEFAULT_CONTENT_CHARSET);
HttpProtocolParams.setUseExpectContinue(params, true);
SchemeRegistry schReg = new SchemeRegistry();
PlainSocketFactory.getSocketFactory(), 80));
schReg.register(new Scheme("https",
MySSLSocketFactory.getSocketFactory(), 443));
ClientConnectionManager connMgr =
new ThreadSafeClientConnManager(params, schReg);
return new DefaultHttpClient(connMgr, params);
}
public static HttpClient getSpecialKeyStoreClient(Context context) {
BasicHttpParams params = new BasicHttpParams();
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);
HttpProtocolParams.setUseExpectContinue(params, true);
SchemeRegistry schReg = new SchemeRegistry();
schReg.register(new Scheme("http",
PlainSocketFactory.getSocketFactory(), 80));
schReg.register(new Scheme("https",
CustomerSocketFactory.getSocketFactory(context), 443));
ClientConnectionManager connMgr =
new ThreadSafeClientConnManager(params, schReg);
return new DefaultHttpClient(connMgr, params);
}
}
activity_main.xml
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/create_https_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hello_world"
android:textSize="16sp" />
<TextView
android:id="@+id/content_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="16sp" />
</LinearLayout>
Android使用DefaultHttpClient建立HTTPS连接,关键需要加入对HTTPS的支持:
schReg.register(new Scheme(“https”, SSLSocketFactory.getSocketFactory(), 443));
加入对HTTPS的支持,就可以有效的建立HTTPS连接了,例如“https://www.google.com.hk”了,但是访问自己基于Nginx搭建的HTTPS服务器却不行,因为它使用了不被系统承认的自定义证书,会报出如下问题:No peer certificate。
使用自定义证书并忽略验证的HTTPS连接方式
解决证书不被系统承认的方法,就是跳过系统校验。要跳过系统校验,就不能再使用系统标准的SSL SocketFactory了,需要自定义一个。然后为了在这个自定义SSL SocketFactory里跳过校验,还需要自定义一个TrustManager,在其中忽略所有校验,即TrustAll。
package com.example.photocrop;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.http.conn.ssl.SSLSocketFactory;
public class MySSLSocketFactory extends SSLSocketFactory {
SSLContext sslContext = SSLContext.getInstance("TLS");
public MySSLSocketFactory(KeyStore truststore)
throws NoSuchAlgorithmException, KeyManagementException,
KeyStoreException, UnrecoverableKeyException {
super(truststore);
TrustManager tm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkServerTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
}
@Override
public void checkClientTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
}
};
sslContext.init(null, new TrustManager[] { tm }, null);
}
@Override
public Socket createSocket() throws IOException {
return sslContext.getSocketFactory().createSocket();
}
@Override
public Socket createSocket(Socket socket, String host, int port,
boolean autoClose) throws IOException, UnknownHostException {
return sslContext.getSocketFactory().createSocket(
socket, host, port, autoClose);
}
public static SSLSocketFactory getSocketFactory() {
try {
KeyStore trustStore = KeyStore.getInstance(KeyStore
.getDefaultType());
trustStore.load(null, null);
SSLSocketFactory factory = new MySSLSocketFactory(trustStore);
return factory;
} catch (Exception e) {
e.getMessage();
return null;
}
}
}
同时,需要修改DefaultHttpClient的register方法,改为自己构建的sslsocket:
public static HttpClient getCustomClient() {
BasicHttpParams params = new BasicHttpParams();
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params,
HTTP.DEFAULT_CONTENT_CHARSET);
HttpProtocolParams.setUseExpectContinue(params, true);
SchemeRegistry schReg = new SchemeRegistry();
schReg.register(new Scheme("http",
PlainSocketFactory.getSocketFactory(), 80));
schReg.register(new Scheme("https",
MySSLSocketFactory.getSocketFactory(), 443));
ClientConnectionManager connMgr =
new ThreadSafeClientConnManager(params, schReg);
return new DefaultHttpClient(connMgr, params);
}
这样就可以成功的访问自己构建的基于Nginx的HTTPS虚拟站点了。
缺陷:
不过,虽然这个方案使用了HTTPS,客户端和服务器端的通信内容得到了加密,嗅探程序无法得到传输的内容,但是无法抵挡“中间人攻击”。例如,在内网配置一个DNS,把目标服务器域名解析到本地的一个地址,然后在这个地址上使用一个中间服务器作为代理,它使用一个假的证书与客户端通讯,然后再由这个代理服务器作为客户端连接到实际的服务器,用真的证书与服务器通讯。这样所有的通讯内容都会经过这个代理,而客户端不会感知,这是由于客户端不校验服务器公钥证书导致的。
使用自定义证书建立HTTPS连接
为了防止上面方案可能导致的“中间人攻击”,我们可以下载服务器端公钥证书,然后将公钥证书编译到Android应用中,由应用自己来验证证书。
生成KeyStore
要验证自定义证书,首先要把证书编译到应用中,这需要使用keytool工具生产KeyStore文件。这里的证书就是指目标服务器的公钥,可以从web服务器配置的.crt文件或.pem文件获得。同时,你需要配置bouncycastle,我下载的是bcprov-jdk16-145.jar,至于配置大家自行google就好了。
keytool -importcert -v -trustcacerts -alias example -file www.example.com.crt -keystore example.bks -storetype BKS -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath /home/wzy/Downloads/java/jdk1.7.0_60/jre/lib/ext/bcprov-jdk16-145.jar -storepass pw123456
运行后将显示证书内容并提示你是否确认,输入Y回车即可。
生产KeyStore文件成功后,将其放在app应用的res/raw目录下即可。
使用自定义KeyStore实现连接
思路和TrushAll差不多,也是需要一个自定义的SSLSokcetFactory,不过因为还需要验证证书,因此不需要自定义TrustManager了。
package com.example.photocrop;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import org.apache.http.conn.ssl.SSLSocketFactory;
import android.content.Context;
public class CustomerSocketFactory extends SSLSocketFactory {
private static final String PASSWD = "pw123456";
public CustomerSocketFactory(KeyStore truststore)
throws NoSuchAlgorithmException, KeyManagementException,
KeyStoreException, UnrecoverableKeyException {
super(truststore);
}
public static SSLSocketFactory getSocketFactory(Context context) {
InputStream input = null;
try {
input = context.getResources().openRawResource(R.raw.example);
KeyStore trustStore = KeyStore.getInstance(KeyStore
.getDefaultType());
trustStore.load(input, PASSWD.toCharArray());
SSLSocketFactory factory =
new CustomerSocketFactory(trustStore);
return factory;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
if (input != null) {
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}
input = null;
}
}
}
}
同时,需要修改DefaultHttpClient的register方法,改为自己构建的sslsocket:
public static HttpClient getSpecialKeyStoreClient(Context context) {
BasicHttpParams params = new BasicHttpParams();
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);
HttpProtocolParams.setUseExpectContinue(params, true);
SchemeRegistry schReg = new SchemeRegistry();
schReg.register(new Scheme("http",
PlainSocketFactory.getSocketFactory(), 80));
schReg.register(new Scheme("https",
CustomerSocketFactory.getSocketFactory(context), 443));
ClientConnectionManager connMgr =
new ThreadSafeClientConnManager(params, schReg);
return new DefaultHttpClient(connMgr, params);
}
访问自签名的网站
okhttp默认情况下是支持https协议的网站的,比如
https://www.baidu.com,https://github.com/hongyangAndroid/okhttp-utils等
支持的https的网站基本都是CA机构颁发的证书,默认情况下是可以信任的
自签名就是自己通过keytool去生成一个证书,然后使用,并不是CA机构去颁发的
使用自签名证书的网站,大家在使用浏览器访问的时候,一般都是报风险警告,好在有个大名鼎鼎的网站就是这么干的,https://kyfw.12306.cn/otn/,点击进入12306的购票页面就能看到了。
如下界面:
好了,本篇博文当然不是去说如何去访问12306,而是以12306为例子来说明如何去访问自签名证书的网站。因为部分开发者app与自己服务端交互的时候可能也会遇到自签名证书的。甚至在开发安全级别很高的app时,需要用到双向证书的验证。
HTTPS在传输数据之前需要客户端(浏览器)与服务端(网站)之间进行一次握手,在握手过程中将确立双方加密传输数据的密码信息。握手过程的简单描述如下:
浏览器将自己支持的一套加密算法、HASH算法发送给网站。
网站从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。
浏览器获得网站证书之后,开始验证证书的合法性,如果证书信任,则生成一串随机数字作为通讯过程中对称加密的秘钥。然后取出证书中的公钥,将这串数字以及HASH的结果进行加密,然后发给网站。
网站接收浏览器发来的数据之后,通过私钥进行解密,然后HASH校验,如果一致,则使用浏览器发来的数字串使加密一段握手消息发给浏览器。
浏览器解密,并HASH校验,没有问题,则握手结束。接下来的传输过程将由之前浏览器生成的随机密码并利用对称加密算法进行加密。
握手过程中如果有任何错误,都会使加密连接断开,从而阻止了隐私信息的传输。
根据上面的流程,我们可以看到服务器端会有一个证书,在交互过程中客户端需要去验证证书的合法性,对于权威机构颁发的证书当然我们会直接认为合法。对于自己造的证书,那么我们就需要去校验合法性了,也就是说我们只需要让OkhttpClient去信任这个证书就可以畅通的进行通信了。
当然,对于自签名的网站的访问,网上的部分的做法是直接设置信任所有的证书,对于这种做法肯定是有风险的,所以这里我们不去介绍了,有需要自己去查。
下面我们去考虑,如何让OkHttpClient去信任我们的证书,接下里的例子就是靠12306这个福利站点了。
首先导出12306的证书,这里12306提供了下载地址:12306证书点击下载
下载完成,解压拿到里面的srca.cer,一会需要使用。ps:即使没有提供下载,也可以通过浏览器导出的,自行百度。
(一)、访问自签名的网站
首先把我们下载的srca.cer放到assets文件夹下,其实你可以随便放哪,反正能读取到就行。
然后在我们的OkHttpClientManager里面添加如下的方法:
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);
sslContext.init(
null,
trustManagerFactory.getTrustManagers(),
new SecureRandom());
mOkHttpClient.setSslSocketFactory(sslContext.getSocketFactory());
} catch (Exception e){
e.printStackTrace();
}
}
为了代码可读性,我把异常捕获的部分简化了,可以看到我们提供了一个方法传入InputStream流,InputStream就对应于我们证书的输入流。
代码内部,我们:
构造CertificateFactory对象,通过它的generateCertificate(is)方法得到Certificate。
然后将得到的Certificate放入到keyStore中。
接下来利用keyStore去初始化我们的TrustManagerFactory
由trustManagerFactory.getTrustManagers获得TrustManager[]初始化我们的SSLContext
最后,设置我们mOkHttpClient.setSslSocketFactory即可。
这样就完成了我们代码的编写,其实挺短的,当客户端进行SSL连接时,就可以根据我们设置的证书去决定是服务端信任的证书。
记得在Application中进行初始化:
public class MyApplication extends Application
{
@Override
public void onCreate()
{
super.onCreate();
try
{
OkHttpClientManager.getInstance()
.setCertificates(getAssets().open("srca.cer"));
} catch (IOException e)
{
e.printStackTrace();
}
}
然后尝试以下代码访问12306的网站:
OkHttpClientManager.getAsyn("https://kyfw.12306.cn/otn/",
new OkHttpClientManager.ResultCallback<String>(){
@Override
public void onError(Request request, Exception e){
e.printStackTrace();
}
@Override
public void onResponse(String u){
mTv.setText(u);
}
});
这样即可访问成功。完整代码已经更新至:https://github.com/hongyangAndroid/okhttp-utils,
可以下载里面的sample进行测试,里面包含12306的证书。
ok,到这就可以看到使用Okhttp可以很方便的应对自签名的网站的访问,只需要拿到包含公钥的证书即可。
(二)、使用字符串替代证书
下面继续,有些人可能觉得把证书copy到assets下还是觉得不舒服,其实我们还可以将证书中的内容提取出来,写成字符串常量,这样就不需要证书跟着app去打包了。
zhydeMacBook-Pro:temp zhy$ keytool -printcert -rfc -file srca.cer
—–BEGIN CERTIFICATE—–
MIICmjCCAgOgAwIBAgIIbyZr5/jKH6QwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ04xKTAn
BgNVBAoTIFNpbm9yYWlsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRTUkNBMB4X
DTA5MDUyNTA2NTYwMFoXDTI5MDUyMDA2NTYwMFowRzELMAkGA1UEBhMCQ04xKTAnBgNVBAoTIFNp
bm9yYWlsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRTUkNBMIGfMA0GCSqGSIb3
DQEBAQUAA4GNADCBiQKBgQDMpbNeb34p0GvLkZ6t72/OOba4mX2K/eZRWFfnuk8e5jKDH+9BgCb2
9bSotqPqTbxXWPxIOz8EjyUO3bfR5pQ8ovNTOlks2rS5BdMhoi4sUjCKi5ELiqtyww/XgY5iFqv6
D4Pw9QvOUcdRVSbPWo1DwMmH75It6pk/rARIFHEjWwIDAQABo4GOMIGLMB8GA1UdIwQYMBaAFHle
tne34lKDQ+3HUYhMY4UsAENYMAwGA1UdEwQFMAMBAf8wLgYDVR0fBCcwJTAjoCGgH4YdaHR0cDov
LzE5Mi4xNjguOS4xNDkvY3JsMS5jcmwwCwYDVR0PBAQDAgH+MB0GA1UdDgQWBBR5XrZ3t+JSg0Pt
x1GITGOFLABDWDANBgkqhkiG9w0BAQUFAAOBgQDGrAm2U/of1LbOnG2bnnQtgcVaBXiVJF8LKPaV
23XQ96HU8xfgSZMJS6U00WHAI7zp0q208RSUft9wDq9ee///VOhzR6Tebg9QfyPSohkBrhXQenvQ
og555S+C3eJAAVeNCTeMS3N/M5hzBRJAoffn3qoYdAO1Q8bTguOi+2849A==
—–END CERTIFICATE—–
使用keytool命令,以rfc样式输出。keytool命令是JDK里面自带的。
有了这个字符串以后,我们就不需要srca.cer这个文件了,直接编写以下代码:
public class MyApplication extends Application{
private String CER_12306 = "-----BEGIN CERTIFICATE-----\n" +
"MIICmjCCAgOgAwIBAgIIbyZr5/jKH6QwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ04xKTAn\n" +
"BgNVBAoTIFNpbm9yYWlsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRTUkNBMB4X\n" +
"DTA5MDUyNTA2NTYwMFoXDTI5MDUyMDA2NTYwMFowRzELMAkGA1UEBhMCQ04xKTAnBgNVBAoTIFNp\n" +
"bm9yYWlsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRTUkNBMIGfMA0GCSqGSIb3\n" +
"DQEBAQUAA4GNADCBiQKBgQDMpbNeb34p0GvLkZ6t72/OOba4mX2K/eZRWFfnuk8e5jKDH+9BgCb2\n" +
"9bSotqPqTbxXWPxIOz8EjyUO3bfR5pQ8ovNTOlks2rS5BdMhoi4sUjCKi5ELiqtyww/XgY5iFqv6\n" +
"D4Pw9QvOUcdRVSbPWo1DwMmH75It6pk/rARIFHEjWwIDAQABo4GOMIGLMB8GA1UdIwQYMBaAFHle\n" +
"tne34lKDQ+3HUYhMY4UsAENYMAwGA1UdEwQFMAMBAf8wLgYDVR0fBCcwJTAjoCGgH4YdaHR0cDov\n" +
"LzE5Mi4xNjguOS4xNDkvY3JsMS5jcmwwCwYDVR0PBAQDAgH+MB0GA1UdDgQWBBR5XrZ3t+JSg0Pt\n" +
"x1GITGOFLABDWDANBgkqhkiG9w0BAQUFAAOBgQDGrAm2U/of1LbOnG2bnnQtgcVaBXiVJF8LKPaV\n" +
"23XQ96HU8xfgSZMJS6U00WHAI7zp0q208RSUft9wDq9ee///VOhzR6Tebg9QfyPSohkBrhXQenvQ\n" +
"og555S+C3eJAAVeNCTeMS3N/M5hzBRJAoffn3qoYdAO1Q8bTguOi+2849A==\n" +
"-----END CERTIFICATE-----";
@Override
public void onCreate(){
super.onCreate();
OkHttpClientManager.getInstance()
.setCertificates(new Buffer()
.writeUtf8(CER_12306)
.inputStream());
}
注意Buffer是okio包下的,okhttp依赖okio。
ok,这样就省去将cer文件一起打包进入apk了。
接下来介绍,如何去生成证书以及在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
找到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/
temp/zhy_server.jks(填写你的路径).keystorePass值为密钥库密码:123456。
然后启动即可,对于命令行启动,依赖环境变量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端
目前我们app端依靠的应该是zhy_client.kjs。
ok,大家还记得,我们在支持https的时候调用了这么俩行代码:
sslContext.init(null, trustManagerFactory.getTrustManagers(),
new SecureRandom());
mOkHttpClient.setSslSocketFactory(sslContext.getSocketFactory());
注意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.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());
再次运行即可。然后就成功的做到了双向的验证,关于双向这块大家了解下即可。
源码都在https://github.com/hongyangAndroid/okhttp-utils之中。
ok,到此本篇博文就结束了,文章相当的长~~ 关于okhttp在https协议下的使用,应该没什么问题。
ps:如果大家对okhttp-utils有任何建议,非常欢迎提出,最近根据大家的需求修改相当频繁~~
第一步:打开某个地址的连接
这里咱打开我的博客地址,用GET方法请求
URL url = new URL("http://blog.youkuaiyun.com/xastdm");
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("GET");
第二步:判断是Https请求,设置SSLSocketFactory
如果是Https请求,那么做安全校验等操作,这里设置SSLSokcetFactory,这里有两种方法,一种是包涵Https证书的,一种是没有证书直接允许Https请求的,而证书不证书就是从SSLContext中来的:
// 设置SSLSocketFoactory,这里有两种:1.需要安全证书 2.不需要安全证书;看官且往下看
if (urlConnection instanceof HttpsURLConnection) { // 是Https请求
SSLContext sslContext = SSLContextUtil.getSSLContext();
if (sslContext != null) {
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslSocketFactory);
}
}
那么SSLContext怎么生成呢,哈哈先不急,咱们先把这个请求的流程走完,证书的加载往后边看哦。
第三步:设置必要属性
// 设置属性
urlConnection.setConnectTimeout(8 * 1000);
urlConnection.setReadTimeout(8 * 1000);
第四步:读取数据,发送到主线程,断开连接
int responseCode = urlConnection.getResponseCode();
if (responseCode == 200) { // 请求成功
InputStream inputStream = urlConnection.getInputStream();
// 读取结果,发送到主线程
...
inputStream.close();
}
urlConnection.disconnect();
完整代码
URL url = new URL("http://blog.youkuaiyun.com/yanzhenjie1003");
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("GET");
// 设置SSLSocketFoactory,这里有两种:1.需要安全证书 2.不需要安全证书;看官且往下看
if (urlConnection instanceof HttpsURLConnection) { // 是Https请求
SSLContext sslContext = SSLContextUtil.getSSLContext();
if (sslContext != null) {
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslSocketFactory);
}
}
// 设置属性
urlConnection.setConnectTimeout(8 * 1000);
urlConnection.setReadTimeout(8 * 1000);
int responseCode = urlConnection.getResponseCode();
if (responseCode == 200) { // 请求成功
InputStream inputStream = urlConnection.getInputStream();
// 读取结果,发送到主线程
...
inputStream.close();
}
urlConnection.disconnect();
使用流行框架NoHttp怎么玩Https
用NoHttp不要太简单,什么传参数、传文件、下载之类的,基本都是一两句话就搞定。
Request<String> request = NoHttp.createStringRequest(url, RequestMethod.POST);
// 注意这里设置SSLSokcetFactory的代码是相同的
SSLContext sslContext = SSLContextUtil.getSSLContext();
if (sslContext != null) {
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
httpsRequest.setSSLSocketFactory(socketFactory);
requestQueue.add(0, request, httpListener);// 添加到请求队列,等待接受结果
}
我们注意到上面设置Socket的代码是相同的,剩下的就是一句话new一个请求对象就完事。是不是比原生的还要简单啊?
这个框架叫NoHttp,是一个Android开源网络框架。
源码托管在:https://github.com/yanzhenjie/NoHttp
教程博客:http://blog.youkuaiyun.com/yanzhenjie1003
SSLSocketFactory对象怎么来
上面不论是纯Android代码还是NoHttp框架都用到了SSLContext,这家伙呢就是负责证书管理和信任管理器的,我们说Https可以有证书也可以没有证书,我们来看这两种情况。
有安全证书的SSLContext
我们把Https的证书放在assets目录下,然后通过流加载:
public static SSLContext getSSLContext() {
// 生成SSLContext对象
SSLContext sslContext = SSLContext.getInstance("TLS");
// 从assets中加载证书
InputStream inStream = Application.getInstance().getAssets().open("srca.cer");
// 证书工厂
CertificateFactory cerFactory = CertificateFactory.getInstance("X.509");
Certificate cer = cerFactory.generateCertificate(inStream);
// 密钥库
KeyStore kStore = KeyStore.getInstance("PKCS12");
kStore.load(null, null);
kStore.setCertificateEntry("trust", cer);// 加载证书到密钥库中
// 密钥管理器
KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyFactory.init(kStore, null);// 加载密钥库到管理器
// 信任管理器
TrustManagerFactory tFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tFactory.init(kStore);// 加载密钥库到信任管理器
// 初始化
sslContext.init(keyFactory.getKeyManagers(), tFactory.getTrustManagers(), new SecureRandom());
return sslContext;
}
需要强调的是,最后一句中的new SecureRandom()在Android4.4之前的系统中有Bug。
修复Android系统中SecureRandom的Bug
Android 4.4之前版本的Java加密架构(JCA)中使用的Apache Harmony 6.0M3及其之前版本的SecureRandom实现存在安全漏洞,具体位于classlib/modules/security/src/main/java/common/org/apache/harmony/security/provider/crypto/SHA1PRNG_SecureRandomImpl.java文件的engineNextBytes()方法里。当用户没有提供用于产生随机数的种子时,程序不能正确调整偏移量,导致伪随机数生成器(PRNG)生成随机序列的过程可被预测。
But值得高兴的NoHttp内部已经fix了这一bug,如果大家要自己写框架,恐怕要写很多代码去修复这个问题啦,所以推荐各位看官还是使用NoHttp。
没有安全证书的SSLContext
上面看需要安全证书的生成SSLContext就可以了,然后不需要证书的请求,需要两个对象,一个是SSLContext(上面已经解释过了);另一个是HostnameVerifier,顾名思义就是主机名称匹配的意思,我们看代码。
public static SSLContext getSLLContext() {
SSLContext sslContext = null;
try {
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}}, new SecureRandom());
} catch (Exception e) {
e.printStackTrace();
}
return sslContext;
}
就这么简单,只需要传一个null证书管理器喝一个默认的信任管理器即可。
下面再来看HostnameVerifier,既然是主机名称校验,那我们直接通过:
private static HostnameVerifier hostnameVerifier = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
};
这个怎么用呢?Android原生代码用法中,获取SSLContext的方法名不一样,多了setHostnameVerifier,其它跟有安全证书的使用一致:
URL url = new URL("http://blog.youkuaiyun.com/yanzhenjie1003");
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("GET");
// 设置SSLSocketFoactory,这里有两种:1.需要安全证书 2.不需要安全证书;看官且往下看
if (urlConnection instanceof HttpsURLConnection) { // 是Https请求
SSLContext sslContext = SSLContextUtil.getSSLContext();
if (sslContext != null) {
SSLSocketFactory sslSocketFactory = sslContext.getSLLContextNoCertificate();
((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslSocketFactory);
((HttpsURLConnection) urlConnection).setHostnameVerifier(SSLContextUtil.hostnameVerifier);
}
}
// 设置属性
urlConnection.setConnectTimeout(8 * 1000);
urlConnection.setReadTimeout(8 * 1000);
int responseCode = urlConnection.getResponseCode();
if (responseCode == 200) { // 请求成功
InputStream inputStream = urlConnection.getInputStream();
// 读取结果,发送到主线程
...
inputStream.close();
}
urlConnection.disconnect();
NoHttp用法,代码和上边的NoHttp加载证书的一样,多了一句setHostnameVerifier的:
Request<String> request = NoHttp.createStringRequest(url, RequestMethod.POST);
SSLContext sslContext = SSLContextUtil.getSSLContext();
if (sslContext != null) {
SSLSocketFactory socketFactory = sslContext.getSLLContextNoCertificate();
httpsRequest.setSSLSocketFactory(socketFactory);
httpsRequest.setHostnameVerifier(SSLContextUtil.hostnameVerifier);
requestQueue.add(0, request, httpListener);// 添加到请求队列,等待接受结果
}
https是如何工作的
http://blog.youkuaiyun.com/dd864140130/article/details/52598107
对称加密:加密和解密使用相同的密钥
非对称加密:其加密算法需要两个密钥:公开密钥(publickey)和私有密钥(private),两者是一对的。如果用公钥加密,只能用私钥才能解密。
非对称加密保密性好,因为公钥可以广泛传播,但是私钥只有其所有者知道。在一个安全的非对称密钥加密方案中,当信息用公钥加密后,只有用私钥才能解密。所以,即使一个黑客拿到你公钥加密过后的信息,也无法解密它,因为它没有配对的私钥。这样,传输的消息就是安全的。但加密和解密花费的时间较长,不适合对大文件加密而只适合对少量的数据加密。常见的非对称加密算法有RSA,ECC,DSA(数字签名)等。
Hash算法:是一种单向算法,通过Hash算法可以对目标数据生成一段特定长度、唯一的hash值,但是不能通过这个hash值重新计算出原始的数据,因此也称之为摘要算法,经常被用在不需要数据还原的密码加密以及数据完整性校验上,常用的算法有MD2,MD4,MD5,SHA等。
加密(Cipher)
在Java 1.2时,引入JCR(Java 加密扩展)系统,用来负责java中的密钥和证书。
我们都知道,如果我们想要加密或解密一些信息,我们必须要有一个密钥。这好比你想要开门或者锁门,必须要有钥匙一样。
在java中,密钥由KeyGenerator或KeyPairGenerator生成。前者用来生成对称密钥,后者用来生成非对称密钥。
证书(Certificate)
在现实中,如果你进入到钻石专卖店中想要买一颗钻石,你怎么知道钻石是真的?作为一个普通人来说,我们没有钻石方面的知识,但是如果这钻石有个由美国政府颁发的许可证,我们就会相信它是真的。(去过珠宝店的同学都知道,每件珠宝都有自己的鉴定书)
证书的作用也是如此。在计算机世界中,它可能是包含一些密钥,是另一个证书(姑且称之为证书B好了)。这些密钥是我们需要的,而证书B是一个许可证,用来证明这个证书是可信赖的。
做个简短的解释,每个钻石都有自己的“身份证书”,但是如何说明这个身份证书是合法的,而不是自己伪造的?因此,我们需要一个权威的机构来证明这个钻石的身份证书是合法的,如果这个钻石的身份是合法的,该权威机构就会为其颁发一个“许可证”,这个许可证就相当于上面我们说到的证书B。可以看到,证书B的目的就是证明这个身份证书是这个钻石的身份证书,即证明某某东西是某某东西的东西
问题来了:我们怎么确定证书B是可信赖的呢?这个问题非常棒。
Android已经把将近150个CA根证书(数字证书认证机构认证过的证书)内置在我们手机中。这150多个证书被全世界信赖,他们就像是美国的大法官。
这150多个证书类似我们刚才说的证书B,分别用来证明某某东西是某某东西的东西
B证书中里有另外一个证书(姑且称之为C证书),我们通过检查C来确定C是否是可信任的。。。通过这个证书链,如果我们找到的最后一个或根证书 和手机中预置的150个证书中某个相同,我们就可以这个证书原件(此处就是B)
这里我对证书链进行说明。证书之间的信任关系是可以嵌套的。比如,CA信任D,D信任C,C信任B
,B信任A。。。这就是证书链。只要我们信任证书链上的头一个证书,那么后续的证书都是可以信任的。这里也就是如果我信任了证书CA,那么后面的D,C,B,A都是可以信任的。这里来打个比方,架设军队中的每个士兵都只认识自己的直接上级,那么这时候总司令怎么确认某个士兵是自己部队 当中的呢?总司令(CA)会问的直接下级(D),而司令的直接下级又会找自己的直接下级,依次往下找…如果最后能找到这个士兵直属上级,那就说明这个士兵是该部队当中的。
附:证书有多种格式
x.509
x.509证书通常用于包含一个公钥
PKCS12
PKCS12证书通常用来包含一个私钥。因此,PKCS12需要密码才能打开。
Https
现在我们来了解https部分。Https(http over ssl)包含上面提到的加密和证书两部分,被设计用来在Internet安全进行通信。
如何安全的通信?
如何安全通信呢?对称加密是我们最先想到的方案:将数据进行加密,然后将加密过的数据和密钥同时传到服务器,服务器使用这个密钥解密加密过后的数据。
现在,我们来看看这种可能的场景:黑客截获了该通信,这意味着黑客拥有了密钥和密文。一旦黑客有了密钥,那么解密密文就是很简单的事情了,我们的数据就这样泄漏了。
如何使用非对称加密?
上面的解决方案非常不安全。我们继续往下看。使用非对称加密怎么样?
这个想法非常棒:
服务端发送给你公钥,你使用这个公钥加密数据。因为服务端是唯一拥有私钥的,
这意味着只有服务端能够解密密文。即使黑客截获了该通讯,但因为没有私钥也就无法解密密文。
但是,非对称加密比对称加密更加耗时。为了用户体验,不建议使用非对称加密这种方式来加密/解密大量的数据
最终方案
前两种方案都无法解决我们安全通信,我们怎么结合上面的两种方案呢?来看看最终方案:
上面这张图片已经清楚的展示了HTTPS工作的流程。
1.[Server]生成一对密钥:公钥和私钥,我们称之为“KeyPub”,“KeyPri”
2.[Server]服务端将公钥(KeyPub)发送到客户端
3.[Client]生成一个对称密钥(姑且称之为key2),然后用key2加密数据。
4.[Client]使用公钥(KeyPub)加密key2.这时,key2是安全的,因为只有服务器有私钥KeyPri
5.[Client]发送用key2加密后的信息及用KeyPub加密过的key2到服务端
6.[Server]服务端使用KeyPri解密得到加密过的key2,得到真正的key2
7.[Server]使用key2解密消息正文。这样,数据就被安全的传输到了服务端。
结论
由于对称加密比非对称加密快,https决定使用对称加密来加密数据,使用非对称加密对称加密生成的密钥,以确保安全。
这篇文章最后并没有很好的说明证书的作用,因此如果你以前没了解过,到此可能产生一些困惑,后面我会再单独写一篇文章来说明下。
Retrofit中如何正确的使用
httpshttp://blog.youkuaiyun.com/dd864140130/article/details/52625666
中间人攻击(MITM攻击):
黑客拦截并篡改网络中的通信数据。又分为被动MITM和主动MITM,被动MITM只窃取通信数据而不修改,而主动MITM不当能窃取数据,还会篡改通信数据。
证书锁定的缺点
证书锁定尽管带了较高的安全性,但是这种安全性的提高却牺牲了灵活性。一旦当证书发生变化时,我们的客户端也必须随之升级,除此之外,我们的服务端不得不为了兼容以前的客户端而做出一些妥协或者说直接停用以前的客户端,这对开发者和用户来说并不是那么的友好。
但实际上,极少情况下我们才会变动证书。因此,如果产品安全性要求比较高还是启动证书锁定吧。
单向自定义证书+非对称加密和对称加密的混用
http://www.jianshu.com/p/59a102f150aa
android中进行https连接的方式
http://blog.youkuaiyun.com/a79412906/article/details/10060795
密码学基础
http://www.williamlong.info/archives/499.html
Android端支持HTTP和HTTPS
http://www.oschina.net/question/2245602_177671
Android使用https链接
http://blog.youkuaiyun.com/earbao/article/details/22584507
Android Https请求详细demo
http://itindex.net/detail/51666-android-https-demo?utm_source=tuicool&utm_medium=referral
android http 和https请求
http://www.cnblogs.com/zhuqiang/p/3623786.html