IT이야기

SSLHandshakeException: Android N/7.0에서 핸드셰이크 실패

cyworld 2021. 10. 10. 13:59
반응형

SSLHandshakeException: Android N/7.0에서 핸드셰이크 실패


저는 (파워) 사용자가 백엔드 애플리케이션을 실행하기 위해 자신의 서버(예: nginx)를 설정해야 하는 앱을 개발 중입니다. 앱에서 해당 도메인을 구성해야 연결할 수 있습니다. 저는 주로 제 휴대폰(sony z3c)에서 테스트해 왔으며 5.1용 개발을 시작했습니다. 나중에 6.0에 대한 업데이트를 받았지만 여전히 에뮬레이터 내부에서 작동하는 5.1을 유지했습니다. 얼마 전에 7.0용 이미지로 AVD 작업을 시작했는데 놀랍게도 ssl 핸드셰이크가 실패했다고 알려주면서 내 서버에 연결되지 않습니다. 내 nginx 구성은 꽤 엄격하지만 5.1과 6.0 모두에서 작동하므로 .... ?!

내가 아는 것은 다음과 같습니다.

  • 지원 라이브러리에 v24를 사용합니다. 즉, compileSdkVersion은 24입니다.
  • 발리 v1.0.0을 사용 합니다.
  • TLSSocketFactory 시도했지만 아무 것도 변경되지 않습니다. 이것은 어쨌든 이전 SDK 버전에 대한 SSL3 사용을 방지하기 위해 대부분 사용되는 것 같습니다.
  • 시간 초과를 늘리려고 시도했지만 아무 것도 변경되지 않았습니다.
  • HttpURLConnection을 직접 사용해 보았지만 스택 추적을 제외하고는 아무 것도 변경하지 않았습니다(발리 참조가 없지만 그렇지 않으면 동일함).

TLSSocketFactory가 없으면 요청은 Volley.newRequestQueue(context).

이것은 내가 안드로이드 스튜디오에서 본 것입니다:

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's encrypt 인증서를 사용하고 다음 구성을 가지고 있습니다.

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;

이 암호를 테스트하기 위해 스크립트가 있고 이 암호를 확인합니다(서버 네트워크 외부의 칙칙한 vps에서 실행).

ECDHE-RSA-AES256-GCM-SHA384 테스트 중...예
ECDHE-ECDSA-AES256-GCM-SHA384...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDHE-RSA-AES256-SHA384...NO(sslv3 경고 핸드셰이크 실패) 테스트
ECDHE-ECDSA-AES256-SHA384...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDHE-RSA-AES256-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDHE-ECDSA-AES256-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
테스트 SRP-DSS-AES-256-CBC-SHA...NO(sslv3 경고 핸드셰이크 실패)
SRP-RSA-AES-256-CBC-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
테스트 DHE-DSS-AES256-GCM-SHA384...NO(sslv3 경고 핸드셰이크 실패)
테스트 DHE-RSA-AES256-GCM-SHA384...NO(sslv3 경고 핸드셰이크 실패)
테스트 DHE-RSA-AES256-SHA256...NO(sslv3 경고 핸드셰이크 실패)
테스트 DHE-DSS-AES256-SHA256...NO(sslv3 경고 핸드셰이크 실패)
테스트 DHE-RSA-AES256-SHA...NO(sslv3 경고 핸드셰이크 실패)
테스트 DHE-DSS-AES256-SHA...NO(sslv3 경고 핸드셰이크 실패)
테스트 DHE-RSA-CAMELLIA256-SHA...NO(sslv3 경고 핸드셰이크 실패)
테스트 DHE-DSS-CAMELLIA256-SHA...NO(sslv3 경고 핸드셰이크 실패)
AECDH-AES256-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
테스트 SRP-AES-256-CBC-SHA...NO(sslv3 경고 핸드셰이크 실패)
테스트 ADH-AES256-GCM-SHA384...NO(sslv3 경고 핸드셰이크 실패)
테스트 ADH-AES256-SHA256...NO(sslv3 경고 핸드셰이크 실패)
테스트 ADH-AES256-SHA...NO(sslv3 경고 핸드셰이크 실패)
테스트 ADH-CAMELLIA256-SHA...NO(sslv3 경고 핸드셰이크 실패)
ECDH-RSA-AES256-GCM-SHA384...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDH-ECDSA-AES256-GCM-SHA384...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDH-RSA-AES256-SHA384...NO(sslv3 경고 핸드셰이크 실패) 테스트
ECDH-ECDSA-AES256-SHA384...NO(sslv3 경고 핸드셰이크 실패) 테스트
ECDH-RSA-AES256-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDH-ECDSA-AES256-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
테스트 AES256-GCM-SHA384...NO(sslv3 경고 핸드셰이크 실패)
테스트 AES256-SHA256...NO(sslv3 경고 핸드셰이크 실패)
테스트 AES256-SHA...NO(sslv3 경고 핸드셰이크 실패)
테스트 CAMELLIA256-SHA...NO(sslv3 경고 핸드셰이크 실패)
PSK-AES256-CBC-SHA...NO 테스트 중(사용 가능한 암호 없음)
ECDHE-RSA-DES-CBC3-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDHE-ECDSA-DES-CBC3-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
테스트 SRP-DSS-3DES-EDE-CBC-SHA...NO(sslv3 경고 핸드셰이크 실패)
SRP-RSA-3DES-EDE-CBC-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
EDH-RSA-DES-CBC3-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
EDH-DSS-DES-CBC3-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
AECDH-DES-CBC3-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
테스트 SRP-3DES-EDE-CBC-SHA...NO(sslv3 경고 핸드셰이크 실패)
ADH-DES-CBC3-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDH-RSA-DES-CBC3-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDH-ECDSA-DES-CBC3-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
DES-CBC3-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
PSK-3DES-EDE-CBC-SHA...NO 테스트(사용 가능한 암호 없음)
ECDHE-RSA-AES128-GCM-SHA256 테스트 중...예
ECDHE-ECDSA-AES128-GCM-SHA256...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDHE-RSA-AES128-SHA256...NO(sslv3 경고 핸드셰이크 실패) 테스트 중
ECDHE-ECDSA-AES128-SHA256...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDHE-RSA-AES128-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDHE-ECDSA-AES128-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
테스트 SRP-DSS-AES-128-CBC-SHA...NO(sslv3 경고 핸드셰이크 실패)
SRP-RSA-AES-128-CBC-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
테스트 DHE-DSS-AES128-GCM-SHA256...NO(sslv3 경고 핸드셰이크 실패)
테스트 DHE-RSA-AES128-GCM-SHA256...NO(sslv3 경고 핸드셰이크 실패)
테스트 DHE-RSA-AES128-SHA256...NO(sslv3 경고 핸드셰이크 실패)
테스트 DHE-DSS-AES128-SHA256...NO(sslv3 경고 핸드셰이크 실패)
테스트 DHE-RSA-AES128-SHA...NO(sslv3 경고 핸드셰이크 실패)
테스트 DHE-DSS-AES128-SHA...NO(sslv3 경고 핸드셰이크 실패)
DHE-RSA-SEED-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
테스트 DHE-DSS-SEED-SHA...NO(sslv3 경고 핸드셰이크 실패)
테스트 DHE-RSA-CAMELLIA128-SHA...NO(sslv3 경고 핸드셰이크 실패)
테스트 DHE-DSS-CAMELLIA128-SHA...NO(sslv3 경고 핸드셰이크 실패)
AECDH-AES128-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
테스트 SRP-AES-128-CBC-SHA...NO(sslv3 경고 핸드셰이크 실패)
테스트 ADH-AES128-GCM-SHA256...NO(sslv3 경고 핸드셰이크 실패)
테스트 ADH-AES128-SHA256...NO(sslv3 경고 핸드셰이크 실패)
테스트 ADH-AES128-SHA...NO(sslv3 경고 핸드셰이크 실패)
ADH-SEED-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
테스트 ADH-CAMELLIA128-SHA...NO(sslv3 경고 핸드셰이크 실패)
ECDH-RSA-AES128-GCM-SHA256...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDH-ECDSA-AES128-GCM-SHA256...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDH-RSA-AES128-SHA256...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDH-ECDSA-AES128-SHA256...NO(sslv3 경고 핸드셰이크 실패) 테스트 중
ECDH-RSA-AES128-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDH-ECDSA-AES128-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
테스트 AES128-GCM-SHA256...NO(sslv3 경고 핸드셰이크 실패)
테스트 AES128-SHA256...NO(sslv3 경고 핸드셰이크 실패)
테스트 AES128-SHA...NO(sslv3 경고 핸드셰이크 실패)
SEED-SHA...NO 테스트 중(sslv3 경고 핸드셰이크 실패)
테스트 CAMELLIA128-SHA...NO(sslv3 경고 핸드셰이크 실패)
PSK-AES128-CBC-SHA...NO 테스트 중(사용 가능한 암호 없음)
ECDHE-RSA-RC4-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDHE-ECDSA-RC4-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
AECDH-RC4-SHA...NO 테스트 중(sslv3 경고 핸드셰이크 실패)
테스트 ADH-RC4-MD5...NO(sslv3 경고 핸드셰이크 실패)
ECDH-RSA-RC4-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDH-ECDSA-RC4-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
RC4-SHA...NO 테스트 중(sslv3 경고 핸드셰이크 실패)
테스트 RC4-MD5...NO(sslv3 경고 핸드셰이크 실패)
PSK-RC4-SHA...NO 테스트 중(사용 가능한 암호 없음)
EDH-RSA-DES-CBC-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
EDH-DSS-DES-CBC-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
ADH-DES-CBC-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
DES-CBC-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
EXP-EDH-RSA-DES-CBC-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
EXP-EDH-DSS-DES-CBC-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
EXP-ADH-DES-CBC-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
EXP-DES-CBC-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
EXP-RC2-CBC-MD5...NO(sslv3 경고 핸드셰이크 실패) 테스트 중
EXP-ADH-RC4-MD5...NO(sslv3 경고 핸드셰이크 실패) 테스트 중
EXP-RC4-MD5...NO 테스트 중(sslv3 경고 핸드셰이크 실패)
ECDHE-RSA-NULL-SHA...NO(sslv3 경고 핸드셰이크 실패) 테스트
ECDHE-ECDSA-NULL-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
AECDH-NULL-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDH-RSA-NULL-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
ECDH-ECDSA-NULL-SHA...NO 테스트(sslv3 경고 핸드셰이크 실패)
테스트 NULL-SHA256...NO(sslv3 경고 핸드셰이크 실패)
NULL-SHA...NO 테스트 중(sslv3 경고 핸드셰이크 실패)
테스트 NULL-MD5...NO(sslv3 경고 핸드셰이크 실패

내가 할 수 에뮬레이터의 브라우저에서 서버 URL을 열고 I 자체가 할 수있는 시스템을 알 수 있도록 완벽한 JSON 응답을 얻을.

그래서 질문은 Android 7에서 연결할 수 없는 이유입니다.

업데이트 :

tcpdump 및 wireshark를 사용하여 캡처된 패킷을 보았고 활성화된 암호가 ClientHello에 있으므로 문제가 되지 않습니다.

암호 스위트 (18 스위트)

암호 모음: 알 수 없음(0xcca9)
암호 제품군: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256(0xc02b)
암호 제품군: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384(0xc02c)
암호 모음: 알 수 없음(0xcca8)
암호 제품군: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256(0xc02f)
암호 제품군: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384(0xc030)
암호 제품군: TLS_DHE_RSA_WITH_AES_128_GCM_SHA256(0x009e)
암호 제품군: TLS_DHE_RSA_WITH_AES_256_GCM_SHA384(0x009f)
암호 제품군: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA(0xc009)
암호 제품군: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA(0xc00a)
암호 제품군: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA(0xc013)
암호 제품군: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA(0xc014)
암호 제품군: TLS_DHE_RSA_WITH_AES_128_CBC_SHA(0x0033)
암호 제품군: TLS_DHE_RSA_WITH_AES_256_CBC_SHA(0x0039)
암호 제품군: TLS_RSA_WITH_AES_128_GCM_SHA256(0x009c)
암호 제품군: TLS_RSA_WITH_AES_256_GCM_SHA384(0x009d)
암호 제품군: TLS_RSA_WITH_AES_128_CBC_SHA(0x002f)
암호 제품군: TLS_RSA_WITH_AES_256_CBC_SHA(0x0035)

보시다시피 일치하지만 다음 TLSv1.2 패킷은 다음 0xc02f같이 0xc030말합니다 Alert (21), Handshake Failure (40).

업데이트 2 :

다음은 ClientHello에 있는 Android 5.1의 곡선입니다.

타원 곡선(25개 곡선)

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

ServerHello secp384r1 (0x0018)에서 반환됩니다.

그리고 이것은 Android 7에서 가져온 것입니다.

타원 곡선(1곡선)

타원 곡선: secp256r1(0x0017)

핸드셰이크 실패의 결과.

secp384r1을 제거하거나 기본값(prime256v1)으로 교체하여 nginx 구성을 변경하면 작동합니다. 그래서 나는 질문이 남아 있다고 생각합니다. 타원 곡선을 추가할 수 있습니까?

캡처된 데이터는 에뮬레이터 사용 시 Android 7.0 기기(일반 모바일 4G) 사용 시와 동일합니다.

업데이트 3 :

작은 업데이트이지만 언급할 가치가 있습니다. Android 7.1.1(!)을 사용하여 에뮬레이터에서 작동하도록 했습니다. 다음 데이터를 보여줍니다(다시 tcpdump를 사용하여 수집하고 wireshark를 사용하여 확인).

타원 곡선(3개의 곡선)

타원 곡선: secp256r1(0x0017)
타원 곡선: secp384r1(0x0018)
타원 곡선: secp512r1(0x0019)

동일한 18개의 암호 스위트를 보여줍니다.


이것은 Android 7.0의 알려진 회귀이며 Google에서 인정하고 Android 7.1.1 릴리스 이전에 수정되었습니다. 버그 보고서는 다음과 같습니다. https://code.google.com/p/android/issues/detail?id=224438 .

분명히 하자면, 여기 버그는 Cornelis가 질문에서 지적한 것처럼 7.0이 하나의 타원 곡선(prime256v1, secp256r1, NIST P-256)만 지원한다는 것입니다. 따라서 사용자가 이 문제에 직면한 경우 다음과 같은 해결 방법을 사용할 수 있습니다(사용자가 Android 7.1.1로 업그레이드하는 것이 이상적이라는 사실은 무시).

  • 타원 곡선을 사용하도록 서버를 구성합니다 prime256v1. 예를 들어 Nginx 1.10에서는 ssl_ecdh_curve prime256v1;.

  • 그래도 작동하지 않으면 타원 곡선 암호화(예: DHE-RSA-AES256-GCM-SHA384) 에 의존하지 않는 이전 암호 제품군을 사용합니다 (데이터 보안 측면에서 여기에서 수행하는 작업을 이해해야 함).

참고: 저는 타원 곡선 암호화 전문가가 아니므 로 내 제안이 보안에 미치는 영향에 대해 직접 조사하십시오. 이 답변을 작성하는 동안 참조한 다른 링크는 다음과 같습니다.


자체 서명 인증서에 문제가 있었고 문제는 Android 7.0에서 허용되지 않는 암호에 있었습니다.

나는 달렸다: openssl s_client -showcerts -connect <domain>:<port>

내가 찾은 결과에서 :

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

Android Equivalent of the Cipher를 검색하여 Retrofit Readapter에 추가했습니다.

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에 따라 '잘못된' 암호를 사용하는 모든 연결이 허용되었습니다.

1년 후 이 답변을 다시 볼 때 여전히 게시한 것에 만족합니다. 반면에 인증서를 올바른 Cypher 제품군으로 변경할 수 있는 위치에 있다면 항상 "다운그레이드"보다 이 작업을 수행하십시오. 앱에서 제품군을 허용합니다. 자체 서명된 인증서(예: 포함된 인증서)로 작업해야 하는 경우 이것이 효과적인 솔루션입니다.


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()]);
    }

}

이 코드에서 장치에서 지원하는 모든 암호를 간단히 추가합니다. 저에게는 이것이 작동합니다. 누군가에게 도움이 될 수 있습니다) 건배)

ps 매니페스트에 보안 네트워크 구성 매개변수를 추가할 필요가 없습니다.


나는 이것을 사용하여 "javax.net.ssl.SSLHandshakeException: Handshake failed" 오류를 수정하고 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()
            }

        }

기본적으로 모든 앱의 보안 연결(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()]));

여기도 마찬가지입니다. sll_ecdh_curve prime384v1 설정을 사용하는 내 Nginx 서버. 불행히도 백엔드 담당자는 클라이언트의 보안 정책으로 인해 Vicky Chijwani의 지침에 따라 Nginx 서버를 구성하는 것을 허용하지 않았습니다. Valley와 최신 버전의 OkHttp 라이브러리를 사용해 보았지만 소용이 없었습니다. 이 버그를 우회하려면 WebView를 사용하여 Adroid 7.0 기기에서 API 서비스와 통신해야 했습니다. 여기 내 어댑터 클래스가 있습니다. 다른 사람이 유용하게 사용할 수 있기를 바랍니다.

/**
* 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();
        }
    }
}

다른 솔루션으로 문제를 해결하지 못했기 때문에 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);
    }
});

ReferenceURL : https://stackoverflow.com/questions/39133437/sslhandshakeexception-handshake-failed-on-android-n-7-0

반응형