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}