本文档介绍WebView场景下使用HTTPDNS时,拦截请求并基于HttpURLConnection实现原生请求,实现“IP直连”的方案。
1. 背景说明
阿里云 HTTPDNS 是一种有效避免 DNS 劫持的手段。在进行网络请求时,可以通过调用 HTTPDNS 提供的 API 来绕过系统默认的 DNS 解析,杜绝 DNS 劫持风险,并获得更高的解析精准度,从而提升客户端网络性能与稳定性。
在 WebView 加载的网页中,当网页发起网络请求时,我们可以通过拦截 WebView 内的请求,让原生层(App)来执行实际的网络请求。在原生层发起网络请求的过程中,即可使用 HTTPDNS 提供的解析结果,替换系统 DNS 解析,以防止 DNS 劫持。
本文档介绍WebView场景下使用HTTPDNS时,拦截请求并基于HttpURLConnection实现原生请求的方案。但考虑到现阶段OkHttp
已经是Android上主流的网络框架,使用OkHttp
是更简洁优雅的选择,我们建议参考Android端HTTPDNS+Webview+OkHttp最佳实践方案进行实现,只有确实无法使用OkHttp
时,才参考本文档使用。
本文档为Android WebView场景下接入HTTPDNS的参考方案,提供的相关代码也为参考代码,非线上生产环境正式代码,建议您仔细阅读本文档,进行合理评估后再进行接入。
由于Android生态碎片化严重,各厂商也进行了不同程度的定制,建议您灰度接入,并监控线上异常,有问题欢迎您随时通过技术支持向我们反馈,方便我们及时优化。
当前最佳实践文档只针对结合使用时,如何使用HTTPDNS解析出的IP,关于HTTPDNS本身的解析服务,请先查看Android SDK接入。
2. WebView只拦截GET请求的限制及影响
在 Android 的 WebView 中,通过 WebViewClient.shouldInterceptRequest()
方法只能获取到 GET 请求的详细信息,对于 POST 等其他请求方式,body
并不提供给开发者,因此无法进行全面拦截和替换为 HTTPDNS 解析。然而,这并不代表接入 HTTPDNS 毫无意义。原因如下:
网页中大部分资源请求(图片、CSS、JS 等静态资源)均为 GET 请求 一般网页所需加载的静态资源,如图片、脚本、样式表等,几乎全部使用 GET 请求获取。因此通过拦截 GET 请求并使用 HTTPDNS 来解析域名,可以覆盖到主要的资源加载过程。
API 调用中相当比例也使用 GET 请求 虽然部分数据交互可能使用 POST,但很多简单的 API 或查询请求,出于便利和缓存考虑,也使用 GET。此时也会被拦截并采用 HTTPDNS 解析。
仅拦截部分请求也能有效避免绝大多数劫持风险 DNS 劫持往往针对热门域名或静态资源域名发起,而这些域名请求通常是 GET。因此,哪怕只对 GET 请求使用 HTTPDNS,也能规避大多数公共资源或主要服务接口的劫持问题,实现 80% 甚至更高比例的保护。
综上所述,虽然 WebView 层面目前仅能通过原生方式拦截 GET 请求,但在实际业务中,这种方式通常已足以涵盖主要流量,从而发挥 80% 以上 的 DNS 安全防护效果。
3. 代码示例
HTTPDNS+WebView
最佳实践完整代码请参考WebView+HTTPDNS Android Demo。
4. 拦截接口说明
void setWebViewClient (WebViewClient client);
WebView
提供了setWebViewClient
接口对网络请求进行拦截,通过重写WebViewClient
中的shouldInterceptRequest
方法,我们可以拦截到所有的网络请求:
public class WebViewClient {
// API < 21
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
...
}
// API >= 21
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
...
}
......
}
shouldInterceptRequest
有两个版本:
当
API < 21
时,shouldInterceptRequest
方法的版本为:public WebResourceResponse shouldInterceptRequest(WebView view, String url)
此时仅能获取到请求URL,请求方法、头部信息以及body等均无法获取,强行拦截该请求可能无法得到正确响应。所以当
API < 21
时,不对请求进行拦截:public WebResourceResponse shouldInterceptRequest(WebView view, String url) { return super.shouldInterceptRequest(view, url); }
当
API >= 21
时,shouldInterceptRequest
提供了新版:public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request)
其中
WebResourceRequest
结构为:public interface WebResourceRequest { Uri getUrl(); // 请求URL boolean isForMainFrame(); // 是否由主MainFrame发出的请求 boolean isRedirect(); //是否重定向请求 boolean hasGesture(); // 是否是由某种行为(如点击)触发 String getMethod(); // 请求方法 Map<String, String> getRequestHeaders(); // 头部信息 }
可以看到,在API >= 21
时,在拦截请求时,可以获取到如下信息:
请求URL
请求方法:POST, GET…
请求头
5. 实践使用
WebView场景下的请求拦截逻辑如下所示:
仅拦截GET请求
设置头部信息
HTTPS请求证书校验
SNI场景
重定向
MIME&Encoding
6. 仅拦截GET请求
由于WebResourceRequest
并没有提供请求body
信息,所以只能拦截GET
请求,代码示例如下:
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
val scheme = request!!.url.scheme!!.trim()
val method = request.method
val headerFields = request.requestHeaders
val url = request.url.toString()
// 无法拦截body,拦截方案只能正常处理不带body的请求;
if ((scheme.equals("http", ignoreCase = true) || scheme.equals("https", ignoreCase = true))
&& method.equals("get", ignoreCase = true)
) {
//TODO 在 设置头部信息 部分介绍
} else {
return super.shouldInterceptRequest(view, request)
}
}
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String scheme = request.getUrl().getScheme().trim();
String method = request.getMethod();
Map<String, String> headerFields = request.getRequestHeaders();
String url = request.getUrl().toString();
// 无法拦截body,拦截方案只能正常处理不带body的请求;
if ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))
&& method.equalsIgnoreCase("get")) {
//TODO 在 设置头部信息 部分介绍
} else {
return super.shouldInterceptRequest(view, request);
}
}
7. 设置头部信息
这里把请求资源的方法抽象出来。
fun recursiveRequest(
path: String,
headers: Map<String?, String?>?,
reffer: String?
): URLConnection? {
val conn: HttpURLConnection
var url: URL? = null
try {
url = URL(path)
// 同步接口获取IP
val httpdnsResult = HttpDns.getService(accountID)
.getHttpDnsResultForHostSync(url.host, RequestIpType.both)
var ip: String? = null
if (httpdnsResult.ips != null && httpdnsResult.ips.isNotEmpty()) {
ip = httpdnsResult.ips[0]
} else if (httpdnsResult.ipv6s != null && httpdnsResult.ipv6s.isNotEmpty()) {
ip = httpdnsResult.ipv6s[0]
}
if (!TextUtils.isEmpty(ip)) {
// 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置
val newUrl = path.replaceFirst(url.host.toRegex(), ip!!)
conn = URL(newUrl).openConnection() as HttpURLConnection
conn.connectTimeout = 30000
conn.readTimeout = 30000
conn.instanceFollowRedirects = false
// 添加原有头部信息
if (headers != null) {
headers.forEach{ entry ->
conn.setRequestProperty(entry.key, entry.value)
}
}
// 设置HTTP请求头Host域
conn.setRequestProperty("Host", url.host)
//TODO 在 HTTPS请求证书校验 部分介绍
} else {
return null
}
//TODO 在 重定向 部分介绍
} catch (e: MalformedURLException) {
Log.w(TAG, "recursiveRequest MalformedURLException")
} catch (e: IOException) {
Log.w(TAG, "recursiveRequest IOException")
} catch (e: Exception) {
Log.w(TAG, "unknow exception")
}
return null
}
public URLConnection recursiveRequest(String path, Map<String, String> headers, String reffer) {
HttpURLConnection conn;
URL url = null;
try {
url = new URL(path);
// 同步接口获取IP
HTTPDNSResult httpdnsResult = HttpDns.getService(accountID).getHttpDnsResultForHostSync(url.getHost(), RequestIpType.both);
String ip = null;
if (httpdnsResult.getIps() != null && httpdnsResult.getIps().length > 0) {
ip = httpdnsResult.getIps()[0];
} else if (httpdnsResult.getIpv6s() != null && httpdnsResult.getIpv6s().length > 0) {
ip = httpdnsResult.getIpv6s()[0];
}
if (!TextUtils.isEmpty(ip)) {
// 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置
String newUrl = path.replaceFirst(url.getHost(), ip);
conn = (HttpURLConnection) new URL(newUrl).openConnection();
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(false);
// 添加原有头部信息
if (headers != null) {
for (Map.Entry<String, String> field : headers.entrySet()) {
conn.setRequestProperty(field.getKey(), field.getValue());
}
}
// 设置HTTP请求头Host域
conn.setRequestProperty("Host", url.getHost());
//TODO 在 HTTPS请求证书校验 部分介绍
} else {
return null;
}
//TODO 在 重定向 部分介绍
} catch (MalformedURLException e) {
Log.w(TAG, "recursiveRequest MalformedURLException");
} catch (IOException e) {
Log.w(TAG, "recursiveRequest IOException");
} catch (Exception e) {
Log.w(TAG, "unknow exception");
}
return null;
}
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? {
val scheme = request!!.url.scheme!!.trim()
val method = request.method
val headerFields = request.requestHeaders
val url = request.url.toString()
// 无法拦截body,拦截方案只能正常处理不带body的请求;
if ((scheme.equals("http", ignoreCase = true) || scheme.equals("https", ignoreCase = true))
&& method.equals("get", ignoreCase = true)
) {
try {
val connection = recursiveRequest(url, headerFields, null)
?: return super.shouldInterceptRequest(view, request)
//TODO 在 MIME&Encoding 部分介绍
} catch (e: MalformedURLException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
}
return super.shouldInterceptRequest(view, request)
}
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String scheme = request.getUrl().getScheme().trim();
String method = request.getMethod();
Map<String, String> headerFields = request.getRequestHeaders();
String url = request.getUrl().toString();
// 无法拦截body,拦截方案只能正常处理不带body的请求;
if ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))
&& method.equalsIgnoreCase("get")) {
try {
URLConnection connection = recursiveRequest(url, headerFields, null);
if (connection == null) {
return super.shouldInterceptRequest(view, request);
}
//TODO 在 MIME&Encoding 部分介绍
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return super.shouldInterceptRequest(view, request);
}
8. HTTPS请求证书校验
如果拦截到的请求是HTTPS
请求,需要进行证书校验:
fun recursiveRequest(
path: String,
headers: Map<String?, String?>?,
reffer: String?
): URLConnection? {
val conn: HttpURLConnection
var url: URL? = null
try {
url = URL(path)
// 同步接口获取IP
val httpdnsResult = HttpDns.getService(accountID)
.getHttpDnsResultForHostSync(url.host, RequestIpType.both)
var ip: String? = null
if (httpdnsResult.ips != null && httpdnsResult.ips.size > 0) {
ip = httpdnsResult.ips[0]
} else if (httpdnsResult.ipv6s != null && httpdnsResult.ipv6s.size > 0) {
ip = httpdnsResult.ipv6s[0]
}
if (!TextUtils.isEmpty(ip)) {
// 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置
val newUrl = path.replaceFirst(url.host.toRegex(), ip!!)
conn = URL(newUrl).openConnection() as HttpURLConnection
conn.connectTimeout = 30000
conn.readTimeout = 30000
conn.instanceFollowRedirects = false
// 添加原有头部信息
headers?.forEach{ entry ->
conn.setRequestProperty(entry.key, entry.value)
}
// 设置HTTP请求头Host域
conn.setRequestProperty("Host", url.host)
if (conn is HttpsURLConnection) {
// https场景,证书校验
conn.hostnameVerifier =
HostnameVerifier { _, session ->
var host = conn.getRequestProperty("Host")
if (null == host) {
host = conn.url.host
}
HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session)
}
//TODO 在 SNI场景 部分介绍
}
//TODO 在 重定向 部分介绍
} else {
return null
}
} catch (e: MalformedURLException) {
Log.w(TAG, "recursiveRequest MalformedURLException")
} catch (e: IOException) {
Log.w(TAG, "recursiveRequest IOException")
} catch (e: Exception) {
Log.w(TAG, "unknow exception")
}
return null
}
public URLConnection recursiveRequest(String path, Map<String, String> headers, String reffer) {
HttpURLConnection conn;
URL url = null;
try {
url = new URL(path);
// 同步接口获取IP
HTTPDNSResult httpdnsResult = HttpDns.getService(accountID).getHttpDnsResultForHostSync(url.getHost(), RequestIpType.both);
String ip = null;
if (httpdnsResult.getIps() != null && httpdnsResult.getIps().length > 0) {
ip = httpdnsResult.getIps()[0];
} else if (httpdnsResult.getIpv6s() != null && httpdnsResult.getIpv6s().length > 0) {
ip = httpdnsResult.getIpv6s()[0];
}
if (!TextUtils.isEmpty(ip)) {
// 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置
String newUrl = path.replaceFirst(url.getHost(), ip);
conn = (HttpURLConnection) new URL(newUrl).openConnection();
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(false);
// 添加原有头部信息
if (headers != null) {
for (Map.Entry<String, String> field : headers.entrySet()) {
conn.setRequestProperty(field.getKey(), field.getValue());
}
}
// 设置HTTP请求头Host域
conn.setRequestProperty("Host", url.getHost());
if (conn instanceof HttpsURLConnection) {
HttpsURLConnection httpsURLConnection = (HttpsURLConnection)conn;
// https场景,证书校验
httpsURLConnection.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
String host = httpsURLConnection.getRequestProperty("Host");
if (null == host) {
host = httpsURLConnection.getURL().getHost();
}
return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
}
});
//TODO 在 SNI场景 部分介绍
}
//TODO 在 重定向 部分介绍
} else {
return null;
}
} catch (MalformedURLException e) {
Log.w(TAG, "recursiveRequest MalformedURLException");
} catch (IOException e) {
Log.w(TAG, "recursiveRequest IOException");
} catch (Exception e) {
Log.w(TAG, "unknow exception");
}
return null;
}
9. SNI场景
如果请求涉及到SNI场景,需要自定义SSLSocket,对SNI场景不熟悉的用户可以参考SNI:
9.1 自定义SSLSocket
class TlsSniSocketFactory constructor(conn: HttpsURLConnection): SSLSocketFactory() {
private val mConn: HttpsURLConnection
init {
mConn = conn
}
override fun createSocket(plainSocket: Socket?, host: String?, port: Int, autoClose: Boolean): Socket {
var peerHost = mConn.getRequestProperty("Host")
if (peerHost == null) {
peerHost = host
}
val address = plainSocket!!.inetAddress
if (autoClose) {
// we don't need the plainSocket
plainSocket.close()
}
// create and connect SSL socket, but don't do hostname/certificate verification yet
val sslSocketFactory =
SSLCertificateSocketFactory.getDefault(0) as SSLCertificateSocketFactory
val ssl = sslSocketFactory.createSocket(address, R.attr.port) as SSLSocket
// enable TLSv1.1/1.2 if available
ssl.enabledProtocols = ssl.supportedProtocols
// set up SNI before the handshake
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
// setting sni hostname
sslSocketFactory.setHostname(ssl, peerHost)
} else {
// No documented SNI support on Android <4.2, trying with reflection
try {
val setHostnameMethod = ssl.javaClass.getMethod(
"setHostname",
String::class.java
)
setHostnameMethod.invoke(ssl, peerHost)
} catch (e: Exception) {
}
}
// verify hostname and certificate
val session = ssl.session
if (!HttpsURLConnection.getDefaultHostnameVerifier()
.verify(peerHost, session)
) throw SSLPeerUnverifiedException(
"Cannot verify hostname: $peerHost"
)
return ssl
}
}
public class TlsSniSocketFactory extends SSLSocketFactory {
private HttpsURLConnection mConn;
public TlsSniSocketFactory(HttpsURLConnection conn) {
mConn = conn;
}
@Override
public Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException {
String peerHost = mConn.getRequestProperty("Host");
if (peerHost == null)
peerHost = host;
InetAddress address = plainSocket.getInetAddress();
if (autoClose) {
// we don't need the plainSocket
plainSocket.close();
}
// create and connect SSL socket, but don't do hostname/certificate verification yet
SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0);
SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(address, port);
// enable TLSv1.1/1.2 if available
ssl.setEnabledProtocols(ssl.getSupportedProtocols());
// set up SNI before the handshake
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
// setting sni hostname
sslSocketFactory.setHostname(ssl, peerHost);
} else {
// No documented SNI support on Android <4.2, trying with reflection
try {
java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class);
setHostnameMethod.invoke(ssl, peerHost);
} catch (Exception e) {
}
}
// verify hostname and certificate
SSLSession session = ssl.getSession();
if (!HttpsURLConnection.getDefaultHostnameVerifier().verify(peerHost, session))
throw new SSLPeerUnverifiedException("Cannot verify hostname: " + peerHost);
return ssl;
}
}
9.2 设置自定义SSLSocket
fun recursiveRequest(
path: String,
headers: Map<String?, String?>?,
reffer: String?
): URLConnection? {
val conn: HttpURLConnection
var url: URL? = null
try {
url = URL(path)
// 同步接口获取IP
val httpdnsResult = HttpDns.getService(accountID)
.getHttpDnsResultForHostSync(url.host, RequestIpType.both)
var ip: String? = null
if (httpdnsResult.ips != null && httpdnsResult.ips.isNotEmpty()) {
ip = httpdnsResult.ips[0]
} else if (httpdnsResult.ipv6s != null && httpdnsResult.ipv6s.isNotEmpty()) {
ip = httpdnsResult.ipv6s[0]
}
if (!TextUtils.isEmpty(ip)) {
// 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置
val newUrl: String = path.replaceFirst(url.host, ip)
conn = URL(newUrl).openConnection() as HttpURLConnection
conn.connectTimeout = 30000
conn.readTimeout = 30000
conn.instanceFollowRedirects = false
// 添加原有头部信息
headers?.forEach { entry ->
conn.setRequestProperty(entry.key, entry.value)
}
// 设置HTTP请求头Host域
conn.setRequestProperty("Host", url.host)
if (conn is HttpsURLConnection) {
// https场景,证书校验
conn.hostnameVerifier = HostnameVerifier { _, session ->
var host = conn.getRequestProperty("Host")
if (null == host) {
host = conn.url.host
}
HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session)
}
// sni场景,创建SSLScocket
conn.sslSocketFactory = TlsSniSocketFactory(conn)
}
//TODO 在 重定向 部分介绍
} else {
return null
}
} catch (e: MalformedURLException) {
Log.w(TAG, "recursiveRequest MalformedURLException")
} catch (e: IOException) {
Log.w(TAG, "recursiveRequest IOException")
} catch (e: Exception) {
Log.w(TAG, "unknow exception")
}
return null
}
public URLConnection recursiveRequest(String path, Map<String, String> headers, String reffer) {
HttpURLConnection conn;
URL url = null;
try {
url = new URL(path);
// 同步接口获取IP
HTTPDNSResult httpdnsResult = HttpDns.getService(accountID).getHttpDnsResultForHostSync(url.getHost(), RequestIpType.both);
String ip = null;
if (httpdnsResult.getIps() != null && httpdnsResult.getIps().length > 0) {
ip = httpdnsResult.getIps()[0];
} else if (httpdnsResult.getIpv6s() != null && httpdnsResult.getIpv6s().length > 0) {
ip = httpdnsResult.getIpv6s()[0];
}
if (!TextUtils.isEmpty(ip)) {
// 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置
String newUrl = path.replaceFirst(url.getHost(), ip);
conn = (HttpURLConnection) new URL(newUrl).openConnection();
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(false);
// 添加原有头部信息
if (headers != null) {
for (Map.Entry<String, String> field : headers.entrySet()) {
conn.setRequestProperty(field.getKey(), field.getValue());
}
}
// 设置HTTP请求头Host域
conn.setRequestProperty("Host", url.getHost());
if (conn instanceof HttpsURLConnection) {
HttpsURLConnection httpsURLConnection = (HttpsURLConnection)conn;
// https场景,证书校验
httpsURLConnection.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
String host = httpsURLConnection.getRequestProperty("Host");
if (null == host) {
host = httpsURLConnection.getURL().getHost();
}
return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
}
});
// sni场景,创建SSLScocket
httpsURLConnection.setSSLSocketFactory(new TlsSniSocketFactory(httpsURLConnection));
}
//TODO 在 重定向 部分介绍
} else {
return null;
}
} catch (MalformedURLException e) {
Log.w(TAG, "recursiveRequest MalformedURLException");
} catch (IOException e) {
Log.w(TAG, "recursiveRequest IOException");
} catch (Exception e) {
Log.w(TAG, "unknow exception");
}
return null;
}
10. 处理重定向
如果服务端返回重定向,此时需要判断原有请求中是否含有cookie:
如果原有请求报头含有cookie,因为cookie是以域名为粒度进行存储的,重定向后cookie会改变,且无法获取到新请求URL下的cookie,所以放弃拦截。
如果不含cookie,重新发起二次请求。
fun recursiveRequest(
path: String,
headers: Map<String, String?>?,
reffer: String?
): URLConnection? {
val conn: HttpURLConnection
var url: URL? = null
try {
url = URL(path)
// 同步接口获取IP
val httpdnsResult = HttpDns.getService(accountID)
.getHttpDnsResultForHostSync(url.host, RequestIpType.both)
var ip: String? = null
if (httpdnsResult.ips != null && httpdnsResult.ips.isNotEmpty()) {
ip = httpdnsResult.ips[0]
} else if (httpdnsResult.ipv6s != null && httpdnsResult.ipv6s.isNotEmpty()) {
ip = httpdnsResult.ipv6s[0]
}
if (!TextUtils.isEmpty(ip)) {
// 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置
val newUrl = path.replaceFirst(url.host.toRegex(), ip!!)
conn = URL(newUrl).openConnection() as HttpURLConnection
conn.connectTimeout = 30000
conn.readTimeout = 30000
conn.instanceFollowRedirects = false
// 添加原有头部信息
headers?.forEach { entry ->
conn.setRequestProperty(entry.key, entry.value)
}
// 设置HTTP请求头Host域
conn.setRequestProperty("Host", url.host)
if (conn is HttpsURLConnection) {
// https场景,证书校验
conn.hostnameVerifier =
HostnameVerifier { _, session ->
var host = conn.getRequestProperty("Host")
if (null == host) {
host = conn.url.host
}
HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session)
}
// sni场景,创建SSLScocket
conn.sslSocketFactory = TlsSniSocketFactory(conn)
}
} else {
return null
}
val code = conn.responseCode // Network block
return if (code in 300..399) {
// 原有报头中含有cookie,放弃拦截
var containCookie = false
if (headers != null) {
for (item in headers.keys) {
if (item.contains("Cookie")) {
containCookie = true
break
}
}
}
if (containCookie) {
return null
}
var location = conn.getHeaderField("Location")
if (location == null) {
location = conn.getHeaderField("location")
}
if (location != null) {
if (!(location.startsWith("http://") || location
.startsWith("https://"))
) {
//某些时候会省略host,只返回后面的path,所以需要补全url
val originalUrl = URL(path)
location = (originalUrl.protocol + "://"
+ originalUrl.host + location)
}
recursiveRequest(location, headers, path)
} else {
// 无法获取location信息,让浏览器获取
null
}
} else {
// redirect finish.
conn
}
} catch (e: MalformedURLException) {
Log.w(TAG, "recursiveRequest MalformedURLException")
} catch (e: IOException) {
Log.w(TAG, "recursiveRequest IOException")
} catch (e: Exception) {
Log.w(TAG, "unknow exception")
}
return null
}
public URLConnection recursiveRequest(String path, Map<String, String> headers, String reffer) {
HttpURLConnection conn;
URL url = null;
try {
url = new URL(path);
// 同步接口获取IP
HTTPDNSResult httpdnsResult = HttpDns.getService(accountID).getHttpDnsResultForHostSync(url.getHost(), RequestIpType.both);
String ip = null;
if (httpdnsResult.getIps() != null && httpdnsResult.getIps().length > 0) {
ip = httpdnsResult.getIps()[0];
} else if (httpdnsResult.getIpv6s() != null && httpdnsResult.getIpv6s().length > 0) {
ip = httpdnsResult.getIpv6s()[0];
}
if (!TextUtils.isEmpty(ip)) {
// 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置
String newUrl = path.replaceFirst(url.getHost(), ip);
conn = (HttpURLConnection) new URL(newUrl).openConnection();
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(false);
// 添加原有头部信息
if (headers != null) {
for (Map.Entry<String, String> field : headers.entrySet()) {
conn.setRequestProperty(field.getKey(), field.getValue());
}
}
// 设置HTTP请求头Host域
conn.setRequestProperty("Host", url.getHost());
if (conn instanceof HttpsURLConnection) {
final HttpsURLConnection httpsURLConnection = (HttpsURLConnection) conn;
// https场景,证书校验
httpsURLConnection.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
String host = httpsURLConnection.getRequestProperty("Host");
if (null == host) {
host = httpsURLConnection.getURL().getHost();
}
return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
}
});
// sni场景,创建SSLScocket
httpsURLConnection.setSSLSocketFactory(new TlsSniSocketFactory(httpsURLConnection));
}
} else {
return null;
}
int code = conn.getResponseCode();// Network block
if (code >= 300 && code < 400) {
// 原有报头中含有cookie,放弃拦截
boolean containCookie = false;
if (headers != null) {
for (Map.Entry<String, String> headerField : headers.entrySet()) {
if (headerField.getKey().contains("Cookie")) {
containCookie = true;
break;
}
}
}
if (containCookie) {
return null;
}
String location = conn.getHeaderField("Location");
if (location == null) {
location = conn.getHeaderField("location");
}
if (location != null) {
if (!(location.startsWith("http://") || location
.startsWith("https://"))) {
//某些时候会省略host,只返回后面的path,所以需要补全url
URL originalUrl = new URL(path);
location = originalUrl.getProtocol() + "://"
+ originalUrl.getHost() + location;
}
return recursiveRequest(location, headers, path);
} else {
// 无法获取location信息,让浏览器获取
return null;
}
} else {
// redirect finish.
return conn;
}
} catch (MalformedURLException e) {
Log.w(TAG, "recursiveRequest MalformedURLException");
} catch (IOException e) {
Log.w(TAG, "recursiveRequest IOException");
} catch (Exception e) {
Log.w(TAG, "unknow exception");
}
return null;
}
11. 处理MIME&Encoding
如果拦截网络请求,需要返回一个WebResourceResponse
:
public WebResourceResponse(String mimeType, String encoding, InputStream data) ;
创建WebResourceResponse
对象需要提供:
请求的MIME类型
请求的编码
请求的输入流
其中请求输入流可以通过URLConnection.getInputStream()
获取到,而MIME
类型和encoding
可以通过请求的ContentType
获取到,即通过URLConnection.getContentType()
,如:
text/html;charset=utf-8
但并不是所有的请求都能得到完整的contentType
信息,此时可以参考如下策略:
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? {
val scheme = request!!.url.scheme!!.trim()
val method = request.method
val headerFields = request.requestHeaders
val url = request.url.toString()
// 无法拦截body,拦截方案只能正常处理不带body的请求;
if ((scheme.equals("http", ignoreCase = true) || scheme.equals("https", ignoreCase = true))
&& method.equals("get", ignoreCase = true)
) {
try {
val connection = recursiveRequest(url, headerFields, null)
?: return super.shouldInterceptRequest(view, request)
// 注*:对于POST请求的Body数据,WebResourceRequest接口中并没有提供,这里无法处理
val contentType = connection.contentType
var mime: String? = null
val charset: String? = null
if (contentType != null) {
mime = contentType.split(";".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()[0]
val fields = contentType.split(";".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
if (fields.size > 1) {
var charset = fields[1]
if (charset.contains("=")) {
charset = charset.substring(charset.indexOf("=") + 1)
}
}
}
val httpURLConnection = connection as HttpURLConnection
val statusCode = httpURLConnection.responseCode
val response = httpURLConnection.responseMessage
val headers = httpURLConnection.headerFields
val headerKeySet: Set<String> = headers.keys
// 无mime类型的请求不拦截
return if (TextUtils.isEmpty(mime)) {
super.shouldInterceptRequest(view, request)
} else {
// 二进制资源无需编码信息
if (!TextUtils.isEmpty(charset) || (mime!!.startsWith("image")
|| mime.startsWith("audio")
|| mime.startsWith("video"))
) {
val resourceResponse =
WebResourceResponse(mime, charset, httpURLConnection.inputStream)
resourceResponse.setStatusCodeAndReasonPhrase(statusCode, response)
val responseHeader: MutableMap<String, String> = HashMap()
for (key in headerKeySet) {
// HttpUrlConnection可能包含key为null的报头,指向该http请求状态码
responseHeader[key] = httpURLConnection.getHeaderField(key)
}
resourceResponse.responseHeaders = responseHeader
resourceResponse
} else {
super.shouldInterceptRequest(view, request)
}
}
} catch (e: MalformedURLException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
}
return super.shouldInterceptRequest(view, request)
}
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String scheme = request.getUrl().getScheme().trim();
String method = request.getMethod();
Map<String, String> headerFields = request.getRequestHeaders();
String url = request.getUrl().toString();
// 无法拦截body,拦截方案只能正常处理不带body的请求;
if ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))
&& method.equalsIgnoreCase("get")) {
try {
URLConnection connection = recursiveRequest(url, headerFields, null);
if (connection == null) {
return super.shouldInterceptRequest(view, request);
}
// 注*:对于POST请求的Body数据,WebResourceRequest接口中并没有提供,这里无法处理
String contentType = connection.getContentType();
String mime = null;
String charset = null;
if (contentType != null) {
mime = contentType.split(";")[0];
String[] fields = contentType.split(";");
if (fields.length > 1) {
String charset = fields[1];
if (charset.contains("=")) {
charset = charset.substring(charset.indexOf("=") + 1);
}
}
}
HttpURLConnection httpURLConnection = (HttpURLConnection) connection;
int statusCode = httpURLConnection.getResponseCode();
String response = httpURLConnection.getResponseMessage();
Map<String, List<String>> headers = httpURLConnection.getHeaderFields();
Set<String> headerKeySet = headers.keySet();
// 无mime类型的请求不拦截
if (TextUtils.isEmpty(mime)) {
return super.shouldInterceptRequest(view, request);
} else {
// 二进制资源无需编码信息
if (!TextUtils.isEmpty(charset) || (mime.startsWith("image")
|| mime.startsWith("audio")
|| mime.startsWith("video"))) {
WebResourceResponse resourceResponse = new WebResourceResponse(mime, charset, httpURLConnection.getInputStream());
resourceResponse.setStatusCodeAndReasonPhrase(statusCode, response);
Map<String, String> responseHeader = new HashMap<String, String>();
for (String key : headerKeySet) {
// HttpUrlConnection可能包含key为null的报头,指向该http请求状态码
responseHeader.put(key, httpURLConnection.getHeaderField(key));
}
resourceResponse.setResponseHeaders(responseHeader);
return resourceResponse;
} else {
return super.shouldInterceptRequest(view, request);
}
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return super.shouldInterceptRequest(view, request);
}
12. 总结
场景 | 总结 |
场景 | 总结 |
不可用场景 |
|
可用场景 | 前提条件:
可用场景:
|
- 本页导读 (0)
- 1. 背景说明
- 2. WebView只拦截GET请求的限制及影响
- 3. 代码示例
- 4. 拦截接口说明
- 5. 实践使用
- 6. 仅拦截GET请求
- 7. 设置头部信息
- 8. HTTPS请求证书校验
- 9. SNI场景
- 9.1 自定义SSLSocket
- 9.2 设置自定义SSLSocket
- 10. 处理重定向
- 11. 处理MIME&Encoding
- 12. 总结