您现在的位置是:首页 > 文章详情

安卓应用安全指南 5.4.1 通过 HTTPS 的通信 示例代码

日期:2018-03-31点击:377

5.4.1 通过 HTTPS 的通信 示例代码

原书:Android Application Secure Design/Secure Coding Guidebook

译者:飞龙

协议:CC BY-NC-SA 4.0

你可以通过下面的图表(图 5.4-1)找出你应该实现的 HTTP / HTTPS 通信类型。

当发送或接收敏感信息时,将使用 HTTPS 通信,因为其通信通道使用 SSL / TLS 加密。 以下敏感信息需要 HTTPS 通信。

  • Web 服务的登录 ID /密码。
  • 保持认证状态的信息(会话 ID,令牌,Cookie 等)
  • 取决于 Web 服务的重要/机密信息(个人信息,信用卡信息等)

具有网络通信的智能手机应用是“系统”和 Web 服务器的一部分。 而且你必须根据整个“系统”的安全设计和编码,为每个通信选择 HTTP 或 HTTPS。 表 5.4-1 用于比较 HTTP 和 HTTPS。 表 5.4-2 是示例代码的差异。

表 5.4-1 HTTP 与 HTTPS 通信方式的比较

HTTP HTTPS
特性 URL
加密内容
内容的篡改检测
对服务器进行认证
损害的风险 由攻击者读取内容
由攻击者修改内容
应用访问了伪造的服务器

表 5.4-2 HTTP/HTTPS 通信示例代码的解释

示例代码 通信 收发敏感信息 服务器证书
通过 HTTP 的通信 HTTP 不适用 -
通过 HTTPS 的通信 HTTPS OK 服务器证书由可信第三方机构签署,例如 Cybertrust 和 VeriSign
通过 HTTPS 使用私有证书的通信 HTTTPS OK 私有证书(经常能在内部服务器或测试服务器上看到的操作)

Android 支持java.net.HttpURLConnection / javax.net.ssl.HttpsURLConnection作为 HTTP / HTTPS 通信 API。 在 Android 6.0(API Level 23)版本中,另一个 HTTP 客户端库 Apache HttpClient 的支持已被删除。

5.4.1.1 通过 HTTP 进行通信

它基于两个前提,即通过 HTTP 通信发送/接收的所有内容都可能被攻击者嗅探和篡改,并且你的目标服务器可能被攻击者准备的假服务器替换。 只有在没有造成损害或损害在允许范围内的情况下,才能使用 HTTP 通信,即使在本地也是如此。 如果应用无法接受该前提,请参阅“5.4.1.2 通过 HTTPS 进行通信”和“5.4.1.3 通过 HTTPS 使用私有证书进行通信”。

以下示例代码显示了一个应用,它在 Web 服务器上执行图像搜索,获取结果图像并显示它。与服务器的 HTTP 通信在搜索时执行两次。第一次通信是搜索图像数据,第二次是获取它。它使用AsyncTask创建用于通信过程的工作线程,来避免在 UI 线程上执行通信。与服务器的通信中发送/接收的内容,在这里不被认为是敏感的(例如,用于搜索的字符串,图像的 URL 或图像数据)。因此,接收到的数据,如图像的 URL 和图像数据,可能由攻击者提供。为了简单地显示示例代码,在示例代码中没有采取任何对策,通过将接收到的攻击数据视为可容忍的。此外,在 JSON 解析或显示图像数据期间,可能出现异常的处理将被忽略。根据应用规范,有必要正确处理例外情况。

要点:

  1. 发送的数据中不得包含敏感信息。
  2. 假设收到的数据可能来自攻击者。

HttpImageSearch.java

package org.jssec.android.https.imagesearch; import android.os.AsyncTask; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; public abstract class HttpImageSearch extends AsyncTask<String, Void, Object> { @Override protected Object doInBackground(String... params) { byte[] responseArray; // -------------------------------------------------------- // Communication 1st time: Execute image search // -------------------------------------------------------- // *** POINT 1 *** Sensitive information must not be contained in send data. // Send image search character string StringBuilder s = new StringBuilder(); for (String param : params){ s.append(param); s.append('+'); } s.deleteCharAt(s.length() - 1); String search_url = "http://ajax.googleapis.com/ajax/services/search/images?v=1.0&q=" + s.toString(); responseArray = getByteArray(search_url); if (responseArray == null) { return null; } // *** POINT 2 *** Suppose that received data may be sent from attackers. // This is sample, so omit the process in case of the searching result is the data from an attacker. // This is sample, so omit the exception process in case of JSON purse. String image_url; try { String json = new String(responseArray); image_url = new JSONObject(json).getJSONObject("responseData") .getJSONArray("results").getJSONObject(0).getString("url"); } catch(JSONException e) { return e; } // -------------------------------------------------------- // Communication 2nd time: Get images // -------------------------------------------------------- // *** POINT 1 *** Sensitive information must not be contained in send data. if (image_url != null ) { responseArray = getByteArray(image_url); if (responseArray == null) { return null; } } // *** POINT 2 *** Suppose that received data may be sent from attackers. return responseArray; } private byte[] getByteArray(String strUrl) { byte[] buff = new byte[1024]; byte[] result = null; HttpURLConnection response; BufferedInputStream inputStream = null; ByteArrayOutputStream responseArray = null; int length; try { URL url = new URL(strUrl); response = (HttpURLConnection) url.openConnection(); response.setRequestMethod("GET"); response.connect(); checkResponse(response); inputStream = new BufferedInputStream(response.getInputStream()); responseArray = new ByteArrayOutputStream(); while ((length = inputStream.read(buff)) != -1) { if (length > 0) { responseArray.write(buff, 0, length); } } result = responseArray.toByteArray(); } catch (IOException e) { e.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { // This is sample, so omit the exception process } } if (responseArray != null) { try { responseArray.close(); } catch (IOException e) { // This is sample, so omit the exception process } } } return result; } private void checkResponse(HttpURLConnection response) throws IOException { int statusCode = response.getResponseCode(); if (HttpURLConnection.HTTP_OK != statusCode) { throw new IOException("HttpStatus: " + statusCode); } } }

ImageSearchActivity.java

package org.jssec.android.https.imagesearch; import android.app.Activity; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.os.Bundle; import android.view.View; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; public class ImageSearchActivity extends Activity { private EditText mQueryBox; private TextView mMsgBox; private ImageView mImgBox; private AsyncTask<String, Void, Object> mAsyncTask ; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mQueryBox = (EditText)findViewById(R.id.querybox); mMsgBox = (TextView)findViewById(R.id.msgbox); mImgBox = (ImageView)findViewById(R.id.imageview); } @Override protected void onPause() { // After this, Activity may be deleted, so cancel the asynchronization process in advance. if (mAsyncTask != null) mAsyncTask.cancel(true); super.onPause(); } public void onHttpSearchClick(View view) { String query = mQueryBox.getText().toString(); mMsgBox.setText("HTTP:" + query); mImgBox.setImageBitmap(null); // Cancel, since the last asynchronous process might not have been finished yet. if (mAsyncTask != null) mAsyncTask.cancel(true); // Since cannot communicate by UI thread, communicate by worker thread by AsynchTask. mAsyncTask = new HttpImageSearch() { @Override protected void onPostExecute(Object result) { // Process the communication result by UI thread. if (result == null) { mMsgBox.append("¥nException occurs¥n"); } else if (result instanceof Exception) { Exception e = (Exception)result; mMsgBox.append("¥nException occurs¥n" + e.toString()); } else { // Exception process when image display is omitted here, since it's sample. byte[] data = (byte[])result; Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length); mImgBox.setImageBitmap(bmp); } } }.execute(query); // pass search character string and start asynchronous process } public void onHttpsSearchClick(View view) { String query = mQueryBox.getText().toString(); mMsgBox.setText("HTTPS:" + query); mImgBox.setImageBitmap(null); // Cancel, since the last asynchronous process might not have been finished yet. if (mAsyncTask != null) mAsyncTask.cancel(true); // Since cannot communicate by UI thread, communicate by worker thread by AsynchTask. mAsyncTask = new HttpsImageSearch() { @Override protected void onPostExecute(Object result) { // Process the communication result by UI thread. if (result instanceof Exception) { Exception e = (Exception)result; mMsgBox.append("¥nException occurs¥n" + e.toString()); } else { byte[] data = (byte[])result; Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length); mImgBox.setImageBitmap(bmp); } } }.execute(query); // pass search character string and start asynchronous process } }

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.jssec.android.https.imagesearch" android:versionCode="1" android:versionName="1.0"> <uses-permission android:name="android.permission.INTERNET"/> <application  android:icon="@drawable/ic_launcher" android:allowBackup="false" android:label="@string/app_name" > <activity  android:name=".ImageSearchActivity" android:label="@string/app_name" android:theme="@android:style/Theme.Light" android:exported="true" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>

5.4.1.2 使用 HTTPS 进行通信

在 HTTPS 通信中,检查服务器是否可信,以及传输的数据是否加密。 为了验证服务器,Android HTTPS 库验证“服务器证书”,它在 HTTPS 事务的握手阶段从服务器传输,其要点如下:

  • 服务器证书由可信的第三方证书机构签署
  • 服务器证书的期限和其他属性有效
  • 服务器的主机名匹配服务器证书的主题字段中的 CN(通用名称)或 SAN(主题备用名称)

如果上述验证失败,则会引发SSLException(服务器证书验证异常)。 这可能意味着中间人攻击或服务器证书缺陷。 你的应用必须根据应用规范,以适当的顺序处理异常。

下一个示例代码用于 HTTPS 通信,它使用可信的第三方证书机构颁发的服务器证书连接到 Web 服务器。 对于使用私有服务器证书的 HTTPS 通信,请参阅“5.4.1.3 通过 HTTPS 使用私有证书进行通信”。

以下示例代码展示了一个应用,它在 Web 服务器上执行图像搜索,获取结果图像并显示它。 与服务器的 HTTPS 通信在搜索时执行两次。 第一次通信是搜索图像数据,第二次是获取它。 它使用AsyncTask创建用于通信过程的工作线程,来避免在 UI 线程上执行通信。 与服务器的通信中发送/接收的所有内容,在这里被认为是敏感的(例如,用于搜索的字符串,图像的 URL 或图像数据)。 为了简单地显示示例代码,不会执行针对SSLException的特殊处理。 根据应用规范,有必要正确处理异常。 另外,下面的示例代码允许使用 SSLv3 进行通信。 通常,我们建议配置远程服务器上的设置来禁用 SSLv3,以避免针对 SSLv3 中的漏洞(称为 POODLE)的攻击。

要点:

  1. URI 以https://开头。
  2. 发送数据中可能包含敏感信息。
  3. 尽管数据是从通过 HTTPS 连接的服务器发送的,但要小心并安全地处理收到的数据。
  4. SSLException应该在应用中以适当的顺序处理。

HttpsImageSearch.java

package org.jssec.android.https.imagesearch; import org.json.JSONException; import org.json.JSONObject; import android.os.AsyncTask; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; public abstract class HttpsImageSearch extends AsyncTask<String, Void, Object> { @Override protected Object doInBackground(String... params) { byte[] responseArray; // -------------------------------------------------------- // Communication 1st time : Execute image search // -------------------------------------------------------- // *** POINT 1 *** URI starts with https://. // *** POINT 2 *** Sensitive information may be contained in send data. StringBuilder s = new StringBuilder(); for (String param : params){ s.append(param); s.append('+'); } s.deleteCharAt(s.length() - 1); String search_url = "https://ajax.googleapis.com/ajax/services/search/images?v=1.0&q=" + s.toString(); responseArray = getByteArray(search_url); if (responseArray == null) { return null; } // *** POINT 3 *** Handle the received data carefully and securely, // even though the data was sent from the server connected by HTTPS. // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." String image_url; try { String json = new String(responseArray); image_url = new JSONObject(json).getJSONObject("responseData") .getJSONArray("results").getJSONObject(0).getString("url"); } catch(JSONException e) { return e; } // -------------------------------------------------------- // Communication 2nd time : Get image // -------------------------------------------------------- // *** POINT 1 *** URI starts with https://. // *** POINT 2 *** Sensitive information may be contained in send data. if (image_url != null ) { responseArray = getByteArray(image_url); if (responseArray == null) { return null; } } return responseArray; } private byte[] getByteArray(String strUrl) { byte[] buff = new byte[1024]; byte[] result = null; HttpURLConnection response; BufferedInputStream inputStream = null; ByteArrayOutputStream responseArray = null; int length; try { URL url = new URL(strUrl); response = (HttpURLConnection) url.openConnection(); response.setRequestMethod("GET"); response.connect(); checkResponse(response); inputStream = new BufferedInputStream(response.getInputStream()); responseArray = new ByteArrayOutputStream(); while ((length = inputStream.read(buff)) != -1) { if (length > 0) { responseArray.write(buff, 0, length); } } result = responseArray.toByteArray(); } catch (IOException e) { e.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { // This is sample, so omit the exception process } } if (responseArray != null) { try { responseArray.close(); } catch (IOException e) { // This is sample, so omit the exception process } } } return result; } private void checkResponse(HttpURLConnection response) throws IOException { int statusCode = response.getResponseCode(); if (HttpURLConnection.HTTP_OK != statusCode) { throw new IOException("HttpStatus: " + statusCode); } } }

其他示例代码文件与“5.4.1.1 通过 HTTP 进行通信”相同,因此请参阅“5.4.1.1 通过 HTTP 进行通信”。

5.4.1.3 使用私有证书通过 HTTPS 进行通信

这部分展示了一个 HTTPS 通信的示例代码,其中包含私人颁发的服务器证书(私有证书),但不是可信的第三方机构颁发的服务器证书。 请参阅“5.4.3.1 如何创建私有证书并配置服务器”,来创建私有证书机构和私有证书的根证书,并在 Web 服务器中设置 HTTPS。 示例程序的资产中包含cacert.crt文件。 它是私有证书机构的根证书文件。

以下示例代码展示了一个应用,在 Web 服务器上获取图像并显示该图像。 HTTPS 用于与服务器的通信。 它使用AsyncTask创建用于通信过程的工作线程,来避免在 UI 线程上执行通信。 与服务器的通信中发送/接收的所有内容(图像的 URL 和图像数据)都被认为是敏感的。 为了简单地显示示例代码,不会执行针对SSLException的特殊处理。 根据应用规范,有必要正确处理异常。

要点:

  1. 使用私人证书机构的根证书来验证服务器证书。
  2. URI 以https://开头。
  3. 发送数据中可能包含敏感信息。
  4. 接收的数据可以像服务器一样被信任。
  5. SSLException应该在应用中以适当的顺序处理。

PrivateCertificathettpsGet.java

package org.jssec.android.https.privatecertificate; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.security.KeyStore; import java.security.SecureRandom; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManagerFactory; import android.content.Context; import android.os.AsyncTask; public abstract class PrivateCertificateHttpsGet extends AsyncTask<String, Void, Object> { private Context mContext; public PrivateCertificateHttpsGet(Context context) { mContext = context; } @Override protected Object doInBackground(String... params) { TrustManagerFactory trustManager; BufferedInputStream inputStream = null; ByteArrayOutputStream responseArray = null; byte[] buff = new byte[1024]; int length; try { URL url = new URL(params[0]); // *** POINT 1 *** Verify a server certificate with the root certificate of a private certificate authority. // Set keystore which includes only private certificate that is stored in assets, to client. KeyStore ks = KeyStoreUtil.getEmptyKeyStore(); KeyStoreUtil.loadX509Certificate(ks, mContext.getResources().getAssets().open("cacert.crt")); // *** POINT 2 *** URI starts with https://. // *** POINT 3 *** Sensitive information may be contained in send data. trustManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManager.init(ks); SSLContext sslCon = SSLContext.getInstance("TLS"); sslCon.init(null, trustManager.getTrustManagers(), new SecureRandom()); HttpURLConnection con = (HttpURLConnection)url.openConnection(); HttpsURLConnection response = (HttpsURLConnection)con; response.setDefaultSSLSocketFactory(sslCon.getSocketFactory()); response.setSSLSocketFactory(sslCon.getSocketFactory()); checkResponse(response); // *** POINT 4 *** Received data can be trusted as same as the server. inputStream = new BufferedInputStream(response.getInputStream()); responseArray = new ByteArrayOutputStream(); while ((length = inputStream.read(buff)) != -1) { if (length > 0) { responseArray.write(buff, 0, length); } } return responseArray.toByteArray(); } catch(SSLException e) { // *** POINT 5 *** SSLException should be handled with an appropriate sequence in an application. // Exception process is omitted here since it's sample. return e; } catch(Exception e) { return e; } finally { if (inputStream != null) { try { inputStream.close(); } catch (Exception e) { // This is sample, so omit the exception process } } if (responseArray != null) { try { responseArray.close(); } catch (Exception e) { // This is sample, so omit the exception process } } } } private void checkResponse(HttpURLConnection response) throws IOException { int statusCode = response.getResponseCode(); if (HttpURLConnection.HTTP_OK != statusCode) { throw new IOException("HttpStatus: " + statusCode); } } }

KeyStoreUtil.java

package org.jssec.android.https.privatecertificate; import java.io.IOException; import java.io.InputStream; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.Enumeration; public class KeyStoreUtil { public static KeyStore getEmptyKeyStore() throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { KeyStore ks = KeyStore.getInstance("BKS"); ks.load(null); return ks; } public static void loadAndroidCAStore(KeyStore ks) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { KeyStore aks = KeyStore.getInstance("AndroidCAStore"); aks.load(null); Enumeration<String> aliases = aks.aliases(); while (aliases.hasMoreElements()) { String alias = aliases.nextElement(); Certificate cert = aks.getCertificate(alias); ks.setCertificateEntry(alias, cert); } } public static void loadX509Certificate(KeyStore ks, InputStream is) throws CertificateException, KeyStoreException { try { CertificateFactory factory = CertificateFactory.getInstance("X509"); X509Certificate x509 = (X509Certificate)factory.generateCertificate(is); String alias = x509.getSubjectDN().getName(); ks.setCertificateEntry(alias, x509); } finally { try { is.close(); } catch (IOException e) { /* This is sample, so omit the exception process */ } } } }

PrivateCertificateHttpsActivity.java

package org.jssec.android.https.privatecertificate; import android.app.Activity; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.os.Bundle; import android.view.View; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; public class PrivateCertificateHttpsActivity extends Activity { private EditText mUrlBox; private TextView mMsgBox; private ImageView mImgBox; private AsyncTask<String, Void, Object> mAsyncTask ; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mUrlBox = (EditText)findViewById(R.id.urlbox); mMsgBox = (TextView)findViewById(R.id.msgbox); mImgBox = (ImageView)findViewById(R.id.imageview); } @Override protected void onPause() { // After this, Activity may be discarded, so cancel asynchronous process in advance. if (mAsyncTask != null) mAsyncTask.cancel(true); super.onPause(); } public void onClick(View view) { String url = mUrlBox.getText().toString(); mMsgBox.setText(url); mImgBox.setImageBitmap(null); // Cancel, since the last asynchronous process might have not been finished yet. if (mAsyncTask != null) mAsyncTask.cancel(true); // Since cannot communicate through UI thread, communicate by worker thread by AsynchTask. mAsyncTask = new PrivateCertificateHttpsGet(this) { @Override protected void onPostExecute(Object result) { // Process the communication result through UI thread. if (result instanceof Exception) { Exception e = (Exception)result; mMsgBox.append("¥nException occurs¥n" + e.toString()); } else { byte[] data = (byte[])result; Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length); mImgBox.setImageBitmap(bmp); } } }.execute(url); // Pass URL and start asynchronization process } }
原文链接:https://yq.aliyun.com/articles/595245
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章