Android Socket与HTTPS校验

在Android中使用HTTPS的场景比较频繁,所以对于HTTPS的证书应该如何校验呢?关于HTTPS的校验原理可以参考我之前写的一篇文章: 《 HTTPS协议实现原理 》 ,相信看完后应该对HTTPS有一个比较大致的了解。而且对HTTP(s)请求的工具进行了封装,需要体会这种封装工具类的思路,也就是编码中常见的Listener机制。然后是Android中TCP、UDP通信的例子,主要是把Android设备作为Client端,如果对Java的Socket编程比较熟悉的话,这些都是特别简单的示例程序,非常容易看懂。

TCP/UDP 简单示例

下面的例子演示了Client向Server发送了一串小写英文,Server返回大写字符串的功能:

UDPServer.java:

 1public class UDPServer {
 2    private static final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
 3    public static void main(String[] args) throws Exception {
 4        DatagramSocket datagramSocket;
 5        datagramSocket = new DatagramSocket(8090);
 6        byte[] buf;
 7        DatagramPacket packet;
 8        while (true){
 9            buf = new byte[1024];
10            packet = new DatagramPacket(buf, buf.length);
11            datagramSocket.receive(packet);
12            String content = new String(packet.getData());
13            InetAddress address = packet.getAddress();
14            System.out.println(format.format(new Date()) + "-" + address + "-" + content);
15            int port = packet.getPort();
16            String replyContent = content.toUpperCase();
17            byte[] sendData = replyContent.getBytes();
18            DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, address, port);
19            datagramSocket.send(sendPacket);
20        }
21    }
22}

UDPClient.java:

 1public class UDPClient {
 2    private static final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
 3    public static void main(String[] args) throws Exception {
 4        System.out.println("请输入一句英文,服务器会返回其大写形式[exit退出]");
 5        Scanner scanner = new Scanner(System.in);
 6        InetAddress address = InetAddress.getLocalHost();
 7        DatagramPacket packet;
 8        DatagramSocket socket = new DatagramSocket();
 9        while(true){
10            String line = scanner.nextLine();
11            if("exit".equals(line)) break;
12            byte[] bytes = line.getBytes();
13            packet = new DatagramPacket(bytes, bytes.length, address, 8090);
14            socket.send(packet);
15            byte[] recvBuf = new byte[1024];
16            DatagramPacket recvPacket = new DatagramPacket(recvBuf, recvBuf.length);
17            socket.receive(recvPacket);
18            System.out.println(format.format(new Date()) + "-" + address + "-" + new String(recvBuf));
19        }
20        socket.close();
21    }
22}

TCPServer.java:

 1public class TCPServer {
 2    static SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
 3    public static void main(String[] args) throws IOException {
 4        ServerSocket serverSocket = new ServerSocket(9090);
 5        while (true){
 6            Socket socket = serverSocket.accept();
 7            InetAddress address = socket.getInetAddress();
 8            InputStream is = socket.getInputStream();
 9            byte[] readBuf = new byte[1024];
10            try{
11                int len = is.read(readBuf);
12                String recv = new String(readBuf, 0, len);
13                System.out.println(format.format(new Date()) + "-" + address + "-" + recv);
14                OutputStream os = socket.getOutputStream();
15                os.write(recv.toUpperCase().getBytes());
16            } catch (SocketException e){
17                System.err.println("客户端未发送信息");
18            } finally {
19                socket.close();
20            }
21        }
22    }
23}

TCPClient.java:

 1public class TCPClient {
 2    private static final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
 3    public static void main(String[] args) throws Exception {
 4        System.out.println("请输入一句英文,服务器会返回其大写形式[exit退出]");
 5        Scanner scanner = new Scanner(System.in);
 6        while(true){
 7            Socket socket = new Socket("127.0.0.1", 9090);
 8            String line = scanner.nextLine();
 9            if("exit".equals(line)) break;
10            OutputStream os = socket.getOutputStream();
11            os.write(line.getBytes());
12            InputStream is = socket.getInputStream();
13            byte[] readBuf = new byte[1024];
14            String recv = new String(readBuf, 0, is.read(readBuf));
15            InetAddress address = socket.getInetAddress();
16            System.out.println(format.format(new Date()) + "-" + address + "-" + recv);
17            socket.close();
18        }
19    }
20}

Client移植到Android

将两个Client移植到Android:

activity_main.xml

 1<?xml version="1.0" encoding="utf-8"?>
 2<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 3    xmlns:app="http://schemas.android.com/apk/res-auto"
 4    xmlns:tools="http://schemas.android.com/tools"
 5    android:layout_width="match_parent"
 6    android:layout_height="match_parent"
 7    android:orientation="vertical"
 8    android:padding="10dp"
 9    tools:context=".MainActivity">
10
11    <EditText
12        android:hint="输入发送内容"
13        android:id="@+id/et_content"
14        android:layout_width="match_parent"
15        android:layout_height="wrap_content"/>
16
17    <LinearLayout
18        android:orientation="horizontal"
19        android:layout_width="match_parent"
20        android:layout_height="wrap_content">
21        <EditText
22            android:text="192.168.1.113:8090"
23            android:id="@+id/et_udp_server"
24            android:layout_width="0dp"
25            android:layout_weight="1"
26            android:layout_height="wrap_content">
27        </EditText>
28        <Button
29            android:text="UDP发送"
30            android:onClick="sendUdpMessage"
31            android:layout_weight="1"
32            android:layout_width="0dp"
33            android:layout_height="wrap_content"/>
34    </LinearLayout>
35    <LinearLayout
36        android:orientation="horizontal"
37        android:layout_width="match_parent"
38        android:layout_height="wrap_content">
39        <EditText
40            android:text="192.168.1.113:9090"
41            android:id="@+id/et_tcp_server"
42            android:layout_width="0dp"
43            android:layout_weight="1"
44            android:layout_height="wrap_content">
45        </EditText>
46        <Button
47            android:text="TCP发送"
48            android:onClick="sendTcpMessage"
49            android:layout_weight="1"
50            android:layout_width="0dp"
51            android:layout_height="wrap_content"/>
52    </LinearLayout>
53    <TextView
54        android:id="@+id/tv_show"
55        android:text="收到回复:"
56        android:layout_width="match_parent"
57        android:layout_height="wrap_content"/>
58</LinearLayout>

MainActivity.java:

 1public class MainActivity extends AppCompatActivity {
 2    private static final String TAG = "MainActivity";
 3    private static final SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss", Locale.CHINA);
 4    private EditText etInput;
 5    private TextView textView;
 6    private EditText udpServerET;
 7    private EditText tcpServerET;
 8
 9    @Override
10    protected void onCreate(Bundle savedInstanceState) {
11        super.onCreate(savedInstanceState);
12        setContentView(R.layout.activity_main);
13        etInput = findViewById(R.id.et_content);
14        textView = findViewById(R.id.tv_show);
15        udpServerET = findViewById(R.id.et_udp_server);
16        tcpServerET = findViewById(R.id.et_tcp_server);
17    }
18
19    public void sendTcpMessage(View view) {
20        String[] tcpInfo = tcpServerET.getText().toString().split(":");
21        String inputContent = etInput.getText().toString();
22        new Thread(()->{
23            try (Socket socket = new Socket(tcpInfo[0], Integer.parseInt(tcpInfo[1]))){
24                OutputStream os = socket.getOutputStream();
25                os.write(inputContent.getBytes());
26                InputStream is = socket.getInputStream();
27                byte[] readBuf = new byte[1024];
28                String recv = new String(readBuf, 0, is.read(readBuf));
29                InetAddress address = socket.getInetAddress();
30                String ret = String.format("%s-%s-%s", df.format(new Date()), address, recv);
31                runOnUiThread(()-> textView.setText(ret));
32            }catch (IOException e){
33                Log.e(TAG, "sendTcpMessage: Error!");
34            }
35        }).start();
36    }
37
38    public void sendUdpMessage(View view) {
39        String[] udpInfo = udpServerET.getText().toString().split(":");
40        String inputContent = etInput.getText().toString();
41        new Thread(()->{
42            try {
43                DatagramSocket socket = new DatagramSocket();
44                byte[] bytes = inputContent.getBytes();
45                InetAddress address = InetAddress.getByName(udpInfo[0]);
46                int serverPort = Integer.parseInt(udpInfo[1]);
47                DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, serverPort);
48                socket.send(packet);
49                byte[] recvBuf = new byte[1024];
50                DatagramPacket recvPacket = new DatagramPacket(recvBuf, recvBuf.length);
51                socket.receive(recvPacket);
52                String ret = String.format("%s-%s-%s", df.format(new Date()), address, new String(recvBuf));
53                runOnUiThread(()-> textView.setText(ret));
54            }catch (IOException e){
55                Log.e(TAG, "sendUdpMessage: Error!");
56            }
57        }).start();
58    }
59}

AndroidManifest.xml:

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

注意点:1、网络访问权限 2、子线程代码中使用runOnUiThread()方法可更新UI

Android访问HTTPS

对于一个普通的HTTP请求,我们可以使用如下方式来发起请求,下面是一个简易的Http请求工具类:

 1public class HttpUtils {
 2    private static Handler mUIHandler = new Handler(Looper.getMainLooper());
 3
 4    interface HttpListener {
 5        void onSuccess(String content);
 6
 7        void onFail(Exception e);
 8    }
 9
10    public static void doGet(String urlStr, HttpListener listener) {
11        new Thread(() -> {
12            Looper.prepare();
13            try {
14                URL url = new URL(urlStr);
15                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
16                conn.setRequestMethod("GET");
17                conn.setConnectTimeout(5000);
18                conn.setReadTimeout(5000);
19                conn.connect();
20
21                try (InputStream is = conn.getInputStream();
22                     InputStreamReader reader = new InputStreamReader(is)
23                ) {
24                    char[] buf = new char[4096];
25                    int len;
26                    StringBuilder sb = new StringBuilder();
27                    while ((len = reader.read(buf)) != -1) {
28                        sb.append(new String(buf, 0, len));
29                    }
30                    mUIHandler.post(() -> listener.onSuccess(sb.toString()));
31                } catch (IOException e) {
32                    e.printStackTrace();
33                    listener.onFail(e);
34                }
35            }catch (IOException e){
36                e.printStackTrace();
37                listener.onFail(e);
38            }
39        }).start();
40    }
41}

1、不校验证书(不推荐)

MyX509TrustManager.java,MyX509TrustManager实现不做任何事情:

 1...
 2import java.security.cert.CertificateException;
 3import java.security.cert.X509Certificate;
 4
 5import javax.net.ssl.X509TrustManager;
 6
 7public class MyX509TrustManager implements X509TrustManager {
 8
 9    @Override
10    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
11		// TODO...
12    }
13
14    @Override
15    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
16        // TODO...
17    }
18
19    @Override
20    public X509Certificate[] getAcceptedIssuers() {
21        return new X509Certificate[0];
22    }
23}

HttpsUtils.java

 1...
 2public class HttpsUtils {
 3    private static Handler mUIHandler = new Handler(Looper.getMainLooper());
 4
 5    interface HttpListener {
 6        void onSuccess(String content);
 7
 8        void onFail(Exception e);
 9    }
10
11    public static void doGet(Context context, String urlStr, HttpListener listener) {
12        new Thread(() -> {
13            Looper.prepare();
14            try {
15                URL url = new URL(urlStr);
16                HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
17                SSLContext sslContext = SSLContext.getInstance("TLS");
18                // 放入自定义的MyX509TrustManager对象即可
19                TrustManager[] trustManagers = {new MyX509TrustManager()};
20                sslContext.init(null, trustManagers, new SecureRandom());
21                conn.setSSLSocketFactory(sslContext.getSocketFactory());
22                conn.setRequestMethod("GET");
23                conn.setConnectTimeout(5000);
24                conn.setReadTimeout(5000);
25                conn.connect();
26
27                try (InputStream is = conn.getInputStream();
28                     InputStreamReader reader = new InputStreamReader(is)
29                ) {
30                    char[] buf = new char[4096];
31                    int len;
32                    StringBuilder sb = new StringBuilder();
33                    while ((len = reader.read(buf)) != -1) {
34                        sb.append(new String(buf, 0, len));
35                    }
36                    mUIHandler.post(() -> listener.onSuccess(sb.toString()));
37                } catch (IOException e) {
38                    e.printStackTrace();
39                    listener.onFail(e);
40                }
41            }catch (Exception e){
42                e.printStackTrace();
43                listener.onFail(e);
44            }
45        }).start();
46    }
47}

2、校验证书(推荐)

拿我自己的博客站点来说,想要获得证书只需要在浏览器下载对应的证书即可(选择DER编码二进制和Base64编码均可),保存了一个名为srca.cer的文件到桌面:

将这份证书文件复制到项目的src/main/assets/目录下,没有assets就新建,所以完整路径为src/main/assets/srca.cer。

接下来需要实现MyX509TrustManager.java中的方法:

 1public class MyX509TrustManager implements X509TrustManager {
 2    private static final String TAG = "MyX509TrustManager";
 3    // 证书对象
 4    private X509Certificate serverCert;
 5
 6    public MyX509TrustManager(X509Certificate serverCert) {
 7        this.serverCert = serverCert;
 8    }
 9
10    @Override
11    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
12
13    }
14
15    @Override
16    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
17        // 遍历证书
18        for (X509Certificate certificate: chain){
19            // 校验合法性与是否过期
20            certificate.checkValidity();
21            try {
22                // 校验公钥
23                PublicKey publicKey = serverCert.getPublicKey();
24                certificate.verify(publicKey);
25            } catch (Exception e) {
26                throw new CertificateException(e);
27            }
28        }
29    }
30
31    @Override
32    public X509Certificate[] getAcceptedIssuers() {
33        return new X509Certificate[0];
34    }
35}

同时,将使用keyStore这个API来获取TrustManager数组,HttpsUtils.java如下:

 1public class Https2Utils {
 2    private static Handler mUIHandler = new Handler(Looper.getMainLooper());
 3
 4    interface HttpListener {
 5        void onSuccess(String content);
 6
 7        void onFail(Exception e);
 8    }
 9
10    public static void doGet(Context context, String urlStr, HttpListener listener) {
11        new Thread(() -> {
12            Looper.prepare();
13            try {
14                URL url = new URL(urlStr);
15                HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
16                SSLContext sslContext = SSLContext.getInstance("TLS");
17                X509Certificate serverCert = getCert(context);
18                String defaultType = KeyStore.getDefaultType();
19                KeyStore keyStore = KeyStore.getInstance(defaultType);
20                keyStore.load(null);
21                // 别名、证书
22                keyStore.setCertificateEntry("srca", serverCert);
23                String algorithm = TrustManagerFactory.getDefaultAlgorithm();
24                TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm);
25                trustManagerFactory.init(keyStore);
26                TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
27                sslContext.init(null, trustManagers, new SecureRandom());
28                conn.setSSLSocketFactory(sslContext.getSocketFactory());
29                // 校验域名是否合法
30                conn.setHostnameVerifier((hostname, session) -> {
31                    HostnameVerifier verifier = HttpsURLConnection.getDefaultHostnameVerifier();
32                    return verifier.verify("zouchanglin.cn", session);
33                });
34                conn.setRequestMethod("GET");
35                conn.setConnectTimeout(5000);
36                conn.setReadTimeout(5000);
37                conn.connect();
38
39                try (InputStream is = conn.getInputStream();
40                     InputStreamReader reader = new InputStreamReader(is)
41                ) {
42                    char[] buf = new char[4096];
43                    int len;
44                    StringBuilder sb = new StringBuilder();
45                    while ((len = reader.read(buf)) != -1) {
46                        sb.append(new String(buf, 0, len));
47                    }
48                    mUIHandler.post(() -> listener.onSuccess(sb.toString()));
49                } catch (IOException e) {
50                    e.printStackTrace();
51                    listener.onFail(e);
52                }
53            }catch (Exception e){
54                e.printStackTrace();
55                listener.onFail(e);
56            }
57        }).start();
58    }
59
60    private static X509Certificate getCert(Context context) {
61        try {
62            // src/main/assets/srca.cer
63            InputStream inputStream = context.getAssets().open("srca.cer");
64            CertificateFactory factory = CertificateFactory.getInstance("X.509");
65            return (X509Certificate) factory.generateCertificate(inputStream);
66        } catch (IOException | CertificateException e) {
67            e.printStackTrace();
68        }
69        return null;
70    }
71}

在MainActivity中使用也很简单:

 1public class MainActivity extends AppCompatActivity {
 2
 3    private EditText etUrl;
 4    private TextView tvShow;
 5
 6    @Override
 7    protected void onCreate(Bundle savedInstanceState) {
 8        super.onCreate(savedInstanceState);
 9        setContentView(R.layout.activity_main);
10        etUrl = findViewById(R.id.et_url);
11        tvShow = findViewById(R.id.tv_show);
12    }
13
14    public void loadContent(View view) {
15        String url = etUrl.getText().toString();
16        Https2Utils.doGet(this, url, new Https2Utils.HttpListener() {
17            @Override
18            public void onSuccess(String content) {
19                tvShow.setText(content);
20            }
21
22            @Override
23            public void onFail(Exception e) {
24                Toast.makeText(MainActivity.this, "Failed!", Toast.LENGTH_SHORT).show();
25            }
26        });
27    }
28}

原文地址 《Android Socket与HTTPS校验》