Android Socket與HTTPS校驗

在Android中使用HTTPS的場景比較頻繁,所以對于HTTPS的證書應該如何校驗呢?關于HTTPS的校驗原理可以參考我之前寫的一篇文章:《 HTTPS協(xié)議實現原理 》,相信看完后應該對HTTPS有一個比較大致的了解。而且對HTTP(s)請求的工具進行了封裝,需要體會這種封裝工具類的思路,也就是編碼中常見的Listener機制。然后是Android中TCP、UDP通信的例子,主要是把Android設備作為Client端,如果對Java的Socket編程比較熟悉的話,這些都是特別簡單的示例程序,非常容易看懂。

TCP/UDP 簡單示例

下面的例子演示了Client向Server發(fā)送了一串小寫英文,Server返回大寫字符串的功能:

UDPServer.java:

public class UDPServer {
    private static final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
    public static void main(String[] args) throws Exception {
        DatagramSocket datagramSocket;
        datagramSocket = new DatagramSocket(8090);
        byte[] buf;
        DatagramPacket packet;
        while (true){
            buf = new byte[1024];
            packet = new DatagramPacket(buf, buf.length);
            datagramSocket.receive(packet);
            String content = new String(packet.getData());
            InetAddress address = packet.getAddress();
            System.out.println(format.format(new Date()) + "-" + address + "-" + content);
            int port = packet.getPort();
            String replyContent = content.toUpperCase();
            byte[] sendData = replyContent.getBytes();
            DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, address, port);
            datagramSocket.send(sendPacket);
        }
    }
}

UDPClient.java:

public class UDPClient {
    private static final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
    public static void main(String[] args) throws Exception {
        System.out.println("請輸入一句英文,服務器會返回其大寫形式[exit退出]");
        Scanner scanner = new Scanner(System.in);
        InetAddress address = InetAddress.getLocalHost();
        DatagramPacket packet;
        DatagramSocket socket = new DatagramSocket();
        while(true){
            String line = scanner.nextLine();
            if("exit".equals(line)) break;
            byte[] bytes = line.getBytes();
            packet = new DatagramPacket(bytes, bytes.length, address, 8090);
            socket.send(packet);
            byte[] recvBuf = new byte[1024];
            DatagramPacket recvPacket = new DatagramPacket(recvBuf, recvBuf.length);
            socket.receive(recvPacket);
            System.out.println(format.format(new Date()) + "-" + address + "-" + new String(recvBuf));
        }
        socket.close();
    }
}

TCPServer.java:

public class TCPServer {
    static SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9090);
        while (true){
            Socket socket = serverSocket.accept();
            InetAddress address = socket.getInetAddress();
            InputStream is = socket.getInputStream();
            byte[] readBuf = new byte[1024];
            try{
                int len = is.read(readBuf);
                String recv = new String(readBuf, 0, len);
                System.out.println(format.format(new Date()) + "-" + address + "-" + recv);
                OutputStream os = socket.getOutputStream();
                os.write(recv.toUpperCase().getBytes());
            } catch (SocketException e){
                System.err.println("客戶端未發(fā)送信息");
            } finally {
                socket.close();
            }
        }
    }
}

TCPClient.java:

public class TCPClient {
    private static final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
    public static void main(String[] args) throws Exception {
        System.out.println("請輸入一句英文,服務器會返回其大寫形式[exit退出]");
        Scanner scanner = new Scanner(System.in);
        while(true){
            Socket socket = new Socket("127.0.0.1", 9090);
            String line = scanner.nextLine();
            if("exit".equals(line)) break;
            OutputStream os = socket.getOutputStream();
            os.write(line.getBytes());
            InputStream is = socket.getInputStream();
            byte[] readBuf = new byte[1024];
            String recv = new String(readBuf, 0, is.read(readBuf));
            InetAddress address = socket.getInetAddress();
            System.out.println(format.format(new Date()) + "-" + address + "-" + recv);
            socket.close();
        }
    }
}

Client移植到Android

將兩個Client移植到Android:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="10dp"
    tools:context=".MainActivity">

    <EditText
        android:hint="輸入發(fā)送內容"
        android:id="@+id/et_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <EditText
            android:text="192.168.1.113:8090"
            android:id="@+id/et_udp_server"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content">
        </EditText>
        <Button
            android:text="UDP發(fā)送"
            android:onClick="sendUdpMessage"
            android:layout_weight="1"
            android:layout_width="0dp"
            android:layout_height="wrap_content"/>
    </LinearLayout>
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <EditText
            android:text="192.168.1.113:9090"
            android:id="@+id/et_tcp_server"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content">
        </EditText>
        <Button
            android:text="TCP發(fā)送"
            android:onClick="sendTcpMessage"
            android:layout_weight="1"
            android:layout_width="0dp"
            android:layout_height="wrap_content"/>
    </LinearLayout>
    <TextView
        android:id="@+id/tv_show"
        android:text="收到回復:"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>

MainActivity.java:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private static final SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss", Locale.CHINA);
    private EditText etInput;
    private TextView textView;
    private EditText udpServerET;
    private EditText tcpServerET;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        etInput = findViewById(R.id.et_content);
        textView = findViewById(R.id.tv_show);
        udpServerET = findViewById(R.id.et_udp_server);
        tcpServerET = findViewById(R.id.et_tcp_server);
    }

    public void sendTcpMessage(View view) {
        String[] tcpInfo = tcpServerET.getText().toString().split(":");
        String inputContent = etInput.getText().toString();
        new Thread(()->{
            try (Socket socket = new Socket(tcpInfo[0], Integer.parseInt(tcpInfo[1]))){
                OutputStream os = socket.getOutputStream();
                os.write(inputContent.getBytes());
                InputStream is = socket.getInputStream();
                byte[] readBuf = new byte[1024];
                String recv = new String(readBuf, 0, is.read(readBuf));
                InetAddress address = socket.getInetAddress();
                String ret = String.format("%s-%s-%s", df.format(new Date()), address, recv);
                runOnUiThread(()-> textView.setText(ret));
            }catch (IOException e){
                Log.e(TAG, "sendTcpMessage: Error!");
            }
        }).start();
    }

    public void sendUdpMessage(View view) {
        String[] udpInfo = udpServerET.getText().toString().split(":");
        String inputContent = etInput.getText().toString();
        new Thread(()->{
            try {
                DatagramSocket socket = new DatagramSocket();
                byte[] bytes = inputContent.getBytes();
                InetAddress address = InetAddress.getByName(udpInfo[0]);
                int serverPort = Integer.parseInt(udpInfo[1]);
                DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, serverPort);
                socket.send(packet);
                byte[] recvBuf = new byte[1024];
                DatagramPacket recvPacket = new DatagramPacket(recvBuf, recvBuf.length);
                socket.receive(recvPacket);
                String ret = String.format("%s-%s-%s", df.format(new Date()), address, new String(recvBuf));
                runOnUiThread(()-> textView.setText(ret));
            }catch (IOException e){
                Log.e(TAG, "sendUdpMessage: Error!");
            }
        }).start();
    }
}

AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET"/>
image

注意點:1、網絡訪問權限 2、子線程代碼中使用runOnUiThread()方法可更新UI

Android訪問HTTPS

對于一個普通的HTTP請求,我們可以使用如下方式來發(fā)起請求,下面是一個簡易的Http請求工具類:

public class HttpUtils {
    private static Handler mUIHandler = new Handler(Looper.getMainLooper());

    interface HttpListener {
        void onSuccess(String content);

        void onFail(Exception e);
    }

    public static void doGet(String urlStr, HttpListener listener) {
        new Thread(() -> {
            Looper.prepare();
            try {
                URL url = new URL(urlStr);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("GET");
                conn.setConnectTimeout(5000);
                conn.setReadTimeout(5000);
                conn.connect();

                try (InputStream is = conn.getInputStream();
                     InputStreamReader reader = new InputStreamReader(is)
                ) {
                    char[] buf = new char[4096];
                    int len;
                    StringBuilder sb = new StringBuilder();
                    while ((len = reader.read(buf)) != -1) {
                        sb.append(new String(buf, 0, len));
                    }
                    mUIHandler.post(() -> listener.onSuccess(sb.toString()));
                } catch (IOException e) {
                    e.printStackTrace();
                    listener.onFail(e);
                }
            }catch (IOException e){
                e.printStackTrace();
                listener.onFail(e);
            }
        }).start();
    }
}

1、不校驗證書(不推薦)

MyX509TrustManager.java,MyX509TrustManager實現不做任何事情:

...
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.X509TrustManager;

public class MyX509TrustManager implements X509TrustManager {

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        // TODO...
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        // TODO...
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }
}

HttpsUtils.java

...
public class HttpsUtils {
    private static Handler mUIHandler = new Handler(Looper.getMainLooper());

    interface HttpListener {
        void onSuccess(String content);

        void onFail(Exception e);
    }

    public static void doGet(Context context, String urlStr, HttpListener listener) {
        new Thread(() -> {
            Looper.prepare();
            try {
                URL url = new URL(urlStr);
                HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
                SSLContext sslContext = SSLContext.getInstance("TLS");
                // 放入自定義的MyX509TrustManager對象即可
                TrustManager[] trustManagers = {new MyX509TrustManager()};
                sslContext.init(null, trustManagers, new SecureRandom());
                conn.setSSLSocketFactory(sslContext.getSocketFactory());
                conn.setRequestMethod("GET");
                conn.setConnectTimeout(5000);
                conn.setReadTimeout(5000);
                conn.connect();

                try (InputStream is = conn.getInputStream();
                     InputStreamReader reader = new InputStreamReader(is)
                ) {
                    char[] buf = new char[4096];
                    int len;
                    StringBuilder sb = new StringBuilder();
                    while ((len = reader.read(buf)) != -1) {
                        sb.append(new String(buf, 0, len));
                    }
                    mUIHandler.post(() -> listener.onSuccess(sb.toString()));
                } catch (IOException e) {
                    e.printStackTrace();
                    listener.onFail(e);
                }
            }catch (Exception e){
                e.printStackTrace();
                listener.onFail(e);
            }
        }).start();
    }
}

2、校驗證書(推薦)

拿我自己的博客站點來說,想要獲得證書只需要在瀏覽器下載對應的證書即可(選擇DER編碼二進制和Base64編碼均可),保存了一個名為srca.cer的文件到桌面:

image

將這份證書文件復制到項目的src/main/assets/目錄下,沒有assets就新建,所以完整路徑為src/main/assets/srca.cer。

接下來需要實現MyX509TrustManager.java中的方法:

public class MyX509TrustManager implements X509TrustManager {
    private static final String TAG = "MyX509TrustManager";
    // 證書對象
    private X509Certificate serverCert;

    public MyX509TrustManager(X509Certificate serverCert) {
        this.serverCert = serverCert;
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        // 遍歷證書
        for (X509Certificate certificate: chain){
            // 校驗合法性與是否過期
            certificate.checkValidity();
            try {
                // 校驗公鑰
                PublicKey publicKey = serverCert.getPublicKey();
                certificate.verify(publicKey);
            } catch (Exception e) {
                throw new CertificateException(e);
            }
        }
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }
}

同時,將使用keyStore這個API來獲取TrustManager數組,HttpsUtils.java如下:

public class Https2Utils {
    private static Handler mUIHandler = new Handler(Looper.getMainLooper());

    interface HttpListener {
        void onSuccess(String content);

        void onFail(Exception e);
    }

    public static void doGet(Context context, String urlStr, HttpListener listener) {
        new Thread(() -> {
            Looper.prepare();
            try {
                URL url = new URL(urlStr);
                HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
                SSLContext sslContext = SSLContext.getInstance("TLS");
                X509Certificate serverCert = getCert(context);
                String defaultType = KeyStore.getDefaultType();
                KeyStore keyStore = KeyStore.getInstance(defaultType);
                keyStore.load(null);
                // 別名、證書
                keyStore.setCertificateEntry("srca", serverCert);
                String algorithm = TrustManagerFactory.getDefaultAlgorithm();
                TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm);
                trustManagerFactory.init(keyStore);
                TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
                sslContext.init(null, trustManagers, new SecureRandom());
                conn.setSSLSocketFactory(sslContext.getSocketFactory());
                // 校驗域名是否合法
                conn.setHostnameVerifier((hostname, session) -> {
                    HostnameVerifier verifier = HttpsURLConnection.getDefaultHostnameVerifier();
                    return verifier.verify("zouchanglin.cn", session);
                });
                conn.setRequestMethod("GET");
                conn.setConnectTimeout(5000);
                conn.setReadTimeout(5000);
                conn.connect();

                try (InputStream is = conn.getInputStream();
                     InputStreamReader reader = new InputStreamReader(is)
                ) {
                    char[] buf = new char[4096];
                    int len;
                    StringBuilder sb = new StringBuilder();
                    while ((len = reader.read(buf)) != -1) {
                        sb.append(new String(buf, 0, len));
                    }
                    mUIHandler.post(() -> listener.onSuccess(sb.toString()));
                } catch (IOException e) {
                    e.printStackTrace();
                    listener.onFail(e);
                }
            }catch (Exception e){
                e.printStackTrace();
                listener.onFail(e);
            }
        }).start();
    }

    private static X509Certificate getCert(Context context) {
        try {
            // src/main/assets/srca.cer
            InputStream inputStream = context.getAssets().open("srca.cer");
            CertificateFactory factory = CertificateFactory.getInstance("X.509");
            return (X509Certificate) factory.generateCertificate(inputStream);
        } catch (IOException | CertificateException e) {
            e.printStackTrace();
        }
        return null;
    }
}

在MainActivity中使用也很簡單:

public class MainActivity extends AppCompatActivity {

    private EditText etUrl;
    private TextView tvShow;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        etUrl = findViewById(R.id.et_url);
        tvShow = findViewById(R.id.tv_show);
    }

    public void loadContent(View view) {
        String url = etUrl.getText().toString();
        Https2Utils.doGet(this, url, new Https2Utils.HttpListener() {
            @Override
            public void onSuccess(String content) {
                tvShow.setText(content);
            }

            @Override
            public void onFail(Exception e) {
                Toast.makeText(MainActivity.this, "Failed!", Toast.LENGTH_SHORT).show();
            }
        });
    }
}
image

原文地址《Android Socket與HTTPS校驗》

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容