Android开发实践之《手摸手教你搭建移动端Https服务器》

导读:本篇文章讲解 Android开发实践之《手摸手教你搭建移动端Https服务器》,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

目录

基础知识

准备工作

Keytool

Protecle

生成HTTPS所需证书

使用Keytool生成jks证书

使用Protecle将JSK证书转BKS证书

使用AndroidASync配置BKS证书实现HTTPS服务器

AndroidASync的使用

导入并配置BKS证书

运行测试

遇到的问题

源码

参考资料


基础知识

百度关键词:Android Http服务器,就可以搜出来很多关于搭建Android端Http的服务器的教程了,但是关于Https服务资料是比较少的。

实现HttpServer的第三方框架主要有:NanoHttpd和AndroidAsync这两个,关于她们介绍,请看billlllllllll的《在Android上实现HttpServer》。

本文主要使用AndroidAsync搭建Https服务器,关于的AndroidAsync的使用见:

https://github.com/koush/AndroidAsync

关于Https相关知识可以参考sheldon_blogs的《Android : 关于HTTPS、TLS/SSL认证以及客户端证书导入方法

准备工作

Keytool

keytool是JDK自带的证书管理工具,一般在 /java/bin/目录,配置了JDK环境变量都是可以直接执行的。使用 “keytool -command_name -help” 获取 command_name 的用法。具体可参考:

孤傲苍狼的《Java制作证书的工具keytool用法总结

Android开发实践之《手摸手教你搭建移动端Https服务器》

Protecle

protecle是第三方证书转换生成工具,由于Android平台不识别.keystore和.jks格式的证书库文件,因此Android平台引入一种的证书库格式:BKS,protecle工具可将jks/p12证书转bks证书。

下载链接:

链接: 百度网盘 请输入提取码 提取码: pvkw

生成HTTPS所需证书

使用Keytool生成jks证书

在终端执行命令:keytool -genkey -alias server -keyalg RSA -keystore server.jks在当前目录下生成别名server,加密算法为RSA,证书名称为server的jks证书。

看不懂命令?可先阅读参考:孤傲苍狼的《Java制作证书的工具keytool用法总结

Android开发实践之《手摸手教你搭建移动端Https服务器》

注意:

1、密钥库的密码至少必须6个字符,可以是纯数字或者字母或者数字和字母的组合等等,我这里填的是123456;

2、”名字与姓氏”应该是输入域名,而不是我们的个人姓名,由于项目的实际应用场景只有ip没有域名,因此这里填了个*号代表全匹配,其它的可以不用填,一路Yes或者回车就可以了。当然实际项目肯定是要好好填写的。

证书生成后有个以下提示,可以不用理会,实际测试没有影响。

Warning:JKS 密钥库使用专用格式。建议使用 “keytool -importkeystore -srckeystore server.jks -destkeystore server.jks -deststoretype pkcs12” 迁移到行业标准格式 PKCS12。

我们还可以执行keytool -v -list -keystore server.jks查看证书信息,密钥库口令可不填。

Android开发实践之《手摸手教你搭建移动端Https服务器》

使用Protecle将JSK证书转BKS证书

下载protecle.zip压缩包,解压。使用java -jar portecle.jar 运行此工具。

Android开发实践之《手摸手教你搭建移动端Https服务器》

点击File–>Open Keystore File(ctrl+o),选中server.jks并打开。

Android开发实践之《手摸手教你搭建移动端Https服务器》

在弹出的密码框输入密码(这里是:123456),点OK。

Android开发实践之《手摸手教你搭建移动端Https服务器》

点击Tools–>Change Keystore Type –> BKS,这里截不了图,用你们的头脑想象就行。

Android开发实践之《手摸手教你搭建移动端Https服务器》

在弹出的密码框输入密码(这里还是:123456),点OK。

Android开发实践之《手摸手教你搭建移动端Https服务器》

然后就提示Change Keystore Type Successful了。

Android开发实践之《手摸手教你搭建移动端Https服务器》

点确定,再按快捷键Ctrl+S保存,在弹出的保存对话框里填上server.bks并保存到当前目录,注意这里要带上.jks后缀名,方便后面导入到Android项目的Raw文件夹。

最后关掉protecle工具,已经在当前目录生成了bks证书。

Android开发实践之《手摸手教你搭建移动端Https服务器》

其它博客有介绍使用p12证书转bks证书的,这里也贴下jks转p12的命令,供大家参考。

keytool -importkeystore -srckeystore server.jks -srcstoretype JKS -deststoretype PKCS12 -destkeystore server.p12

使用AndroidASync配置BKS证书实现HTTPS服务器

AndroidASync的使用

具体使用见:https://github.com/koush/AndroidAsync,这里展示下示例源码好了。

AndroidAsync依赖添加上以后,新建一个SSLHttpServer类,代码如下:

package com.nxg.httpsserver;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.text.TextUtils;
import android.util.Log;

import com.koushikdutta.async.http.AsyncHttpGet;
import com.koushikdutta.async.http.Multimap;
import com.koushikdutta.async.http.body.AsyncHttpRequestBody;
import com.koushikdutta.async.http.server.AsyncHttpServer;
import com.koushikdutta.async.http.server.AsyncHttpServerRequest;
import com.koushikdutta.async.http.server.AsyncHttpServerResponse;
import com.koushikdutta.async.http.server.HttpServerRequestCallback;
import com.nxg.httpsserver.api.ApiCodeMsg;
import com.nxg.httpsserver.api.ApiResult;

import java.security.KeyStore;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

/**
 * 轻量的Http服务器
 */
public class SSLHttpServer implements HttpServerRequestCallback {

    private static final String TAG = "SSLHttpServer";
    private static final String X509 = "X509";
    private static final String PASSWORD = "123456";
    public static int DEFAULT_PORT = 8888;
    public static final String GET_HOST_SOFTWARE_VERSION = "/getHostSoftwareVersion";
    public static String URL_GET_HOST_SOFTWARE_VERSION = "https://%s:" + DEFAULT_PORT + GET_HOST_SOFTWARE_VERSION;

    @SuppressLint("StaticFieldLeak")
    private static Context mContext;

    public static void setContext(Context context) {
        SSLHttpServer.mContext = context;
    }

    @SuppressLint("StaticFieldLeak")
    private static SSLHttpServer mInstance;

    private AsyncHttpServer asyncHttpServer;

    public static SSLHttpServer getInstance() {
        if (mInstance == null) {
            synchronized (SSLHttpServer.class) {
                if (mInstance == null) {
                    mInstance = new SSLHttpServer();
                }
            }
        }
        return mInstance;
    }

    private SSLContext sslContext = null;

    private SSLHttpServer() {
        try {
            KeyManagerFactory kmf = KeyManagerFactory.getInstance(X509);
            KeyStore ks = KeyStore.getInstance("BKS");

            ks.load(mContext.getResources().openRawResource(R.raw.server), PASSWORD.toCharArray());
            kmf.init(ks, PASSWORD.toCharArray());

            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType());
            ts.load(mContext.getResources().openRawResource(R.raw.server), PASSWORD.toCharArray());
            tmf.init(ts);

            sslContext = SSLContext.getInstance("TLS");
            sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (asyncHttpServer == null) {
            asyncHttpServer = new AsyncHttpServer();
        }
    }

    /**
     * 开启本地服务
     */
    public void startServer() {
        Log.i(TAG, "startServer: ");
        asyncHttpServer.addAction("OPTIONS", "[\\d\\D]*", this);
        asyncHttpServer.get("[\\d\\D]*", this);
        asyncHttpServer.post("[\\d\\D]*", this);
        asyncHttpServer.listenSecure(DEFAULT_PORT, sslContext);

    }

    @Override
    public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {

        Log.d(TAG, "onRequest: uri = " + request.getPath());

        String uri = request.getPath();

        //这个是获取header参数的地方,一定要谨记哦
        Multimap headers = request.getHeaders().getMultiMap();

        if (headers != null) {
            Log.d(TAG, "onRequest: headers = " + headers.toString());
        }

        //注意:这个地方是获取post请求的参数的地方,一定要谨记哦
        Multimap multimap = ((AsyncHttpRequestBody<Multimap>) request.getBody()).get();

        if (multimap != null) {
            Log.d(TAG, "onRequest: multimap = " + multimap.toString());
        }

        //GET/POST等请求方式
        String method = request.getMethod();
        Log.i(TAG, "onRequest: method = " + method);
        //query 是GET请求方式的参数
        String query = request.getQuery().toString();
        Log.i(TAG, "onRequest: query = " + query);

        //目前主要使用GET请求
        if (TextUtils.equals(method, AsyncHttpGet.METHOD)) {
            Log.i(TAG, "onRequest: request:" + uri);
            response.send(newApiResult(uri, request.getQuery()));

        } else {

            response.send(newApiResult(uri, multimap));
        }
    }

    /**
     * 返回ApiResult的Json格式字符串
     *
     * @param uri      接口
     * @param multimap 参数
     * @return String
     */
    private static String newApiResult(String uri, Multimap multimap) {
        switch (uri) {
            case "/test":
                return GsonUtils.getInstance().toJson(ApiResult.success("Test is ok!"));
            case "/getHostSoftwareVersion":
                //获取软件版本信息
                if (mContext == null) {
                    return GsonUtils.getInstance().toJson(ApiResult.fail(ApiCodeMsg.REQUEST_ERROR_CONTEXT_IS_NULL));
                }
                PackageInfo packageInfo = null;
                try {
                    packageInfo = mContext.getApplicationContext().getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
                } catch (PackageManager.NameNotFoundException e) {
                    e.printStackTrace();
                }
                String localVersion = null;
                if (packageInfo != null) localVersion = packageInfo.versionName;
                return GsonUtils.getInstance().toJson(ApiResult.success(localVersion));
            default:
                return GsonUtils.getInstance().toJson(ApiResult.fail(ApiCodeMsg.REQUEST_ERROR_404));

        }

    }
}

导入并配置BKS证书

将生成的server.bks证书导入到项目的raw文件夹,注意后缀名一定要加上.bks,否则代码里无法直接引用。

Android开发实践之《手摸手教你搭建移动端Https服务器》

关键是这个代码:

 private SSLContext sslContext = null;

    private SSLHttpServer() {
        try {
            KeyManagerFactory kmf = KeyManagerFactory.getInstance(X509);
            KeyStore ks = KeyStore.getInstance("BKS");

            ks.load(mContext.getResources().openRawResource(R.raw.server), PASSWORD.toCharArray());
            kmf.init(ks, PASSWORD.toCharArray());

            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType());
            ts.load(mContext.getResources().openRawResource(R.raw.server), PASSWORD.toCharArray());
            tmf.init(ts);

            sslContext = SSLContext.getInstance("TLS");
            sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (asyncHttpServer == null) {
            asyncHttpServer = new AsyncHttpServer();
        }
    }

    /**
     * 开启本地服务
     */
    public void startServer() {
        Log.i(TAG, "startServer: ");
        asyncHttpServer.addAction("OPTIONS", "[\\d\\D]*", this);
        asyncHttpServer.get("[\\d\\D]*", this);
        asyncHttpServer.post("[\\d\\D]*", this);
        asyncHttpServer.listenSecure(DEFAULT_PORT, sslContext);

    }


//AsyncServer源码
 public void listenSecure(final int port, final SSLContext sslContext) {
        AsyncServer.getDefault().listen(null, port, new ListenCallback() {
            @Override
            public void onAccepted(AsyncSocket socket) {
                AsyncSSLSocketWrapper.handshake(socket, null, port, sslContext.createSSLEngine(), null, null, false,
                new AsyncSSLSocketWrapper.HandshakeCallback() {
                    @Override
                    public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) {
                        if (socket != null)
                            mListenCallback.onAccepted(socket);
                    }
                });
            }

            @Override
            public void onListening(AsyncServerSocket socket) {
                mListenCallback.onListening(socket);
            }

            @Override
            public void onCompleted(Exception ex) {
                mListenCallback.onCompleted(ex);
            }
        });
    }
    
//AsyncSSLSocketWrapper源码
 public static void handshake(AsyncSocket socket,
                                 String host, int port,
                                 SSLEngine sslEngine,
                                 TrustManager[] trustManagers, HostnameVerifier verifier, boolean clientMode,
                                 final HandshakeCallback callback) {
        AsyncSSLSocketWrapper wrapper = new AsyncSSLSocketWrapper(socket, host, port, sslEngine, trustManagers, verifier, clientMode);
        wrapper.handshakeCallback = callback;
        socket.setClosedCallback(new CompletedCallback() {
            @Override
            public void onCompleted(Exception ex) {
                if (ex != null)
                    callback.onHandshakeCompleted(ex, null);
                else
                    callback.onHandshakeCompleted(new SSLException("socket closed during handshake"), null);
            }
        });
        try {
            wrapper.engine.beginHandshake();
            wrapper.handleHandshakeStatus(wrapper.engine.getHandshakeStatus());
        } catch (SSLException e) {
            wrapper.report(e);
        }
    }

KeyManagerFactory类充当基于密钥内容源的密钥管理器的工厂。每个密钥管理器管理特定类型的、由安全套接字所使用的密钥内容。密钥内容是基于 KeyStore 和/或提供者特定的源。详细介绍见KeyManagerFactory

X.509标准是密码学里公钥证书的格式标准,详细介绍见X509证书详解(中文翻译)

KeyStore类表示密钥和证书的存储设施,详细介绍见KeyStore

TrustManagerFactory类充当基于信任材料源的信任管理器的工厂。每个信任管理器管理特定类型的由安全套接字使用的信任材料。信任材料是基于 KeyStore 和/或提供者特定的源。详细介绍见TrustManagerFactory

SSLContext类的实例表示安全套接字协议的实现,它充当用于安全套接字工厂或 SSLEngine 的工厂。用可选的一组密钥和信任管理器及安全随机字节源初始化此类。详细介绍见SSLContext

SSLEngine类允许使用安全套接字层 (SSL) 或 IETF RFC 2246 “Transport Layer Security” (TLS) 协议进行安全通信,但它与传输无关。详细介绍见SSLEngine

本文不涉及HTTPS的原理解析,感兴趣的参考Ajay《一次HTTPS请求的过程》

运行测试

启动HttpsSever并请求接口。

package com.nxg.httpsserver;

import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;

import androidx.appcompat.app.AppCompatActivity;

import com.blankj.utilcode.util.NetworkUtils;
import com.koushikdutta.async.http.AsyncHttpClient;
import com.koushikdutta.async.http.AsyncHttpGet;
import com.koushikdutta.async.http.AsyncHttpResponse;

import java.util.concurrent.ExecutionException;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    private HandlerThread httpHandlerThread;
    private Handler httpHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (httpHandlerThread == null) {
            httpHandlerThread = new HandlerThread("HttpThread");
            httpHandlerThread.start();
            httpHandler = new Handler(httpHandlerThread.getLooper());
        }
        init();
        testRequestSSLHttpServer();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (httpHandlerThread != null) {
            httpHandlerThread.quit();
            httpHandler.removeCallbacksAndMessages(null);
            httpHandlerThread = null;
            httpHandler = null;
        }
    }

    /**
     * 初始化并且启动HttpServer
     */
    private void init() {
        Log.i(TAG, "init: ");
        SSLHttpClient.setContext(getApplicationContext());
        SSLHttpServer.setContext(getApplicationContext());
        SSLHttpServer.getInstance().startServer();
    }

    /**
     * 测试请求Https接口
     */
    private void testRequestSSLHttpServer() {
        Log.i(TAG, "testRequestSSLHttpServer: ");
        String hostIP = NetworkUtils.getIPAddress(true);
        String uri = String.format(SSLHttpServer.URL_GET_HOST_SOFTWARE_VERSION, hostIP);
        Log.i(TAG, "testRequestSSLHttpServer: uri " + uri);
        if (httpHandler != null) {
            httpHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    //耗时操作
                    try {
                        SSLHttpClient.getInstance().getAsyncHttpClient().executeString(new AsyncHttpGet(uri), new AsyncHttpClient.StringCallback() {
                            @Override
                            public void onCompleted(Exception e, AsyncHttpResponse source, String result) {
                                Log.i(TAG, "onCompleted: " + result);
                            }
                        }).get();
                    } catch (ExecutionException | InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, 0);
        }
    }

Android开发实践之《手摸手教你搭建移动端Https服务器》

请求成功。

使用浏览器测试,由于server.bks证书不是通过CA申请的,所以浏览器根证书不信任,这里我们手动点继续连接。

Android开发实践之《手摸手教你搭建移动端Https服务器》

返回成功。

Android开发实践之《手摸手教你搭建移动端Https服务器》

遇到的问题

你可能会遇到java.util.concurrent.ExecutionException: javax.net.ssl.SSLHandshakeException: Handshake failed

请检查:

1、密钥库的密码至少必须6个字符,可以是纯数字或者字母或者数字和字母的组合等等,你要检查下代码里填的密码跟证书设置的密码是否匹配;

2、”名字与姓氏”应该是输入域名,而不是我们的个人姓名或其它无意义的字符,由于项目的实际应用场景只有设备ip没有域名,因此这里填了个*号代表全匹配所有的ip和域名,大家可以尝试这个方法看下这个问题是否能解决。

源码

https://github.com/xiangang/AndroidDevelopmentPractices/tree/master/AndroidHttpsServer

参考资料

https://github.com/NanoHttpd/nanohttpd

https://github.com/koush/AndroidAsync

使用NanoHttpd在Android设备创建https/wss服务端

搭建Android本地可信任的https服务器

keytool 可视化工具 Portecle 使用教程 图文教程 微信认证开发教程

使用NanoHttpd在Android设备创建https/wss服务端

使用SSLSocket实现双向认证(keytool证书创建双向认证证书(这里有根证书)的详细步骤以及踩雷)

Android 上 Https 双向通信— 深入理解KeyManager 和 TrustManagers

android本地搭建Https本地服务器

Java制作证书的工具keytool用法总结Android : 关于HTTPS、TLS/SSL认证以及客户端证书导入方法

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/116893.html

(0)

相关推荐

发表回复

登录后才能评论
半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!