Android实践:Https不再疑惑

来源:互联网 发布:协同过滤算法的实现 编辑:程序博客网 时间:2024/05/18 09:04
近期由于公司的工作需要,需要将原有的http接口切换到https,故做了如下学习和整理。本文先简要说明https协议原理,然后https协议在浏览器和App的实践两方面进行讲述;
一、https协议原理
1.要理解https是什么,我们必须应该理解如下几个关键词,和它们之间的关系:
  http:超文本传输协议,广泛用于从WWW服务器传输超文本到本地浏览器的传输协议;
  SSL/TLS:最广泛的密码通信方案,综合运用了对称密码、消息认证码、公钥密码、数字签名、伪随机数生成密码等密码技术;
  https:在SSL/TLS之上承载HTTP,将两种协议进行叠加;
 
2.在继续讲解https之前,我们得先了解下几个密码学套件,对于比较好理解的对称密码和非对称密码就不进行详细的讲解了:
  
  单项散列函数:根据任意长度的消息计算出固定长度的散列值,来确定文件的完整性;
  
  消息认证码:是一种密钥相关联的单向散列函数。要计算MAC必须持有共享秘钥,消息变化MAC值就会不一致,故验证身份和完整性;
  
  数字签名:反用密钥对,使用自己私钥加密生成签名,验证方用你的公钥能解密即可验证加密方是你,故能验证身份和完整性;
  
  公钥证书:认证机构通过电话、邮件或本人确认后,使用机构的私钥对你的公钥进行签名,保证了你的公钥的正确性;
  
3.完成基本密码套件的学习,接下来我们继续介绍https。由上可知https的核心在于SSL/TLS,让主要解决了如下接个问题:
  机密性:信息传输过程中的被第三方窃听—采用对称密码加密,伪随机数生成器密钥,公钥密码或Diffe-Hellman进行公钥交换;
  完整性:信息传输过程中被中间人篡改—采用单向散列函的消息认证码进行完整性验证;
  认证性:信息传输的对方身份是否合法—对公钥加上数字签名所生成的证书,对通信对象进行认证;
4.在https的进行通信的过程中,它是如何有序的运用上面的密码学套件的呢?如下:
该部分是基于TLS1.0进行说明,TLS协议是由“TLS记录协议”和“TLS握手协议”这两层协议叠加而成:
 
  1.握手协议:除加密之外的各种工作,分为4个子协议:握手协议、密码规格变更协议、警告协议和应用数据协议;
    握手子协议:负责在客户端和服务器之间协商决定密码算法和共享秘钥;
    密码规格变更协议:负责向通信对象传达变更密码方式的信号;
    警告协议:负责在发生错误的时候将错误传达给对方;
    应用数据协议:将TLS上面承载的应用数据传达给通信对象的协议;
 
  2.记录协议:位于TLS握手协议的下层,负责使用对称密码对消息进行压缩、加密以及数据的认证;
    消息被分割成多个较短的片段,然后对每个片段进行压缩;
    压缩片段会被加上消息认证码,保证完整性,并进行数据的认证,可以识别出篡改。为了防止重放攻击,在计算消息认证码时加上了片段的编号;
    经过压缩的片段在加上消息认证码会一起通过对称密码进行加密;
    经过加密的数据再加上由数据类型、版本号、压缩后的长度组成的报头就是最终的报文数据;
 
经过以上的大概讲解,相信大家对https有了更深入的认识,并且也能更好的理解为什么在实践https的时候要生成各种密钥和信任库了。

二、https协议实践

理解上面的相关原理后,我们就开始实现HttpsServlet来模拟简单登录接口,然后通过浏览器和app的访问该https接口;
1.服务端http实现
我们首先实现服务端http协议的get和post通信,项目的结构和主要实现代码如下:

HttpsServlet.java:
[java] view plain copy
  1. public class HttpsServlet extends HttpServlet {  
  2.     @Override  
  3.     protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
  4.         System.out.println("doPost");  
  5.         doLoginRequest(req, resp);  
  6.     }  
  7.   
  8.   
  9.     @Override  
  10.     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
  11.         System.out.println("doGet");  
  12.         doLoginRequest(req, resp);  
  13.     }  
  14.   
  15.   
  16.     //实现简单的登录逻辑  
  17.     private void doLoginRequest(HttpServletRequest req, HttpServletResponse resp) throws IOException {  
  18.         PrintStream printStream = new PrintStream(resp.getOutputStream());  
  19.         HttpsResponse httpsResponse = new HttpsResponse();  
  20.         String userName = req.getParameter("userName");  
  21.         String passWord = req.getParameter("passWord");  
  22.         if ("123".equals(userName) && "123".equals(passWord)) {  
  23.             httpsResponse.setCode("000");  
  24.             httpsResponse.setMessage("login success!");  
  25.         } else {  
  26.             httpsResponse.setCode("004");  
  27.             httpsResponse.setMessage("login faild!");  
  28.         }  
  29.         printStream.println(JSON.toJSONString(httpsResponse));  
  30.     }  
  31. }  
web.xml:
[html] view plain copy
  1. <!DOCTYPE web-app PUBLIC  
  2.  "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"  
  3.  "http://java.sun.com/dtd/web-app_2_3.dtd" >  
  4. <web-app>  
  5.     <display-name>Archetype Created Web Application</display-name>  
  6.     <servlet>  
  7.         <servlet-name>HttpsServlet</servlet-name>  
  8.         <servlet-class>main.com.chengxiang.servlet.HttpsServlet</servlet-class>  
  9.     </servlet>  
  10.     <servlet-mapping>  
  11.         <servlet-name>HttpsServlet</servlet-name>  
  12.         <url-pattern>/HttpsServlet</url-pattern>  
  13.     </servlet-mapping>  
  14. </web-app>  

实现客户端登录的get和post请求,项目目录结构如下:

NextActivity.java:

[java] view plain copy
  1. public class NextActivity extends AppCompatActivity {  
  2.     private EditText userNameEditText;  
  3.     private EditText passWorldEditText;  
  4.     private Button loginButton;  
  5.     private TextView responseTextView;  
  6.   
  7.   
  8.     private Handler handler = new Handler() {  
  9.         @Override  
  10.         public void handleMessage(Message msg) {  
  11.             super.handleMessage(msg);  
  12.             switch (msg.what) {  
  13.                 case 1:  
  14.                     Bundle bundle = msg.getData();  
  15.                     HttpsResponse httpsResponse = (HttpsResponse) bundle.getSerializable("result");  
  16.                     responseTextView.setText(httpsResponse.toString());  
  17.                     break;  
  18.             }  
  19.         }  
  20.     };  
  21.   
  22.   
  23.     @Override  
  24.     protected void onCreate(Bundle savedInstanceState) {  
  25.         super.onCreate(savedInstanceState);  
  26.         setContentView(R.layout.activity_next);  
  27.         userNameEditText = (EditText) findViewById(R.id.next_username_edittext);  
  28.         passWorldEditText = (EditText) findViewById(R.id.next_password_password);  
  29.         loginButton = (Button) findViewById(R.id.next_login_button);  
  30.         responseTextView = (TextView) findViewById(R.id.next_response_text);  
  31.   
  32.   
  33.         assert loginButton != null;  
  34.         loginButton.setOnClickListener(new View.OnClickListener() {  
  35.             @Override  
  36.             public void onClick(View v) {  
  37.                 responseTextView.setText("");  
  38.                 final String userName = userNameEditText.getText().toString();  
  39.                 final String passWorld = passWorldEditText.getText().toString();  
  40.   
  41.   
  42.                 new Thread(new Runnable() {  
  43.                     @Override  
  44.                     public void run() {  
  45.                         doLoginGet(userName, passWorld);  
  46. //                        doLoginPost(userName,passWorld);  
  47.                     }  
  48.                 }).start();  
  49.             }  
  50.         });  
  51.     }  
  52.   
  53.   
  54.     /** 
  55.      * 执行登录Get请求 
  56.      * @param userName 用户名 
  57.      * @param passWorld 用户密码 
  58.      */  
  59.     private void doLoginGet(String userName, String passWorld) {  
  60.         try {  
  61.             //服务器的ip地址根据你自己的欢迎修改  
  62.             URL url = new URL("http://100.80.28.137:8080/qserver/HttpsServlet?userName=" + userName + "&passWord=" + passWorld);  
  63.             HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();  
  64.             httpURLConnection.setRequestMethod("GET");  
  65.             if (httpURLConnection.getResponseCode() == 200) {  
  66.                 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(httpURLConnection.getInputStream()));  
  67.                 String result = new String();  
  68.                 String readLine;  
  69.                 if ((readLine = bufferedReader.readLine()) != null) {  
  70.                     result += readLine;  
  71.                 }  
  72.                 bufferedReader.close();  
  73.                 httpURLConnection.disconnect();  
  74.   
  75.   
  76.                 Message message = handler.obtainMessage();  
  77.                 message.what = 1;  
  78.                 Bundle bundle = new Bundle();  
  79.                 HttpsResponse httpsResponse = JSON.parseObject(result, HttpsResponse.class);  
  80.                 bundle.putSerializable("result", httpsResponse);  
  81.                 message.setData(bundle);  
  82.                 handler.sendMessage(message);  
  83.             }  
  84.         } catch (UnsupportedEncodingException e) {  
  85.             e.printStackTrace();  
  86.         } catch (ProtocolException e) {  
  87.             e.printStackTrace();  
  88.         } catch (MalformedURLException e) {  
  89.             e.printStackTrace();  
  90.         } catch (IOException e) {  
  91.             e.printStackTrace();  
  92.         }  
  93.     }  
  94.   
  95.   
  96.     /** 
  97.      * 执行登录Post请求 
  98.      * @param userName 用户名 
  99.      * @param passWorld 用户密码 
  100.      */  
  101.     private void doLoginPost(String userName, String passWorld) {  
  102.         try {  
  103.             //服务器的ip地址根据你自己的欢迎修改  
  104.             URL url = new URL("http://100.80.28.137:8080/qserver/HttpsServlet");  
  105.             HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();  
  106.             httpURLConnection.setRequestMethod("POST");  
  107.             String params = "userName=" + userName + "&passWord=" + passWorld;  
  108.             BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(httpURLConnection.getOutputStream()));  
  109.             bufferedWriter.write(params.toString());  
  110.             bufferedWriter.flush();  
  111.   
  112.   
  113.             if (httpURLConnection.getResponseCode() == 200) {  
  114.                 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(httpURLConnection.getInputStream()));  
  115.                 String result = new String();  
  116.                 String readLine;  
  117.                 if ((readLine = bufferedReader.readLine()) != null) {  
  118.                     result += readLine;  
  119.                 }  
  120.                 bufferedReader.close();  
  121.                 httpURLConnection.disconnect();  
  122.   
  123.   
  124.                 Message message = handler.obtainMessage();  
  125.                 message.what = 1;  
  126.                 Bundle bundle = new Bundle();  
  127.                 HttpsResponse httpsResponse = JSON.parseObject(result, HttpsResponse.class);  
  128.                 bundle.putSerializable("result", httpsResponse);  
  129.                 message.setData(bundle);  
  130.                 handler.sendMessage(message);  
  131.             }  
  132.         } catch (MalformedURLException e) {  
  133.             e.printStackTrace();  
  134.         } catch (IOException e) {  
  135.             e.printStackTrace();  
  136.         }  
  137.     }  
  138. }  
activity_next.xml:
[html] view plain copy
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     xmlns:tools="http://schemas.android.com/tools"  
  4.     android:layout_width="match_parent"  
  5.     android:layout_height="match_parent"  
  6.     android:orientation="vertical"  
  7.     tools:context="com.qunar.hotel.NextActivity">  
  8.     <LinearLayout  
  9.         android:layout_width="match_parent"  
  10.         android:layout_height="wrap_content"  
  11.         android:orientation="horizontal">  
  12.         <TextView  
  13.             android:id="@+id/next_username_text"  
  14.             android:layout_width="wrap_content"  
  15.             android:layout_height="wrap_content"  
  16.             android:text="UserName:" />  
  17.         <EditText  
  18.             android:id="@+id/next_username_edittext"  
  19.             android:layout_width="match_parent"  
  20.             android:layout_height="wrap_content" />  
  21.     </LinearLayout>  
  22.     <LinearLayout  
  23.         android:layout_width="match_parent"  
  24.         android:layout_height="wrap_content"  
  25.         android:orientation="horizontal">  
  26.         <TextView  
  27.             android:id="@+id/next_password_text"  
  28.             android:layout_width="wrap_content"  
  29.             android:layout_height="wrap_content"  
  30.             android:text="PassWord:" />  
  31.         <EditText  
  32.             android:id="@+id/next_password_password"  
  33.             android:layout_width="match_parent"  
  34.             android:layout_height="wrap_content" />  
  35.     </LinearLayout>  
  36.     <Button  
  37.         android:id="@+id/next_login_button"  
  38.         android:layout_width="match_parent"  
  39.         android:layout_height="wrap_content"  
  40.         android:text="Login" />  
  41.     <TextView  
  42.         android:id="@+id/next_response_text"  
  43.         android:layout_width="match_parent"  
  44.         android:layout_height="wrap_content" />  
  45. </LinearLayout>  
AndroidManifext.xml:
[html] view plain copy
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     package="com.qunar.hotel">  
  4.     //增加网络访问权限  
  5.     <uses-permission android:name="android.permission.INTERNET" />  
  6.    <application  
  7.         android:allowBackup="true"  
  8.         android:icon="@mipmap/ic_launcher"  
  9.         android:label="@string/app_name"  
  10.         android:supportsRtl="true"  
  11.         android:theme="@style/AppTheme">  
  12.         <activity android:name=".HotelActivity" />  
  13.         //登录Activity声明  
  14.         <activity android:name=".NextActivity"></activity>  
  15.     </application>  
  16. </manifest>  
启动服务器tomcat,运行客户端app效果如下:

启动Fiddler抓包如下,Get请求(Post请求可以自行实践)数据包如下,为明文传输!

接下来,我们就开始将当前http连接修改成https,并查看修改后的抓包情况。

2.生成密钥对、证书和信任证书库
(详情请查阅http://docs.oracle.com/javase/7/docs/technotes/tools/solaris/keytool.html)
在生成该操作过程中,我们将主要用Keytool工具:
  keytool:是一个Java数据证书的管理工具,keytool将密钥(key)和证书(certificates)存在一个称为keystore的文件中;
  keystore:在keystore里,包含两种数据:密钥实体(key entity)-密钥(secret key)又或者是公私钥密钥对、可信任的证书实体(trusted certificate entries)-只包含公钥;
生成服务器证书库
[plain] view plain copy
  1. C:\Users\chengxiang.peng.QUNARSERVERS>keytool -genkeypair -v -alias tomcat -keyalg RSA -keystore D:\ssl\tomcat.keystore -dname "CN=www.qserver.com,OU=pengchengxiang,O=pengchengxiang,L=Beijing,ST=Beijing,c=cn" -storepass 123456 -keypass 123456 -validity 365  
  2. 正在为以下对象生成 2,048 位RSA密钥对和自签名证书 (SHA256withRSA) (有效期为 365天):  
  3.          CN=www.qserver.com, OU=pengchengxiang, O=pengchengxiang, L=Beijing, ST=Beijing, C=cn  
  4. [正在存储D:\ssl\tomcat.keystore]  
  -genkey:创建一个新的密钥;
  -alias:密钥别名,每个keystore都关联一个独一无二的别名;
  -keyalg:使用的加密算法,使用RSA;
  -keystore:密钥存储目录,保存在D:\ssl目录下;
  -dname:
    CN(Common Name名字与姓氏)
    OU(Organization Unit组织单位名称)
    O(Organization组织名称)
    L(Locality城市或区域名称)
    ST(State州或省份名称)
    C(Country国家名称)
  -storepass:存取密码,这个密码供系统从keystore文件取信息的时候使用;
  -keypass:私有密钥的密码;
  -validity:有效期,365天;
注意1:生成服务端密钥时,alias必须为tomcat,否则后续启动tomcat服务的时候会报错Caused by: java.io.IOException: Alias name tomcat does not identify a key entry ;
注意2:生成服务端密钥库时,CN必须与服务端的域名或者ip地址相同,否则正常获取证书访问;

从服务器证书库中导出服务器证书
[plain] view plain copy
  1. C:\Users\chengxiang.peng.QUNARSERVERS>keytool -export -v -alias tomcat -keystore D:\ssl\tomcat.keystore -storepass 123456 -rfc -file D:\ssl\tomcat.cer  
  2. 存储在文件 <D:\ssl\tomcat.cer> 中的证书  
生成客户端信任证书库(由服务端证书生成的证书库)
[plain] view plain copy
  1. C:\Users\chengxiang.peng.QUNARSERVERS>keytool -import -v -alias tomcat -file D:\ssl\tomcat.cer -keystore D:\ssl\qproject.truststore -storepass 123456  -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider  
  2. 所有者: CN=www.qserver.com, OU=pengchengxiang, O=pengchengxiang, L=Beijing, ST=Beijing, C=cn  
  3. 发布者: CN=www.qserver.com, OU=pengchengxiang, O=pengchengxiang, L=Beijing, ST=Beijing, C=cn  
  4. 序列号: 61c9f857  
  5. 有效期开始日期: Sun Nov 06 18:14:44 CST 2016, 截止日期: Mon Nov 06 18:14:44 CST2017  
  6. 证书指纹:  
  7.          MD5: 85:BE:DE:EF:43:1A:1B:BC:62:5A:D4:4C:BC:89:C3:E0  
  8.          SHA1: 6A:D5:5F:88:FE:DA:63:9C:A6:85:6E:47:6A:76:FA:C6:CE:D9:A4:BE  
  9.          SHA256: 3A:73:51:F3:75:06:E1:B5:DE:62:59:CB:18:60:BD:AE:F4:0F:2D:B0:7A:  
  10. 02:CC:9D:37:27:87:AE:6F:7F:F6:AC  
  11.          签名算法名称: SHA256withRSA  
  12.          版本: 3  
  13. 扩展:  
  14. #1: ObjectId: 2.5.29.14 Criticality=false  
  15. SubjectKeyIdentifier [  
  16.     KeyIdentifier [         
  17.         0000: F4 64 61 81 5B 41 4F C4   6F D6 CE 66 6D 02 84 17  .da.[AO.o..fm...  
  18.         0010: 29 4E 58 A5                                        )NX.  
  19.     ]  
  20. ]  
  21. 是否信任此证书? [否]:  是  
  22. 证书已添加到密钥库中  
  23. [正在存储D:\ssl\qproject.truststore]  
注意3:-storetype BKS,是Android上面可以识别的格式。如果不指定,jdk默认生成的格式是JKS。 -provider org.bouncycastle.jce.provider.BouncyCastleProvider,需要下载jar包bcprov-jdk16-146.jar放到JDK_HOME\jre\lib\ext\目录下。 注意需要java1.6,其他的版本android下面有版本不匹配的问题;
生成客户端证书库
[plain] view plain copy
  1. C:\Users\chengxiang.peng.QUNARSERVERS>keytool -genkeypair -alias qproject -keyalg RSA -storetype PKCS12 -keystore D:\ssl\qproject.p12 -dname "CN=www.qserver.com,OU=pengchengxiang,O=pengchengxiang,L=Beijing,ST=Beijing,c=cn" -storepass 123456 -keypass 123456 -validity 365  
从客户端证书库中导出客户端证书
[plain] view plain copy
  1. C:\Users\chengxiang.peng.QUNARSERVERS>keytool -export -v -alias qproject -keystore D:\ssl\qproject.p12 -storetype PKCS12 -storepass 123456 -rfc -file D:\ssl\qproject.cer  
  2. 存储在文件 <D:\ssl\qproject.cer> 中的证书  
生成服务端信任证书库(使得服务器信任客户端证书)
[plain] view plain copy
  1. C:\Users\chengxiang.peng.QUNARSERVERS>keytool -import -v -alias qproject -file D:\ssl\qproject.cer -keystore D:\ssl\tomcat.keystore -storepass 123456  
  2. 所有者: CN=www.qserver.com, OU=pengchengxiang, O=pengchengxiang, L=Beijing, ST=Beijing, C=cn  
  3. 发布者: CN=www.qserver.com, OU=pengchengxiang, O=pengchengxiang, L=Beijing, ST=Beijing, C=cn  
  4. 序列号: 6313eac6  
  5. 有效期开始日期: Sun Nov 06 18:26:42 CST 2016, 截止日期: Mon Nov 06 18:26:42 CST2017  
  6. 证书指纹:  
  7.          MD5: F6:1B:DB:98:DF:64:5E:8C:77:F1:6F:A7:DC:5D:B3:EB  
  8.          SHA1: 78:7F:C2:30:A5:A7:60:91:5C:3E:D4:01:F1:C3:5B:CC:3E:09:5B:2E  
  9.          SHA256: 8F:84:4D:DD:E6:1A:E7:68:CA:08:07:CC:45:75:D2:2F:CA:03:33:C3:E6:  
  10. 95:DF:3C:2F:37:8E:6F:39:3F:47:A3  
  11.          签名算法名称: SHA256withRSA  
  12.          版本: 3  
  13. 扩展:  
  14. #1: ObjectId: 2.5.29.14 Criticality=false  
  15.     SubjectKeyIdentifier [  
  16.         KeyIdentifier [  
  17.             0000: 76 F0 FF 0B 04 02 05 3F   AA E8 5B 68 7A B1 DF 22  v......?..[hz.."  
  18.             0010: 7F 04 CB E2                                        ....  
  19.         ]  
  20.     ]  
  21. 是否信任此证书? [否]:  是  
  22. 证书已添加到密钥库中  
  23. [正在存储D:\ssl\tomcat.keystore]  
查看证书库中的全部证书(服务器密钥库,客户端密钥库和信任库)
[plain] view plain copy
  1. C:\Users\chengxiang.peng.QUNARSERVERS>keytool -list -keystore D:\ssl\tomcat.keystore -storepass 123456  
  2. 密钥库类型: JKS  
  3. 密钥库提供方: SUN  
  4. 您的密钥库包含 2 个条目  
  5. qproject, 2016-11-6, trustedCertEntry,  
  6. 证书指纹 (SHA1): 78:7F:C2:30:A5:A7:60:91:5C:3E:D4:01:F1:C3:5B:CC:3E:09:5B:2E  
  7. tomcat, 2016-11-6, PrivateKeyEntry,  
  8. 证书指纹 (SHA1): 6A:D5:5F:88:FE:DA:63:9C:A6:85:6E:47:6A:76:FA:C6:CE:D9:A4:BE  
  9. C:\Users\chengxiang.peng.QUNARSERVERS>keytool -list -keystore D:\ssl\qproject.p12 -storepass 123456  
  10. 密钥库类型: JKS  
  11. 密钥库提供方: SUN  
  12. 您的密钥库包含 1 个条目  
  13. qproject, 2016-11-6, PrivateKeyEntry,  
  14. 证书指纹 (SHA1): 78:7F:C2:30:A5:A7:60:91:5C:3E:D4:01:F1:C3:5B:CC:3E:09:5B:2E  
  15. C:\Users\chengxiang.peng.QUNARSERVERS>keytool -list -keystore D:\ssl\qproject.truststore -storepass 123456 -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider  
  16. 密钥库类型: BKS  
  17. 密钥库提供方: BC  
  18. 您的密钥库包含 1 个条目  
  19. tomcat, 2016-11-6, trustedCertEntry,  
  20. 证书指纹 (SHA1): 6A:D5:5F:88:FE:DA:63:9C:A6:85:6E:47:6A:76:FA:C6:CE:D9:A4:BE  
运行完成后,D:\ssl\目录下生成如下文件:

qproject.cer:客户端证书;
qproject.p12:客户端密钥;
qproject.truststore:客户端信任证书库;
tomcat.cer:服务端证书;
tomcat.keysotre:服务端密钥和信任证书库;

3.Tomcat的配置
(详细请查阅:https://tomcat.apache.org/tomcat-9.0-doc/ssl-howto.html)
修改tomcat的配置文件${catalina.base}/conf/server.xml;
[html] view plain copy
  1. <Connector port="8444" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="150" SSLEnabled="true" scheme="https" secure="true" keystoreFile="D:\ssl\tomcat.keystore" keystorePass="123456" clientAuth="true" sslProtocol="TLS" truststoreFile="D:\ssl\tomcat.keystore" truststorePass="123456"/>  
  port:https访问的端口;
  SSLEnabled:true,开启https服务;
  scheme:https;
  secure:true,开启服务端安全通信,客户端获取服务器端证书;
  keystoreFile:D:\ssl\tomcat.keystore;  
  keystorePass:123456,服务器证书库密码;
  clientAuth:true,开启验证客户端;
  sslProtocol:TLS,使用的协议;
  truststoreFile:D:\ssl\tomcat.keystore,服务器证书库(已导入客户端证书) ;
  truststorePass:123456; 
提示1:启动tomcat服务器,报错如下:
Caused by: java.io.IOException: Alias name tomcat does not identify a key entry
   at org.apache.tomcat.util.net.jsse.JSSEUtil.getKeyManagers(JSSEUtil.java:306)
   at org.apache.tomcat.util.net.AbstractJsseEndpoint.initialiseSsl(AbstractJsseEndpoint.java:90)
   at org.apache.tomcat.util.net.NioEndpoint.bind(NioEndpoint.java:245)
   at org.apache.tomcat.util.net.AbstractEndpoint.init(AbstractEndpoint.java:839)
   at org.apache.coyote.AbstractProtocol.init(AbstractProtocol.java:558)
   at org.apache.coyote.http11.AbstractHttp11Protocol.init(AbstractHttp11Protocol.java:65)
   at org.apache.catalina.connector.Connector.initInternal(Connector.java:1010)
   ... 13 more
处理1:检查你生成的服务器证书的-alias是否为tomcat

由于我们是在本机测试,并且在生成证书的时候CN配置了www.qserver.com,故我们在测试之前添加Hosts配置如下;
[plain] view plain copy
  1. # httpstest  
  2. 127.0.0.1 www.qserver.com  
启动tomcat服务器,我们先用浏览器以get的方式测试下服务端https接口功能。访问成功!服务器接口修改完毕,接下来我们来修改客户端;

提示2:初次访问接口时,会出现如下错误:


处理2:由于服务器Https握手过程中,交换的证书不在浏览器信任的范围,故提示错误。

Internet选项->内容->证书->受信任的根证书颁发机构->导入->tomcat.cer->存入受信任的根证书颁发机构->确认安装此证书;


4.客户端修改

添加项目目录asset,并在该目录下添加服务端验证客户端的证书qproject.p12,和客户端验证服务端的信任证书库qproject.truststore;

添加客户端https身份验证等相关逻辑;
HttpsHelper.java:
[java] view plain copy
  1. public class HttpsHelper {  
  2.     //p12证书类型  
  3.     private static final String KEY_STORE_TYPE_P12 = "PKCS12";  
  4.     //bks证书类型  
  5.     private static final String KEY_STORE_TYPE_BKS = "bks";  
  6.     //客户端给服务器端认证的证书  
  7.     private static final String KEY_STORE_QPRPJECT_PATH = "qproject.p12";  
  8.     //客户端验证服务器端的信任证书库  
  9.     private static final String KEY_STORE_QPROJECTTRUST_PATH = "qproject.truststore";  
  10.     //客户端证书密码  
  11.     private static final String KEY_STORE_PASSWORD = "123456";  
  12.     //客户端信任证书库密码  
  13.     private static final String KEY_STORE_TRUST_PASSWORD = "123456";  
  14.   
  15.   
  16.     /** 
  17.      * 获取SSLContext 
  18.      * @param context 上下文 
  19.      * @return SSLContext 
  20.      */  
  21.     private static SSLContext getSSLContext(Context context) {  
  22.         SSLContext sslContext = null;  
  23.         try {  
  24.             //初始化服务器端需要验证的客户端证书-qprojectKeyStore、客户端信任的服务器端证书库-trustKeyStore  
  25.             KeyStore qprojectKeyStore = KeyStore.getInstance(KEY_STORE_TYPE_P12);  
  26.             KeyStore trustKeyStore = KeyStore.getInstance(KEY_STORE_TYPE_BKS);  
  27.             InputStream qprojectInPutStream = context.getResources().getAssets().open(KEY_STORE_QPRPJECT_PATH);  
  28.             InputStream trustInputStream = context.getResources().getAssets().open(KEY_STORE_QPROJECTTRUST_PATH);  
  29.             try {  
  30.                 qprojectKeyStore.load(qprojectInPutStream, KEY_STORE_PASSWORD.toCharArray());  
  31.                 trustKeyStore.load(trustInputStream, KEY_STORE_TRUST_PASSWORD.toCharArray());  
  32.             } catch (Exception e) {  
  33.                 e.printStackTrace();  
  34.             } finally {  
  35.                 try {  
  36.                     qprojectInPutStream.close();  
  37.                 } catch (Exception e) {  
  38.                     e.printStackTrace();  
  39.                 }  
  40.                 try {  
  41.                     trustInputStream.close();  
  42.                 } catch (Exception e) {  
  43.                     e.printStackTrace();  
  44.                 }  
  45.             }  
  46.   
  47.   
  48.             //初始化SSLContext上下文对象  
  49.             sslContext = SSLContext.getInstance("TLS");  
  50.             TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());  
  51.             trustManagerFactory.init(trustKeyStore);  
  52.             KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("X509");  
  53.             keyManagerFactory.init(qprojectKeyStore, KEY_STORE_PASSWORD.toCharArray());  
  54.             sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);  
  55.         } catch (Exception e) {  
  56.             e.printStackTrace();  
  57.         }  
  58.         return sslContext;  
  59.     }  
  60.   
  61.   
  62.     /** 
  63.      * 获取HttpsURLConnection 
  64.      * 
  65.      * @param context 上下文 
  66.      * @param url     连接url 
  67.      * @param method  请求方式 
  68.      * @return HttpsURLConnection 
  69.      */  
  70.     public static HttpsURLConnection getHttpsURLConnection(Context context, String url, String method) {  
  71.         URL u;  
  72.         HttpsURLConnection connection = null;  
  73.         try {  
  74.             SSLContext sslContext = getSSLContext(context);  
  75.             if (sslContext != null) {  
  76.                 u = new URL(url);  
  77.                 connection = (HttpsURLConnection) u.openConnection();  
  78.                 connection.setRequestMethod(method);//"POST" "GET"  
  79.                 connection.setDoOutput(true);  
  80.                 connection.setDoInput(true);  
  81.                 connection.setUseCaches(false);  
  82.                 connection.setSSLSocketFactory(sslContext.getSocketFactory());  
  83.                 connection.setConnectTimeout(30000);  
  84.                 //忽略请求域名和证书域名的校验  
  85.                 connection.setDefaultHostnameVerifier( new HostnameVerifier(){  
  86.                     public boolean verify(String string,SSLSession ssls) {  
  87.                         return true;  
  88.                     }  
  89.                 });  
  90.             }  
  91.         } catch (Exception e) {  
  92.             e.printStackTrace();  
  93.         }  
  94.         return connection;  
  95.     }  
  96. }  
NextActivity.java:修改获取HttpsURLConnection的方式,和域名https,其它不变
[java] view plain copy
  1. public class NextActivity extends AppCompatActivity {  
  2.     ... ...  
  3.   
  4.     @Override  
  5.     protected void onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         ... ...  
  8.     }  
  9.   
  10.     /** 
  11.      * 执行登录Get请求 
  12.      * @param userName 用户名 
  13.      * @param passWorld 用户密码 
  14.      */  
  15.     private void doLoginGet(String userName, String passWorld) {  
  16.         try {  
  17.             //修改获取HttpsURLConnection的方式,和域名https  
  18.             String url = "https://192.168.1.103:8443/qserver/HttpsServlet?userName=" + userName + "&passWord=" + passWorld;  
  19.             HttpsURLConnection httpsURLConnection = HttpsHelper.getHttpsURLConnection(this,url,"GET");  
  20.             ... ...   
  21.     }  
  22.   
  23.     /** 
  24.      * 执行登录Post请求 
  25.      * @param userName 用户名 
  26.      * @param passWorld 用户密码 
  27.      */  
  28.     private void doLoginPost(String userName, String passWorld) {  
  29.         ... ...  
  30.     }  
  31. }  
提示1:在请求https接口的时候,报错如下:
11-06 17:53:36.244 32662-5351/com.qunar.home W/System.err: java.io.IOException: Wrong version of key store.
11-06 17:53:36.245 32662-5351/com.qunar.home W/System.err:     at com.android.org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi.engineLoad(BcKeyStoreSpi.java:805)
11-06 17:53:36.245 32662-5351/com.qunar.home W/System.err:     at java.security.KeyStore.load(KeyStore.java:590)
11-06 17:53:36.245 32662-5351/com.qunar.home W/System.err:     at com.qunar.hotel.ssl.HttpsHelper.getSSLContext(HttpsHelper.java:46)
11-06 17:53:36.245 32662-5351/com.qunar.home W/System.err:     at com.qunar.hotel.ssl.HttpsHelper.getHttpsURLConnection(HttpsHelper.java:87)
11-06 17:53:36.245 32662-5351/com.qunar.home W/System.err:     at com.qunar.hotel.NextActivity.doLoginGet(NextActivity.java:83)
11-06 17:53:36.245 32662-5351/com.qunar.home W/System.err:     at com.qunar.hotel.NextActivity.access$300(NextActivity.java:27)
11-06 17:53:36.245 32662-5351/com.qunar.home W/System.err:     at com.qunar.hotel.NextActivity$2$1.run(NextActivity.java:67)
11-06 17:53:36.245 32662-5351/com.qunar.home W/System.err:     at java.lang.Thread.run(Thread.java:818)
处理:android系统只支持JKS默认证书格式,支持BKS,故生成客户端证书的时候使用DKS格式生成,见上面章节;

提示2:在请求https接口的时候,报错如下:
11-06 18:52:53.154 21260-23016/com.qunar.home W/System.err: java.io.IOException: Hostname '192.168.1.103' was not verified
11-06 18:52:53.154 21260-23016/com.qunar.home W/System.err:     at com.android.okhttp.Connection.upgradeToTls(Connection.java:205)
11-06 18:52:53.155 21260-23016/com.qunar.home W/System.err:     at com.android.okhttp.Connection.connect(Connection.java:155)
11-06 18:52:53.155 21260-23016/com.qunar.home W/System.err:     at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:282)
11-06 18:52:53.155 21260-23016/com.qunar.home W/System.err:     at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:216)
11-06 18:52:53.155 21260-23016/com.qunar.home W/System.err:     at com.android.okhttp.internal.http.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:391)
11-06 18:52:53.155 21260-23016/com.qunar.home W/System.err:     at com.android.okhttp.internal.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:341)
11-06 18:52:53.155 21260-23016/com.qunar.home W/System.err:     at com.android.okhttp.internal.http.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:509)
11-06 18:52:53.155 21260-23016/com.qunar.home W/System.err:     at com.android.okhttp.internal.http.DelegatingHttpsURLConnection.getResponseCode(DelegatingHttpsURLConnection.java:105)
11-06 18:52:53.155 21260-23016/com.qunar.home W/System.err:     at com.android.okhttp.internal.http.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:25)
11-06 18:52:53.155 21260-23016/com.qunar.home W/System.err:     at com.qunar.hotel.NextActivity.doLoginGet(NextActivity.java:84)
11-06 18:52:53.155 21260-23016/com.qunar.home W/System.err:     at com.qunar.hotel.NextActivity.access$300(NextActivity.java:27)
11-06 18:52:53.155 21260-23016/com.qunar.home W/System.err:     at com.qunar.hotel.NextActivity$2$1.run(NextActivity.java:67)
11-06 18:52:53.155 21260-23016/com.qunar.home W/System.err:     at java.lang.Thread.run(Thread.java:818)
处理:由于我们在测试环境,故请求url中使用了服务器的ip,到时和证书中的 CN(Common Name名字与姓氏)不匹配(在正式的环境不会有该问题)。那么我们默认跳过该认证或者在正式线使用线上的域名访问:
//忽略请求域名和证书域名的校验
connection.setDefaultHostnameVerifier( new HostnameVerifier(){
     public boolean verify(String string,SSLSession ssls) {
          return true;
    }
});


5.tomcat修改

原来的connector配置只有加解密,现在加上服务端身份验证相关配置,修改server.xml文件如下:
[html] view plain copy
  1. <Connector protocol="org.apache.coyote.http11.Http11NioProtocol" port="8443" maxThreads="200" scheme="https"  
  2.                secure="true" SSLEnabled="true" keystoreFile="D:\ssl\tomcat.keystore" keystorePass="123456"  
  3.                clientAuth="true" sslProtocol="TLS" truststoreFile="D:\ssl\tomcat.keystore" truststorePass="123456"/>  
  clientAuth:启动客户端身份验证;
  truststoreFile:服务端信任的客户端证书库;
  truststorePass:信任证书库密码;

6.测试Https接口

安装app,并重新启动tomcat服务,使用Fiddler抓取https请求使用加密传输!


7.代码库
QProject:https://github.com/Pengchengxiang/QProject 分支:feature/https
QServer:https://github.com/Pengchengxiang/QServer 分支:feature/https

原创粉丝点击