在Android中使用HTTPS的场景比较频繁,所以对于HTTPS的证书应该如何校验呢?关于HTTPS的校验原理可以参考我之前写的一篇文章:《 HTTPS协议实现原理 》,相信看完后应该对HTTPS有一个比较大致的了解。而且对HTTP(s)请求的工具进行了封装,需要体会这种封装工具类的思路,也就是编码中常见的Listener机制。然后是Android中TCP、UDP通信的例子,主要是把Android设备作为Client端,如果对Java的Socket编程比较熟悉的话,这些都是特别简单的示例程序,非常容易看懂。
TCP/UDP 简单示例
下面的例子演示了Client向Server发送了一串小写英文,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("客户端未发送信息");
} 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="输入发送内容"
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发送"
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发送"
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"/>
注意点:1、网络访问权限 2、子线程代码中使用runOnUiThread()方法可更新UI
Android访问HTTPS
对于一个普通的HTTP请求,我们可以使用如下方式来发起请求,下面是一个简易的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的文件到桌面:
将这份证书文件复制到项目的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();
}
});
}
}