SSLHandshakeException:Android N / 7.0上的握手失败

时间:2016-08-24 21:39:27

标签: android ssl elliptic-curve sslhandshakeexception android-7.0-nougat

我正在开发一个应用程序,其中(power)用户必须设置自己的服务器(即nginx)才能运行后端应用程序。需要在应用程序中配置相应的域才能连接。我主要是在自己的手机上测试(sony z3c),并开始为5.1开发。后来我收到了6.0的更新,但仍在模拟器中维护了一个工作5.1。不久前,我开始研究带有7.0图像的AVD,令我惊讶的是它不能连接到我的服务器,告诉我ssl握手失败了。我的nginx配置非常严格,但它适用于5.1和6.0,所以......?!

以下是我所知道的:

  • 我使用v24作为支持库,即我的compileSdkVersion是24。
  • 我使用Volley v1.0.0
  • 我已尝试过TLSSocketFactory,但它并没有改变任何事情。这似乎大部分时间都用于防止旧版SDK版本使用SSL3。
  • 我尝试过增加timeout,但它并没有改变任何内容。
  • 我已经尝试过直接使用HttpURLConnection,但除了堆栈跟踪之外它没有任何改变(它没有凌空参考,但除此之外相同)。

如果没有TLSSocketFactory,请求将通过裸请求队列进行,并使用Volley.newRequestQueue(context)进行实例化。

这是我在android studio中看到的:

W/System.err: com.android.volley.NoConnectionError: javax.net.ssl.SSLHandshakeException: Connection closed by peer
W/System.err:     at com.android.volley.toolbox.BasicNetwork.performRequest(BasicNetwork.java:151)
W/System.err:     at com.android.volley.NetworkDispatcher.run(NetworkDispatcher.java:112)
W/System.err: Caused by: javax.net.ssl.SSLHandshakeException: Connection closed by peer
W/System.err:     at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
W/System.err:     at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:357)
W/System.err:     at com.android.okhttp.Connection.connectTls(Connection.java:235)
W/System.err:     at com.android.okhttp.Connection.connectSocket(Connection.java:199)
W/System.err:     at com.android.okhttp.Connection.connect(Connection.java:172)
W/System.err:     at com.android.okhttp.Connection.connectAndSetOwner(Connection.java:367)
W/System.err:     at com.android.okhttp.OkHttpClient$1.connectAndSetOwner(OkHttpClient.java:130)
W/System.err:     at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:329)
W/System.err:     at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:246)
W/System.err:     at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:457)
W/System.err:     at com.android.okhttp.internal.huc.HttpURLConnectionImpl.connect(HttpURLConnectionImpl.java:126)
W/System.err:     at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getOutputStream(HttpURLConnectionImpl.java:257)
W/System.err:     at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.getOutputStream(DelegatingHttpsURLConnection.java:218)
W/System.err:     at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.getOutputStream(HttpsURLConnectionImpl.java)
W/System.err:     at com.android.volley.toolbox.HurlStack.addBodyIfExists(HurlStack.java:264)
W/System.err:     at com.android.volley.toolbox.HurlStack.setConnectionParametersForRequest(HurlStack.java:234)
W/System.err:     at com.android.volley.toolbox.HurlStack.performRequest(HurlStack.java:107)
W/System.err:     at com.android.volley.toolbox.BasicNetwork.performRequest(BasicNetwork.java:96)
W/System.err:   ... 1 more
W/System.err:   Suppressed: javax.net.ssl.SSLHandshakeException: Handshake failed
W/System.err:     at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:429)
W/System.err:       ... 17 more
W/System.err:   Caused by: javax.net.ssl.SSLProtocolException: SSL handshake terminated: ssl=0x7ffef3748040: Failure in SSL library, usually a protocol error
W/System.err: error:10000410:SSL routines:OPENSSL_internal:SSLV3_ALERT_HANDSHAKE_FAILURE (external/boringssl/src/ssl/s3_pkt.c:610 0x7ffeda1d2240:0x00000001)
W/System.err: error:1000009a:SSL routines:OPENSSL_internal:HANDSHAKE_FAILURE_ON_CLIENT_HELLO (external/boringssl/src/ssl/s3_clnt.c:764 0x7ffee9d2b70a:0x00000000)
W/System.err:     at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
W/System.err:     at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:357)
W/System.err:       ... 17 more

因为它说SSLV3_ALERT_HANDSHAKE_FAILURE我只能因为某些原因而尝试使用SSLv3进行连接并失败,但这对我来说没有任何意义。这可能是一个密码问题,但我怎么能说出它试图使用的是什么?我宁愿不在服务器上启用密码,尝试连接并重复。

我的nginx网站使用了let的加密证书,并具有以下配置:

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/ssl/certs/lets-encrypt-x1-cross-signed.pem;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:!aNULL;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
ssl_ecdh_curve secp384r1;
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1.2;

为了测试这些密码我是script并且它确认了这些密码(在服务器网络之外的一个喘息的vps上运行):

Testing ECDHE-RSA-AES256-GCM-SHA384...YES
Testing ECDHE-ECDSA-AES256-GCM-SHA384...NO (sslv3 alert handshake failure)
Testing ECDHE-RSA-AES256-SHA384...NO (sslv3 alert handshake failure)
Testing ECDHE-ECDSA-AES256-SHA384...NO (sslv3 alert handshake failure)
Testing ECDHE-RSA-AES256-SHA...NO (sslv3 alert handshake failure)
Testing ECDHE-ECDSA-AES256-SHA...NO (sslv3 alert handshake failure)
Testing SRP-DSS-AES-256-CBC-SHA...NO (sslv3 alert handshake failure)
Testing SRP-RSA-AES-256-CBC-SHA...NO (sslv3 alert handshake failure)
Testing DHE-DSS-AES256-GCM-SHA384...NO (sslv3 alert handshake failure)
Testing DHE-RSA-AES256-GCM-SHA384...NO (sslv3 alert handshake failure)
Testing DHE-RSA-AES256-SHA256...NO (sslv3 alert handshake failure)
Testing DHE-DSS-AES256-SHA256...NO (sslv3 alert handshake failure)
Testing DHE-RSA-AES256-SHA...NO (sslv3 alert handshake failure)
Testing DHE-DSS-AES256-SHA...NO (sslv3 alert handshake failure)
Testing DHE-RSA-CAMELLIA256-SHA...NO (sslv3 alert handshake failure)
Testing DHE-DSS-CAMELLIA256-SHA...NO (sslv3 alert handshake failure)
Testing AECDH-AES256-SHA...NO (sslv3 alert handshake failure)
Testing SRP-AES-256-CBC-SHA...NO (sslv3 alert handshake failure)
Testing ADH-AES256-GCM-SHA384...NO (sslv3 alert handshake failure)
Testing ADH-AES256-SHA256...NO (sslv3 alert handshake failure)
Testing ADH-AES256-SHA...NO (sslv3 alert handshake failure)
Testing ADH-CAMELLIA256-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-AES256-GCM-SHA384...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-AES256-GCM-SHA384...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-AES256-SHA384...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-AES256-SHA384...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-AES256-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-AES256-SHA...NO (sslv3 alert handshake failure)
Testing AES256-GCM-SHA384...NO (sslv3 alert handshake failure)
Testing AES256-SHA256...NO (sslv3 alert handshake failure)
Testing AES256-SHA...NO (sslv3 alert handshake failure)
Testing CAMELLIA256-SHA...NO (sslv3 alert handshake failure)
Testing PSK-AES256-CBC-SHA...NO (no ciphers available)
Testing ECDHE-RSA-DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing ECDHE-ECDSA-DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing SRP-DSS-3DES-EDE-CBC-SHA...NO (sslv3 alert handshake failure)
Testing SRP-RSA-3DES-EDE-CBC-SHA...NO (sslv3 alert handshake failure)
Testing EDH-RSA-DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing EDH-DSS-DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing AECDH-DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing SRP-3DES-EDE-CBC-SHA...NO (sslv3 alert handshake failure)
Testing ADH-DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing PSK-3DES-EDE-CBC-SHA...NO (no ciphers available)
Testing ECDHE-RSA-AES128-GCM-SHA256...YES
Testing ECDHE-ECDSA-AES128-GCM-SHA256...NO (sslv3 alert handshake failure)
Testing ECDHE-RSA-AES128-SHA256...NO (sslv3 alert handshake failure)
Testing ECDHE-ECDSA-AES128-SHA256...NO (sslv3 alert handshake failure)
Testing ECDHE-RSA-AES128-SHA...NO (sslv3 alert handshake failure)
Testing ECDHE-ECDSA-AES128-SHA...NO (sslv3 alert handshake failure)
Testing SRP-DSS-AES-128-CBC-SHA...NO (sslv3 alert handshake failure)
Testing SRP-RSA-AES-128-CBC-SHA...NO (sslv3 alert handshake failure)
Testing DHE-DSS-AES128-GCM-SHA256...NO (sslv3 alert handshake failure)
Testing DHE-RSA-AES128-GCM-SHA256...NO (sslv3 alert handshake failure)
Testing DHE-RSA-AES128-SHA256...NO (sslv3 alert handshake failure)
Testing DHE-DSS-AES128-SHA256...NO (sslv3 alert handshake failure)
Testing DHE-RSA-AES128-SHA...NO (sslv3 alert handshake failure)
Testing DHE-DSS-AES128-SHA...NO (sslv3 alert handshake failure)
Testing DHE-RSA-SEED-SHA...NO (sslv3 alert handshake failure)
Testing DHE-DSS-SEED-SHA...NO (sslv3 alert handshake failure)
Testing DHE-RSA-CAMELLIA128-SHA...NO (sslv3 alert handshake failure)
Testing DHE-DSS-CAMELLIA128-SHA...NO (sslv3 alert handshake failure)
Testing AECDH-AES128-SHA...NO (sslv3 alert handshake failure)
Testing SRP-AES-128-CBC-SHA...NO (sslv3 alert handshake failure)
Testing ADH-AES128-GCM-SHA256...NO (sslv3 alert handshake failure)
Testing ADH-AES128-SHA256...NO (sslv3 alert handshake failure)
Testing ADH-AES128-SHA...NO (sslv3 alert handshake failure)
Testing ADH-SEED-SHA...NO (sslv3 alert handshake failure)
Testing ADH-CAMELLIA128-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-AES128-GCM-SHA256...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-AES128-GCM-SHA256...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-AES128-SHA256...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-AES128-SHA256...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-AES128-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-AES128-SHA...NO (sslv3 alert handshake failure)
Testing AES128-GCM-SHA256...NO (sslv3 alert handshake failure)
Testing AES128-SHA256...NO (sslv3 alert handshake failure)
Testing AES128-SHA...NO (sslv3 alert handshake failure)
Testing SEED-SHA...NO (sslv3 alert handshake failure)
Testing CAMELLIA128-SHA...NO (sslv3 alert handshake failure)
Testing PSK-AES128-CBC-SHA...NO (no ciphers available)
Testing ECDHE-RSA-RC4-SHA...NO (sslv3 alert handshake failure)
Testing ECDHE-ECDSA-RC4-SHA...NO (sslv3 alert handshake failure)
Testing AECDH-RC4-SHA...NO (sslv3 alert handshake failure)
Testing ADH-RC4-MD5...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-RC4-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-RC4-SHA...NO (sslv3 alert handshake failure)
Testing RC4-SHA...NO (sslv3 alert handshake failure)
Testing RC4-MD5...NO (sslv3 alert handshake failure)
Testing PSK-RC4-SHA...NO (no ciphers available)
Testing EDH-RSA-DES-CBC-SHA...NO (sslv3 alert handshake failure)
Testing EDH-DSS-DES-CBC-SHA...NO (sslv3 alert handshake failure)
Testing ADH-DES-CBC-SHA...NO (sslv3 alert handshake failure)
Testing DES-CBC-SHA...NO (sslv3 alert handshake failure)
Testing EXP-EDH-RSA-DES-CBC-SHA...NO (sslv3 alert handshake failure)
Testing EXP-EDH-DSS-DES-CBC-SHA...NO (sslv3 alert handshake failure)
Testing EXP-ADH-DES-CBC-SHA...NO (sslv3 alert handshake failure)
Testing EXP-DES-CBC-SHA...NO (sslv3 alert handshake failure)
Testing EXP-RC2-CBC-MD5...NO (sslv3 alert handshake failure)
Testing EXP-ADH-RC4-MD5...NO (sslv3 alert handshake failure)
Testing EXP-RC4-MD5...NO (sslv3 alert handshake failure)
Testing ECDHE-RSA-NULL-SHA...NO (sslv3 alert handshake failure)
Testing ECDHE-ECDSA-NULL-SHA...NO (sslv3 alert handshake failure)
Testing AECDH-NULL-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-NULL-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-NULL-SHA...NO (sslv3 alert handshake failure)
Testing NULL-SHA256...NO (sslv3 alert handshake failure)
Testing NULL-SHA...NO (sslv3 alert handshake failure)
Testing NULL-MD5...NO (sslv3 alert handshake failure

可以在模拟器的浏览器中打开server-url并获得完美的json响应,因此我知道系统本身就有能力。

所以问题是,为什么我不能在Android 7上连接?

更新

我使用tcpdump和wireshark查看了捕获的数据包,并且启用的密码位于ClientHello中,因此这不应该是一个问题。

Cipher Suites (18 suites)

Cipher Suite: Unknown (0xcca9)
Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)
Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c)
Cipher Suite: Unknown (0xcca8)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)
Cipher Suite: TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 (0x009e)
Cipher Suite: TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 (0x009f)
Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014)
Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA (0x0033)
Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CBC_SHA (0x0039)
Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c)
Cipher Suite: TLS_RSA_WITH_AES_256_GCM_SHA384 (0x009d)
Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)
Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035)

您可以看到0xc02f0xc030匹配,但下一个TLSv1.2数据包显示为:Alert (21), Handshake Failure (40)

更新2

以下是ClientHello中Android 5.1的曲线:

Elliptic curves (25 curves)

Elliptic curve: sect571r1 (0x000e)
Elliptic curve: sect571k1 (0x000d)
Elliptic curve: secp521r1 (0x0019)
Elliptic curve: sect409k1 (0x000b)
Elliptic curve: sect409r1 (0x000c)
Elliptic curve: secp384r1 (0x0018)
Elliptic curve: sect283k1 (0x0009)
Elliptic curve: sect283r1 (0x000a)
Elliptic curve: secp256k1 (0x0016)
Elliptic curve: secp256r1 (0x0017)
Elliptic curve: sect239k1 (0x0008)
Elliptic curve: sect233k1 (0x0006)
Elliptic curve: sect233r1 (0x0007)
Elliptic curve: secp224k1 (0x0014)
Elliptic curve: secp224r1 (0x0015)
Elliptic curve: sect193r1 (0x0004)
Elliptic curve: sect193r2 (0x0005)
Elliptic curve: secp192k1 (0x0012)
Elliptic curve: secp192r1 (0x0013)
Elliptic curve: sect163k1 (0x0001)
Elliptic curve: sect163r1 (0x0002)
Elliptic curve: sect163r2 (0x0003)
Elliptic curve: secp160k1 (0x000f)
Elliptic curve: secp160r1 (0x0010)
Elliptic curve: secp160r2 (0x0011)

在ServerHello中返回secp384r1 (0x0018)

这是来自Android 7:

Elliptic curves (1 curve)

Elliptic curve: secp256r1 (0x0017)

导致握手失败。

通过删除secp384r1或用默认值(prime256v1)替换它来更改nginx配置确实可以使它工作。所以我猜问题仍然存在:我能添加椭圆曲线吗?

使用模拟器时捕获的数据与使用Android 7.0设备(通用移动4G)时的数据相同。

更新3

小更新,但值得一提:我使用Android 7.1.1(!)在模拟器中使用它。它显示以下数据(再次使用tcpdump抓取并使用wireshark查看):

Elliptic curves (3 curves)

Elliptic curve: secp256r1 (0x0017)
Elliptic curve: secp384r1 (0x0018)
Elliptic curve: secp512r1 (0x0019)

它显示了相同的18个密码套件。

8 个答案:

答案 0 :(得分:47)

这是Android 7.0中的已知回归,由Google承认并在Android 7.1.1发布之前的某个时间修复。以下是错误报告:https://code.google.com/p/android/issues/detail?id=224438

要明确的是,这里的错误是7.0只支持一条椭圆曲线:prime256v1又名secp256r1又名NIST P-256,正如Cornelis在问题中指出的那样。因此,如果您的用户遇到此问题,这些是您可以使用的解决方法(忽略您的用户理想情况下应该升级到Android 7.1.1的事实):

  • 将服务器配置为使用椭圆曲线prime256v1。例如,在Nginx 1.10中,您可以通过设置ssl_ecdh_curve prime256v1;

  • 来完成此操作
  • 如果这不起作用,请使用不依赖于椭圆曲线加密的旧密码套件(例如,DHE-RSA-AES256-GCM-SHA384)(确保您了解您的内容)。在数据安全性方面做到这一点)

注意:是椭圆曲线加密专家,请务必对我的建议的安全隐患进行自己的研究。以下是我在撰写此答案时提到的其他一些链接:

答案 1 :(得分:15)

我遇到了自签名证书的问题,问题出现在Android 7.0未接受的密码中

我跑了:openssl s_client -showcerts -connect <domain>:<port>

在我找到的结果中:

Protocol : TLSv1
Cipher   : DHE-RSA-AES256-SHA

我搜索了密码的Android Equivalent并将其添加到我的Retrofit Restadapter中:

ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
            .tlsVersions(TlsVersion.TLS_1_2)
            .cipherSuites(

CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA)
            .build();

clientBuilder.connectionSpecs(Collections.singletonList(spec));

从这里开始,每个连接都使用正确的证书固定或正确的证书但使用Android 7.0的“错误”密码。

  

一年后回顾这个答案时,我不得不承认我仍然很高兴我发布了它,另一方面,如果您能够将证书更改为正确的Cypher套件,请始终执行此操作“降级”te接受了应用中的套件。如果您必须使用自签名证书(例如嵌入式证书),这对您来说是一个可行的解决方案。

答案 2 :(得分:5)

这里是Volley的工作解决方案:

在单例代码中创建队列之前

public class VolleyServiceSingleton {

    private RequestQueue mRequestQueue;
    private HurlStack mStack;

    private VolleyServiceSingleton(){

        SSLSocketFactoryExtended factory = null;

        try {
            factory = new SSLSocketFactoryExtended();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        }


        final SSLSocketFactoryExtended finalFactory = factory;
        mStack = new HurlStack() {
            @Override
            protected HttpURLConnection createConnection(URL url) throws IOException {
                HttpsURLConnection httpsURLConnection = (HttpsURLConnection) super.createConnection(url);
                try {
                    httpsURLConnection.setSSLSocketFactory(finalFactory);
                    httpsURLConnection.setRequestProperty("charset", "utf-8");

                } catch (Exception e) {
                    e.printStackTrace();
                }
                return httpsURLConnection;
            }

        };



        mRequestQueue = Volley.newRequestQueue(YourApplication.getContext(), mStack, -1);    

    }


}

这里是SSLSocketFactoryExtended:

public class SSLSocketFactoryExtended extends SSLSocketFactory
{
    private SSLContext mSSLContext;
    private String[] mCiphers;
    private String[] mProtocols;


    public SSLSocketFactoryExtended() throws NoSuchAlgorithmException, KeyManagementException
    {
        initSSLSocketFactoryEx(null,null,null);
    }

    public String[] getDefaultCipherSuites()
    {
        return mCiphers;
    }

    public String[] getSupportedCipherSuites()
    {
        return mCiphers;
    }

    public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException
    {
        SSLSocketFactory factory = mSSLContext.getSocketFactory();
        SSLSocket ss = (SSLSocket)factory.createSocket(s, host, port, autoClose);

        ss.setEnabledProtocols(mProtocols);
        ss.setEnabledCipherSuites(mCiphers);

        return ss;
    }

    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException
    {
        SSLSocketFactory factory = mSSLContext.getSocketFactory();
        SSLSocket ss = (SSLSocket)factory.createSocket(address, port, localAddress, localPort);

        ss.setEnabledProtocols(mProtocols);
        ss.setEnabledCipherSuites(mCiphers);

        return ss;
    }

    public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException
    {
        SSLSocketFactory factory = mSSLContext.getSocketFactory();
        SSLSocket ss = (SSLSocket)factory.createSocket(host, port, localHost, localPort);

        ss.setEnabledProtocols(mProtocols);
        ss.setEnabledCipherSuites(mCiphers);

        return ss;
    }

    public Socket createSocket(InetAddress host, int port) throws IOException
    {
        SSLSocketFactory factory = mSSLContext.getSocketFactory();
        SSLSocket ss = (SSLSocket)factory.createSocket(host, port);

        ss.setEnabledProtocols(mProtocols);
        ss.setEnabledCipherSuites(mCiphers);

        return ss;
    }

    public Socket createSocket(String host, int port) throws IOException
    {
        SSLSocketFactory factory = mSSLContext.getSocketFactory();
        SSLSocket ss = (SSLSocket)factory.createSocket(host, port);

        ss.setEnabledProtocols(mProtocols);
        ss.setEnabledCipherSuites(mCiphers);

        return ss;
    }

    private void initSSLSocketFactoryEx(KeyManager[] km, TrustManager[] tm, SecureRandom random)
            throws NoSuchAlgorithmException, KeyManagementException
    {
        mSSLContext = SSLContext.getInstance("TLS");
        mSSLContext.init(km, tm, random);

        mProtocols = GetProtocolList();
        mCiphers = GetCipherList();
    }

    protected String[] GetProtocolList()
    {
        String[] protocols = { "TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"};
        String[] availableProtocols = null;

        SSLSocket socket = null;

        try
        {
            SSLSocketFactory factory = mSSLContext.getSocketFactory();
            socket = (SSLSocket)factory.createSocket();

            availableProtocols = socket.getSupportedProtocols();
        }
        catch(Exception e)
        {
            return new String[]{ "TLSv1" };
        }
        finally
        {
            if(socket != null)
                try {
                    socket.close();
                } catch (IOException e) {
                }
        }

        List<String> resultList = new ArrayList<String>();
        for(int i = 0; i < protocols.length; i++)
        {
            int idx = Arrays.binarySearch(availableProtocols, protocols[i]);
            if(idx >= 0)
                resultList.add(protocols[i]);
        }

        return resultList.toArray(new String[0]);
    }

    protected String[] GetCipherList()
    {
        List<String> resultList = new ArrayList<String>();
        SSLSocketFactory factory = mSSLContext.getSocketFactory();
        for(String s : factory.getSupportedCipherSuites()){
            Log.e("CipherSuite type = ",s);
            resultList.add(s);
        }
        return resultList.toArray(new String[resultList.size()]);
    }

}

在这个代码中我简单地添加了设备支持的所有密码,对我来说这个工作),可能会帮助某人)干杯)

P.S。无需在清单中添加安全网络配置参数。

答案 3 :(得分:2)

  

默认情况下,所有应用程序的安全连接(使用TLS和HTTPS等协议)都信任预安装的系统CA,而面向Android 6.0(API级别23)及更低版本的应用程序默认也信任用户添加的CA存储。

这意味着在Android Nougat(7.0)上,CA的游戏完全改变了。如果您拥有密钥证书,则可以添加网络安全配置文件(如果您有证书),如下所述: https://developer.android.com/training/articles/security-config.html

或者您可以创建自己的TrustManager,如下所述: https://developer.android.com/training/articles/security-ssl.html#SelfSigned

或者您可以启用服务器需要但在Android N上默认未启用的密码套件。例如,以下是我需要在自己的应用程序中添加的两个密码,与旧的Windows CE服务器通信:

    SSLSocket sslsock = (SSLSocket) createSocket();
    List<String> cipherSuitesToEnable = new ArrayList<>();
    cipherSuitesToEnable.add("SSL_RSA_WITH_RC4_128_SHA");
    cipherSuitesToEnable.add("SSL_RSA_WITH_3DES_EDE_CBC_SHA");
    sslsock.setEnabledCipherSuites(cipherSuitesToEnable.toArray(new String[cipherSuitesToEnable.size()]));

答案 4 :(得分:2)

我已经使用它来修复“ javax.net.ssl.SSLHandshakeException:握手失败”错误,并且在Android 7.0和其他版本上也可以正常工作。

将其放在onCreate()类的application方法中。

fun initializeSSLContext(mContext: Context) {
            try {
                SSLContext.getInstance("TLSv1.2")
            } catch (e: NoSuchAlgorithmException) {
                e.printStackTrace()
            }

            try {
                ProviderInstaller.installIfNeeded(mContext.applicationContext)
            } catch (e: GooglePlayServicesRepairableException) {
                e.printStackTrace()
            } catch (e: GooglePlayServicesNotAvailableException) {
                e.printStackTrace()
            }

        }

答案 5 :(得分:1)

同样在这里。我的Nginx服务器使用sll_ecdh_curve prime384v1设置。不幸的是,由于客户的严密性政策,后端人员不允许我根据Vicky Chijwani的指示配置Nginx服务器。我曾尝试使用Valley和最新版本的OkHttp库,但它没有帮助。 为了绕过这个bug,我不得不使用WebView与Adroid 7.0设备上的API服务进行通信。这是我的Adapter类。我希望其他人会发现它很有用。

/**
* Connection to API service using WebView (for Android 7.0 devices)
*
* Created by fishbone on 09.08.17.
*/
@RequiresApi(api = Build.VERSION_CODES.N)
class WebViewHttpsConnection extends ApiConnection {

    private static final long TIMEOUT = 30000;
    private static final String POST_DATA_SCRIPT = "javascript:(function (){" +
            "var xhr = new XMLHttpRequest();\n" +
            "xhr.open(\"POST\", \"%1$s\", true);\n" +
            "xhr.setRequestHeader(\"Content-type\", \"application/json\");\n" +
            "xhr.onreadystatechange = function () {\n" +
            "    if (xhr.readyState === 4) {\n" +
            "        listener.onResult(xhr.status, xhr.responseText);\n" +
            "    }\n" +
            "};\n" +
            "xhr.send('%2$s');\n" +
            "})();";

    WebViewHttpsConnection(Context context) {
        super(context);
    }
    /**
     *  Send data to API Service.
     *
     * @param url URL of API Service
     * @param request JSON Object serialized into String
     * @return API response
     * @throws IOException errors
     */
    @Override
    public String sendData(final URL url, final String request) throws IOException {
        // We should escape backslashes in JSON because JS unescape it back
        final String javaScript = String.format(POST_DATA_SCRIPT, url.toString(),
                request.replace("\\", "\\\\"));
        final RequestResultListener listener = new RequestResultListener();
        // We must use WebView only from 'main' Thread, therefore I using Handler with Application context
        Handler handler = new Handler(getContext().getApplicationContext().getMainLooper());
        handler.post(new Runnable() {

            @SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface"}) // JavaScript is only for me and I'll use it only on Android 7.0 devices, so not scary
            @Override
            public void run() {
                // WebView must be created, configured and called from the same Thread
                final WebView webView = new WebView(getContext(), null);
                webView.getSettings().setJavaScriptEnabled(true);
                webView.addJavascriptInterface(listener, "listener");
                webView.setWebViewClient(new WebViewClient() {

                    @Override
                    public void onPageFinished(WebView view, String url) {
                        // As soon as loaded any page from target domain, we call JS-script that will make POST request
                        webView.loadUrl(javaScript);
                    }
                });
                // I cant use postUrl() method because WebView doesn't allow to define 'Content-type' header, but my API service accepts only 'application/json' content type
                // To complete CORS requirements we make any GET request to lets WebView navigate to the target domain, otherwise it will send 'null' as 'Origin' in headers
                webView.loadUrl(url.toString());
            }
        });
        // And further we waiting for response of API service
        try {
            if (!listener.latch.await(TIMEOUT, TimeUnit.MILLISECONDS)) {
                throw new IOException("Timeout connection to server");
            }
        } catch (InterruptedException e) {
            throw new IOException("Connection to server was interrupted");
        }
        if (listener.code != HttpURLConnection.HTTP_OK) {
            throw new HttpRetryException("Server return error code " + listener.code,
                    listener.code);
        }
        if (TextUtils.isEmpty(listener.result)) {
            throw new HttpRetryException("Service return empty response", listener.code);
        }
        return listener.result;
    }
    /**
     * Callback interface for receiving API Service response from JavaScript inside WebView
     */
    private static class RequestResultListener {

        CountDownLatch latch = new CountDownLatch(1);
        String result = null;
        int code;

        @JavascriptInterface
        public void onResult(int code, String result) {
            this.result = result;
            this.code = code;
            latch.countDown();
        }
    }
}

答案 6 :(得分:1)

我花了4天时间解决此问题,并尝试了所有问题,而我遇到的问题是使用 LetsEncrypt (certbot等)来生成我的证书。

一旦我切换到其他CA,就会开始收到Android 7.0请求。

答案 7 :(得分:0)

我最终不得不实施与Nikolay类似的解决方案,因为其他解决方案都无法解决该问题(尽管管理员确实尝试在不损害安全性的前提下对其进行了修复,但我没有访问API服务器的权限)。

我将解决方案写为一个类,您应该可以将其插入到您的应用中-如下所示:

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.webkit.ValueCallback;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import com.google.gson.Gson;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class BackupAPIService
{
    private static final String TAG = "BackupAPIService";
    private static BackupAPIService sService = null;

    // A context is required in order to run the webview.
    private Context mContext;
    // Headers for the request are stored here - they can be set, added to and removed from.
    private HashMap<String, String> mHeaders = new HashMap<>();
    // This sets the current location of the webview - it is probably best. although generally unnecessary to set this to the main page of the API.
    // However not setting it at all will throw 'no access-control-allow-origin header is present' errors.
    private String mOriginURL = "";
    // Setting a base url to the domain of the API means that any requests don't need to include this beginning of the url,
    // This must remain the same for all requests.
    private String mBaseURL = "";

    public static final int GET = 0, POST = 1;

    // NOTE
    // Please note that a singleton pattern has been used, assuming all calls are made to the same API.
    // If you are using several APIs, you may wish to modify this service to give a separate BackupAPIService object for each.
    // If you are using an unknown number of APIs, you may wish to make the constructor public and remove the static methods.
    // The datatype returned by the API is assumed to be in JSON format - if you wish to change this, you will need to manually edit line 9 of the ajaxRequest String in the GenerateRequest method.


    /**
     * This returns the created BackupAPIService if it exists, and creates a new one otherwise. However the new one will need to be set up.
     * @param context is the context in which to create the WebView. This can include the context from a service.
     * @return the current instance of the BackupAPIService if it exists, otherwise it creates a new one.
     */

    public static BackupAPIService getService(Context context)
    {
        if(sService == null)
        {
            sService = new BackupAPIService(context);
        }
        return sService;
    }

    /**
     * This allows the headers to be set for this instance of the BackupAPIService.
     * @param context is the context in which to create the WebView. This can include the context from a service.
     * @param headers are the custom headers to be sent with this request. If these already exist in the service, they will be updated. Otherwise they will be added onto the list. Please note that String headers and values should be surrounded by single quotes, ie. 'header'. To reset the headers run ResetService.
     * @return the current instance of the BackupAPIService if it exists, otherwise it creates a new one.
     */

    public static BackupAPIService getService(Context context, HashMap<String, String> headers)
    {
        return getService(context).addHeaders(headers);
    }

    /**
     * This allows the headers and the originURL to be set for this instance of the BackupAPIService.
     * @param context is the context in which to create the WebView. This can include the context from a service.
     * @param headers are the custom headers to be sent with this request. If these already exist in the service, they will be updated. Otherwise they will be added onto the list. Please note that String headers and values should be surrounded by single quotes, ie. 'header'. To reset the headers run ResetService.
     * @param originURL is the URL from which the WebView will make its requests. This should be set, as the API will throw a 'no access-control-allow-origin header is present' error otherwise.
     * @return the current instance of the BackupAPIService if it exists, otherwise it creates a new one.
     */

    public static BackupAPIService getService(Context context, HashMap<String, String> headers, String originURL)
    {
        return getService(context).addHeaders(headers).addOriginURL(originURL);
    }

    /**
     * This allows the headers and the originURL to be set for this instance of the BackupAPIService.
     * @param context is the context in which to create the WebView. This can include the context from a service.
     * @param headers are the custom headers to be sent with this request. If these already exist in the service, they will be updated. Otherwise they will be added onto the list. Please note that String headers and values should be surrounded by single quotes, ie. 'header'. To reset the headers run ResetService.
     * @param originURL is the URL from which the WebView will make its requests. This should be set, as the API will throw a 'no access-control-allow-origin header is present' error otherwise.
     * @param baseURL is the part of the API URL that never changes. This will allow you to only pass in the different URL endings as required, saving a lot of writing.
     * @return the current instance of the BackupAPIService if it exists, otherwise it creates a new one.
     */

    public static BackupAPIService getService(Context context, HashMap<String, String> headers, String originURL, String baseURL)
    {
        return getService(context).addHeaders(headers).addOriginURL(originURL).addBaseURL(baseURL);
    }

    /**
     * This resets all of the parameters of the service to their defaults as desired.
     * @param resetHeaders clears the headers if set to true.
     * @param resetBaseURL clears the baseURL if set to true.
     * @param resetOriginURL clears the originURL if set to true.
     */

    public static void ResetService(boolean resetHeaders, boolean resetBaseURL, boolean resetOriginURL)
    {
        if(sService != null)
        {
            if(resetHeaders) sService.mHeaders = new HashMap<>();
            if(resetBaseURL) sService.mBaseURL = "";
            if(resetOriginURL) sService.mOriginURL = "";
        }
    }

    /**
     * This is the basic constructor for the API.
     * @param context is the context in which to create the WebView. This can include the context from a service.
     */

    private BackupAPIService(Context context)
    {
        this.mContext = context;
    }

    /**
     * This allows the headers to be updated with the headers in the object that is sent. Existing headers in the list will be updated with their new values, and new headers will be added. Please note that String headers and values should be surrounded by single quotes, ie. 'header'. This can be done programmatically using the getCompatibleString method.
     * @param headers are the headers being added.
     * @return the current BackupAPIService object.
     */

    public BackupAPIService addHeaders(HashMap<String, String> headers)
    {
        this.mHeaders.putAll(headers);
        return this;
    }

    /**
     * This allows a single header to be added if it does not exist, or updated if it does.
     * @param key is the name of the header. Please note that String headers and values should be surrounded by single quotes, ie. 'header'.
     * @param value is the value of the header as a String.
     * @return the current BackupAPIService object.
     */

    public BackupAPIService addHeader(String key, String value)
    {
        this.mHeaders.put(key, value);
        return this;
    }

    /**
     * This allows a single header to be removed if it exists in the service
     * @param key is the name of the header
     * @return the current BackupAPIService object
     */

    public BackupAPIService removeHeader(String key)
    {
        this.mHeaders.remove(key);
        return this;
    }

    /**
     * This returns whether a variable with a given name exists in the Service
     * @param key is the name of the header
     * @return the current BackupAPIService object
     */

    public boolean getHeaderExists(String key)
    {
        return mHeaders.containsKey(key);
    }

    /**
     * This sets the originURL, which tells the API which website the request is coming from. If this is not set, the request is likely to be blocked.
     * @param originURL is the URL stating which website the request is coming from: you may wish to set this to the main website address of your API.
     * @return the current BackupAPIService object
     */

    public BackupAPIService addOriginURL(String originURL)
    {
        mOriginURL = originURL;
        return this;
    }

    /**
     * This sets the baseURL. If there is a common URL beginning for all your API calls, you can set it here and the just send the rest of the address in your calls to the Post or Get methods.
     * @param baseURL will be added onto the beginning of all of the API URL requests, allowing you to avoid having to write this each time, and send shorter requests.
     * @return the current BackupAPIService object
     */

    public BackupAPIService addBaseURL(String baseURL)
    {
        mBaseURL = baseURL;
        return this;
    }

    /**
     * This allows a post request to be sent, with the parameters as a hashmap. Please note that String parameters should take the format String param = "'{param}'", so that ajax recognises the String as a String - the getCompatibleString method can be used to automatically adds these single quotes to a given String.
     * @param URL is the url of the request - this could just be the second half of the url, if the first half has been set as the baseURL.
     * @param parameters is a hashmap of the parameters.
     * @param ajaxHandler allows you to define your own custom response with the returned information.
     */

    public void Post(String URL, HashMap<String, String> parameters, AjaxHandler ajaxHandler)
    {
        Launch(POST, URL, ajaxHandler, parameters);
    }

    /**
     * This allows a get request to be sent.
     * @param URL is the url of the request - this could just be the second half of the url, if the first half has been set as the baseURL.
     * @param ajaxHandler allows you to define your own custom response with the returned information.
     */

    public void Get(String URL, AjaxHandler ajaxHandler)
    {
        Launch(GET, URL, ajaxHandler);
    }

    /**
     * This is used by the public Post and Get methods to launch a request.
     * @param launchType is defined as either post or get by the POST and GET static constants.
     * @param URL is the url of the request - this could just be the second half of the url, if the first half has been set as the baseURL.
     * @param ajaxHandler allows you to define your own custom response with the returned information.
     * @param parameters is an optional hashmap of the parameters for a post request.
     */

    private void Launch(final int launchType, final String URL, final AjaxHandler ajaxHandler, final  HashMap<String, String>  ... parameters)
    {
        // This piece of code is required in order to allow the WebView to run from a service without throwing errors
        Handler handler = new Handler(Looper.getMainLooper());
        try
        {
            handler.post(
                    new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            GenerateRequest(launchType, URL, ajaxHandler, parameters);
                        }
                    });
        } catch (Exception e)
        {
            e.printStackTrace();
        }
    }

    /**
     * This method generates the actual request.
     * @param launchType is defined as either post or get by the POST and GET static constants.
     * @param URL is the url of the request - this could just be the second half of the url, if the first half has been set as the baseURL.
     * @param ajaxHandler allows you to define your own custom response with the returned information.
     * @param parameters is an optional hashmap of the parameters for a post request.
     */

    private void GenerateRequest(int launchType, String URL, AjaxHandler ajaxHandler, HashMap<String, String> ... parameters)
    {
        String importAjax = "<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script>";
        String customiseAjaxHeaders = "$.ajaxSetup({headers: { ";
        for (Map.Entry<String, String> entry : mHeaders.entrySet())
        {
            customiseAjaxHeaders += entry.getKey() + ": " + entry.getValue() + ", ";
        }
        customiseAjaxHeaders = customiseAjaxHeaders.substring(0, customiseAjaxHeaders.length()-2) + "}});";
        String postParameters = "";
        if(parameters.length>0)
        {
            for (Map.Entry<String, String> entry : parameters[0].entrySet())
            {
                postParameters += entry.getKey() + ": " + entry.getValue() + ", ";
            }
            postParameters = postParameters.substring(0, postParameters.length()-2);
        }
        //String origin = "'app.cleopatra.im'";
        String requestAddress = "'"+mBaseURL + URL + "'";
        String requestType = "Get";
        if(launchType == POST) requestType = "Post";
        String ajaxRequest = customiseAjaxHeaders + " var saveData = " +
                "$.ajax" +
                "(" +
                    "{" +
                        "type: '" + requestType + "'," +
                        "url: " + requestAddress + ", " +
                        "data: " +
                        "{" + postParameters + "}," +
                        "dataType: 'json'," +
                        "success: function(data)" +
                        "{" +
                            "ajaxHandler.handleResults(JSON.stringify(data));" + // This runs the ajax handler created below when the handler successfully returns data
                        "}," +
                        "error:function(request, status)" +
                        "{" +
                            "ajaxHandler.handleFailure('Request Failed: ' + JSON.stringify(request) + ' due to: ' + JSON.stringify(status));" + // This runs the ajax handler created below when the handler unsuccessfully returns data
                        "}" +
                    "}" +
                ");";

        CreateRequestThroughWebView(importAjax, ajaxRequest, ajaxHandler);
    }

    /**
     * This is used to create the generated request through a webview object
     * @param content is the html content of the webview - in this implementation it is currently just an import script for JQuery
     * @param request is the Ajax request script to be run on the webview
     * @param ajaxHandler allows you to define your own custom response with the returned information.
     */

    private void CreateRequestThroughWebView(String content, final String request, AjaxHandler ajaxHandler)
    {
        Log.i(TAG, "Content: " + content + "\nRequest: " + request);
        HashMap<String, String> headers = new HashMap<>();
        // create the new webview - this can run invisibly
        WebView webView = new WebView(mContext);
        webView.getSettings().setAllowUniversalAccessFromFileURLs(true);
        // This creates a webpage at the expected location, which can be accept AJAX commands
        webView.loadDataWithBaseURL(mOriginURL, content, "text/html; charset=utf-8", "utf-8", mOriginURL);
        // Allow JavaScript to run on the page
        webView.getSettings().setJavaScriptEnabled(true);
        // Add a JavaScript interface allowing completed AJAX requests to run Java methods
        webView.addJavascriptInterface(ajaxHandler, "ajaxHandler");

        // override onPageFinished method of WebViewClient to handle AJAX calls
        webView.setWebViewClient(new WebViewClient()
        {
            @Override
            public void onPageFinished(WebView view, String url)
            {
                super.onPageFinished(view, url);
                // Run the JavaScript command once the page has loaded
                view.evaluateJavascript(request, new ValueCallback<String>()
                {
                    @Override
                    public void onReceiveValue(String s)
                    {
                        Log.i(TAG, "Request Completed: " + s);
                    }
                });
            }
        });
    }

    /**
     * This adds additional single quotes to enclose a string representing a header name, header value, variable name or variable value, so it is still recognised as a String when it is passed into Ajax.
     * @param string is the header name, header value, variable name or variable value to be modified.
     * @return an ajax compatible String.
     */

    public static String getCompatibleString(String string)
    {
        return "'" + string + "'";
    }

    /**
     * This adds additional single quotes to enclose two strings representing either a String header name and its value, or posted variable name and its value, so that they are both still recognised as Strings when they are passed into Ajax.
     * @param name is the header or variable name, header value, variable name or variable value to be modified.
     * @param value is the header or variable value to be modified.
     * @return an ajax compatible HashMap which can also be added into any existing HashMaps as necessary.
     */

    public static HashMap<String, String> getCompatibleHashMapEntry(String name, String value)
    {
        HashMap<String, String> result = new HashMap<String, String>();
        result.put(getCompatibleString(name), getCompatibleString(value));
        return result;
    }

    /**
     * This adds additional single quotes to enclose an arbitrary number of two string arrays, each representing String header name and its value, or a posted variable name and its value so that they are both still recognised as Strings when they are passed into Ajax. This is returned as a hashmap ready to be set as the necessary headers or variables for the request.
     * @param nameValuePairs is the header or variable name, header value, variable name or variable value to be modified.
     * @return an ajax compatible HashMapEntry.
     */

    public static HashMap<String, String> getCompatibleHashMap(String[] ... nameValuePairs)
    {
        HashMap<String, String> results = new HashMap<>();
        for (String[] nameValuePair: nameValuePairs)
        {
            results.put(getCompatibleString(nameValuePair[0]), getCompatibleString(nameValuePair[1]));
        }
        return results;
    }



    /**
     * This deals with the results of the Ajax API request.
     * The handleResults method of this abstracted Ajax Javascript Interface should be implemented in order to define your app's behaviour when the request completes.
     * The ConvertResultToObject method can also be used within the handleResults method in order to convert the received json string into the corresponding class.
     * The handleFailure method is called if the Ajax request fails.
     */

    public abstract static class AjaxHandler
    {
        private static final String TAG = "ajaxHandler";
        private final Context context;

        public AjaxHandler(Context context)
        {
            this.context = context;
        }

        /**
         * Overwrite this method to handle the response to your request
         * @param results is a String representation of the result from the WebView Query
         */

        @android.webkit.JavascriptInterface
        public abstract void handleResults(String results);

        /**
         * Overwrite this method to handle any failed requests
         * @param message is a String representation of the failure message from the WebView Query
         */


        @android.webkit.JavascriptInterface
        public abstract void handleFailure(String message);

        /**
         * This should generate a predefined class object from a JSON response. It was not used in the final implementation, so it has not been tested and may need tweaking
         * @param json is the json representation of the class
         * @param classOfT is the class of the object to be populated
         * @return an object of class T created from the JSON string
         */

        public <T> T ConvertResultToObject(String json, Class<T> classOfT)
        {
            Gson gson = new Gson();
            return gson.fromJson(json, classOfT);
        }
    }
}

示例实现如下:

// Example Ajax Implementation


// Setup basic variables


// Set up context
Context context = this.context;
// Set the beginning of the URL which is the same for all API requests
String API_URL = "https://mywebsite.com/myapi/";
// Set up the url from which requests should originate
String BASE_URL = "www.mywebsite.com";

// Set up variables to pass into the requests
// Header names and values
String API_KEY_NAME = "{API_KEY_NAME}";
String API_KEY_VALUE = "{API_KEY_VALUE}";
// Parameter names and values
String USER_NAME = "User";
String USER_VALUE = "{User name}";
String PASSWORD_NAME = "Password";
String PASSWORD_VALUE = "{User Password}";
String USER_ID_NAME = "User_id";
int USER_ID_VALUE = 7;
// Note that Strings need an extra set of single quotes to be recognised as a String when passed into Ajax. Methods have been provided which do that.
// Get the headers as a HashMap, and convert both name and value to an Ajax compatible String
HashMap<String, String> headers = BackupAPIService.getCompatibleHashMapEntry(API_KEY_NAME, API_KEY_VALUE);
// Get the parameters as a HashMap, and convert the appropriate names and values to Ajax compatible Strings
HashMap<String, String> parameters = BackupAPIService.getCompatibleHashMap(new String[] {USER_NAME, USER_VALUE}, new String[] {PASSWORD_NAME, PASSWORD_VALUE});
// Add the parameters where the values are not Strings
parameters.put(BackupAPIService.getCompatibleString(USER_ID_NAME), USER_ID_VALUE+"");
// Create or get service
BackupAPIService backupService = BackupAPIService.getService(context, headers, BASE_URL, API_URL);
// Post a request with the necessary addition to the API url set earlier for this request, the parameters set above, and an implementation of AjaxHandler which handles the results and failures as you desire
backupService.Post("user", parameters, new BackupAPIService.AjaxHandler(context){
    @Override
    public void handleResults(String results)
    {
        Log.e(TAG, "Success!! " + results);
    }

    @Override
    public void handleFailure(String message)
    {
        Log.e(TAG, "Failure :( " + message);
    }
});