资讯动态

Android WebView 跨域拦截与可二次开发浏览器实践

发布时间:2026/10/23 2:02:01 来源:尧图企业网站定制
简介这份开源Android浏览器项目源码面向移动开发学习者与WebView实践者围绕在应用内访问跨域网页这一需求提供可参考的浏览器实现与定制思路。压缩包共269个文件、约5.92MB以47个Java源文件、90个XML布局与资源、100个PNG图片为主另有少量sample示例、mk构建文件及html测试页便于从界面、逻辑到资源组织逐层阅读。项目已有722人学习适合具备Java与Android SDK基础、希望理解WebView初始化、页面加载事件、网络请求拦截与跨域处理如CORS、JavaScript接口交互的读者。借助完整目录结构与源码注释说明可梳理Activity、Intent、生命周期、权限控制及书签、前进后退等模块的实现方式并对照示例页面调试跨域访问流程积累浏览器性能与体验优化的排错经验。1. 一个把「跨域」当第一需求的 Android 浏览器PC 上跑得好好的 H5 页面塞进 Android WebView 就白屏控制台一行红字has been blocked by CORS policy——这个场景做过混合开发的人多半都遇过。这份开源的 Android 浏览器项目核心卖点就落在这件事上它不只是一个能输网址的 WebView 壳子而是把跨域请求、file://协议访问、混合内容加载这些默认被系统拦下的能力做成了可配置、可拦截、可二次开发的完整工程。适合谁一类是想给自家 App 内嵌 H5、又不想被 CORS 反复卡住的开发者另一类是拿它当调试浏览器专门用来验证接口在不同源下的实际表现。下面按工程骨架、跨域实现、编译跑通、踩坑记录、二次开发五段把它拆开看。2. 工程骨架拆解从 Activity 到 WebView 的完整加载链路这个项目表面上是个浏览器骨子里是「一个 Activity 一套 WebView 配置 一层请求代理」。理解它的捷径不是从首页 UI 看起而是从一次loadUrl往下追谁创建了 WebView、谁接管了导航、谁在资源层动手脚。三者的职责边界划清楚了后面改什么都不会乱。2.1 目录结构与模块职责典型结构长这样不同分支命名略有出入但分层逻辑一致app/ ├── src/main/java/com/example/browser/ │ ├── ui/ │ │ ├── BrowserActivity.java // 主容器承载 WebView 与地址栏 │ │ └── SettingsActivity.java // 跨域开关、UA、缓存策略 │ ├── web/ │ │ ├── BrowserWebViewClient.java // 导航拦截 资源拦截 │ │ ├── BrowserChromeClient.java // 进度、标题、JS 弹窗 │ │ └── WebViewInitializer.java // WebSettings 统一配置 │ ├── bridge/ │ │ └── JsBridge.java // JavascriptInterface 暴露给前端 │ └── util/ │ ├── UrlUtils.java // scheme 判定、域名提取 │ └── CookieHelper.java // CookieManager 同步 └── src/main/res/ ├── layout/activity_browser.xml └── xml/network_security_config.xmlui层只管界面和生命周期web层是真正干活的地方bridge层是前端调原生的入口util收口零散逻辑。这种分法不算高级但对二次开发很友好——想改跨域策略只动web和util不用碰 UI。我一般接手别人的 WebView 项目第一件事就是看WebViewClient有没有被拆出来拆了就说明作者想过扩展性。2.2 WebView 初始化与 WebSettings 配置这是整个项目的地基。默认的WebSettings是保守的跨域相关的几个开关全是关的不显式打开后面写再多拦截逻辑都是白搭。public class WebViewInitializer { public static void init(WebView webView, Context context) { WebSettings settings webView.getSettings(); // JS 是现代页面的必需品不开等于看静态 HTML settings.setJavaScriptEnabled(true); // DOM StorageVue/React 打包产物普遍依赖 localStorage settings.setDomStorageEnabled(true); // 数据库缓存部分离线页面会用到 settings.setDatabaseEnabled(true); // 跨域能力的关键三连 // 允许 file:// 页面读取本地文件 settings.setAllowFileAccess(true); // 允许 file:// 页面通过 XHR/fetch 访问其他源 settings.setAllowFileAccessFromFileURLs(true); // 允许 file:// 页面访问任意来源Android 5.0 起默认关闭 settings.setAllowUniversalAccessFromFileURLs(true); // 混合内容https 页面加载 http 资源默认在 M 之后是 BLOCK if (Build.VERSION.SDK_INT Build.VERSION_CODES.LOLLIPOP) { settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); } // 缓存策略跨域调试时建议先关缓存避免读到旧响应 settings.setCacheMode(WebSettings.LOAD_DEFAULT); // 支持手势缩放浏览器场景基本都要 settings.setSupportZoom(true); settings.setBuiltInZoomControls(true); settings.setDisplayZoomControls(false); // 开启远程调试配合 chrome://inspect 看控制台 if (Build.VERSION.SDK_INT Build.VERSION_CODES.KITKAT) { WebView.setWebContentsDebuggingEnabled(true); } } }setAllowUniversalAccessFromFileURLs和setAllowFileAccessFromFileURLs是这套配置的核心。前者放开的是「任意源」后者只放开「同协议内的其他文件」安全边界差一个量级。很多项目只开了后者结果本地打开的 HTML 调接口还是被拦然后开始怀疑人生。参数上setMixedContentMode的ALWAYS_ALLOW会让 https 页面里的 http 图片、脚本照常加载代价是安全性下降——如果只是内网调试开着最省事要上生产建议改成MIXED_CONTENT_COMPATIBILITY_MODE让 WebView 自己按资源类型判断。setCacheMode(LOAD_DEFAULT)是折中方案实际调试时我更倾向临时切成LOAD_NO_CACHE不然改了响应头、刷新页面还是老结果排查起来非常费劲。2.3 加载链路从输入 URL 到页面渲染一次完整加载经过四道关每道关都有可插入的钩子阶段触发点可介入的回调导航发起用户输入或loadUrlshouldOverrideUrlLoading请求发出前每个子资源shouldInterceptRequest页面开始DOM 就绪前onPageStarted页面完成资源全部加载完onPageFinished导航层负责「要不要让 WebView 自己加载这个 URL」资源层负责「这个请求的响应长什么样」。前者决定跳转行为后者决定跨域成败。项目里把这两件事放进同一个WebViewClient的两个方法逻辑清晰但要注意shouldInterceptRequest在子线程执行不能碰 UIshouldOverrideUrlLoading在主线程不能做耗时操作。public class BrowserWebViewClient extends WebViewClient { Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { String url request.getUrl().toString(); // http/https 交给 WebView 自己处理 if (url.startsWith(http://) || url.startsWith(https://)) { return false; } // 自定义 schemetel:、mailto:、市场协议抛给系统 try { Intent intent new Intent(Intent.ACTION_VIEW, Uri.parse(url)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); view.getContext().startActivity(intent); } catch (ActivityNotFoundException e) { // 没有应用能处理静默吞掉别崩溃 } return true; } Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); // 页面加载完再做一次 Cookie 同步避免首屏请求丢 Cookie CookieHelper.sync(view.getContext()); } }shouldOverrideUrlLoading返回false表示「我不处理WebView 你自己加载」返回true表示「我接管了」。这里返回false让 http/https 走默认流程是为了保留浏览器的前进后退能力——如果返回true再手动loadUrl历史栈会被打乱返回键行为就变得很怪。踩过这个坑的人应该记得那种「点返回直接退出 App」的酸爽。3. 跨域访问核心拦截请求与 CORS 响应头注入WebView 的跨域判定和桌面浏览器不完全一样很多人把 PC 上的经验直接搬过来结果怎么调都不对。这一章把差异讲清楚再落到具体代码。3.1 为什么 WebView 里的跨域和浏览器不一样桌面浏览器的同源策略由浏览器内核统一执行服务端返回什么头、页面怎么发请求规则是透明的。WebView 多了一层它既是浏览器内核又是宿主 App 的一部分宿主可以通过WebSettings改规则、通过shouldInterceptRequest改响应。这层能力是双刃剑——用好了能绕开跨域限制用不好会出现「同一个接口App 里能调、浏览器里不能调」的玄学现象。具体差异有三点第一file://协议下的源判定。桌面浏览器把file://视为不透明源几乎禁止一切跨源请求Android WebView 在setAllowUniversalAccessFromFileURLs(true)之后会放宽这个限制让本地 HTML 能直接fetch远程接口。这是很多离线包方案的基础。第二CORS 预检请求的处理。OPTIONS预检在 WebView 里同样会发但shouldInterceptRequest能拦到它也就意味着你可以在原生层直接返回预检结果不用等服务器。这招我常用来兜底那些没配 CORS 的老接口。第三Cookie 的同源判定。WebView 的CookieManager默认只对同域请求生效跨域请求要显式开setAcceptThirdPartyCookies否则会出现「登录成功但接口 401」的诡异情况。3.2 shouldInterceptRequest 拦截并重写响应头核心思路把跨域请求在原生层代发一遍拿回响应后手动补上 CORS 头再交给 WebView。这样页面端的fetch看到的响应就是「服务端已经允许跨域」的样子。public class CorsWebViewClient extends WebViewClient { private final OkHttpClient httpClient new OkHttpClient.Builder() .followRedirects(true) .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .build(); Override public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { // 只拦 GETPOST 的 body 拿不到后面章节单独说 if (!GET.equalsIgnoreCase(request.getMethod())) { return null; } String url request.getUrl().toString(); // 主文档不拦避免破坏导航和历史栈 if (request.isForMainFrame()) { return null; } try { Request.Builder builder new Request.Builder().url(url).get(); // 原样透传请求头UA、Referer、Cookie 都要带 for (Map.EntryString, String entry : request.getRequestHeaders().entrySet()) { builder.header(entry.getKey(), entry.getValue()); } Response response httpClient.newCall(builder.build()).execute(); MapString, String headers new HashMap(); for (String name : response.headers().names()) { headers.put(name, response.headers().get(name)); } // 核心三行补 CORS 响应头 headers.put(Access-Control-Allow-Origin, *); headers.put(Access-Control-Allow-Methods, GET, POST, PUT, DELETE, OPTIONS); headers.put(Access-Control-Allow-Headers, *); // 需要带 Cookie 时必须指定具体来源不能用 * headers.put(Access-Control-Allow-Credentials, true); String contentType response.header(Content-Type, application/octet-stream); String mimeType contentType.split(;)[0].trim(); String encoding utf-8; if (contentType.contains(charset)) { encoding contentType.substring(contentType.indexOf(charset) 8).trim(); } return new WebResourceResponse( mimeType, encoding, response.code(), response.message(), headers, response.body().byteStream() ); } catch (IOException e) { // 失败返回 null让 WebView 走默认逻辑别把页面拖死 return null; } } }逐段说明。request.isForMainFrame()判断是不是主文档主文档拦截会打乱导航栈直接放行。请求头透传那一段不能省很多接口靠Referer或自定义头做鉴权丢了就 403。mimeType的拆分是血泪经验Content-Type常常是application/json; charsetutf-8这种带参数的格式直接塞进WebResourceResponse的第一个参数WebView 会解析失败页面表现为样式全丢、脚本不执行。split(;)[0]是必须的。Access-Control-Allow-Credentials: true和Access-Control-Allow-Origin: *在规范上是互斥的真要让跨域请求带 CookieOrigin得回填成请求的具体来源不能用通配符。这一点后面避坑章节还会提。提示shouldInterceptRequest运行在非 UI 线程用同步 OkHttp 是安全的但也别在这里做重 CPU 操作它会被每个子资源调用性能敏感。3.3 JavaScript Bridge 与 file:// 页面的跨域策略本地 HTML 通过file://加载时就算开了setAllowUniversalAccessFromFileURLs部分接口仍会因为缺少Origin头被拒。项目里用JavascriptInterface提供一层原生代理前端调AndroidBridge.request(url)由原生发请求再回调结果。public class JsBridge { private final Context context; private final OkHttpClient client new OkHttpClient(); private final Handler mainHandler new Handler(Looper.getMainLooper()); public JsBridge(Context context) { this.context context.getApplicationContext(); } JavascriptInterface public void request(String url, String callbackName) { // 网络请求放子线程回调切回主线程 new Thread(() - { String result; try { Request request new Request.Builder() .url(url) .addHeader(Origin, file://) .build(); Response response client.newCall(request).execute(); result response.body() ! null ? response.body().string() : ; } catch (IOException e) { result {\error\:\ e.getMessage() \}; } final String payload result; mainHandler.post(() - { // 回调前端注册的全局函数参数要 JSON 转义 String js window. callbackName ( JSONObject.quote(payload) ); ((WebView) ((Activity) context).findViewById(R.id.web_view)) .evaluateJavascript(js, null); }); }).start(); } JavascriptInterface public String getAppInfo() { return {\platform\:\android\,\version\:\1.0\}; } }注册方式webView.addJavascriptInterface(new JsBridge(this), AndroidBridge)。前端侧调用是AndroidBridge.request(https://api.example.com/data, onData)然后定义function onData(res) { console.log(res) }。参数说明第一个参数是目标 URL第二个是回调函数名必须是前端已经挂到window上的全局函数。JSONObject.quote做字符串转义别省接口返回里有引号或换行时不转义直接拼 JS 会语法错误。安全上JavascriptInterface注解是 API 17 的强制要求没有注解的方法不会被注入这是系统给的隔离机制但也意味着只要页面加载了就能调这些方法所以别在主文档里注入写文件、读通讯录这类能力。file://页面还有个细节Origin头是null而不是file://。上面代码里写addHeader(Origin, file://)是为了骗过部分校验宽松的服务端严格实现的服务端会拒绝。真要彻底解决还是靠 3.2 的响应头重写或者干脆把本地页面放到一个WebViewAssetLoader的虚拟域名下让它有正常的https://源。4. 编译与跑通从源码到可安装 APK拿到源码第一件事不是读代码是先让它跑起来。跑通了再改改坏了能回滚这是基本盘。4.1 环境与 Gradle 配置需要 JDK 17、Android SDKcompileSdk 34 左右、Gradle 8.x。项目根目录的build.gradle里通常是 AGP 版本声明app模块的配置如下android { namespace com.example.browser compileSdk 34 defaultConfig { applicationId com.example.browser minSdk 21 // WebView 跨域相关 API 在 21 之后才稳定 targetSdk 34 versionCode 1 versionName 1.0 } buildTypes { release { minifyEnabled false // 先关混淆跨域调试期别给自己加难度 proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation androidx.webkit:webkit:1.8.0 // WebViewAssetLoader 在这里 implementation com.squareup.okhttp3:okhttp:4.11.0 implementation com.google.android.material:material:1.11.0 }minSdk 21不是随便定的。setMixedContentMode是 API 21 引入shouldInterceptRequest带WebResourceRequest参数的重载也是 21 才有。低于 21 只能拿到 URL 字符串拿不到请求头和 method跨域拦截基本残废。androidx.webkit这个依赖建议加上WebViewAssetLoader能把本地资源映射成https://appassets.androidplatform.net/这样的虚拟源从根上绕开file://的一堆限制。minifyEnabled false在调试期是明智的。开了混淆之后JavascriptInterface标注的方法名可能被改前端调AndroidBridge.request就找不到方法报错信息还特别隐晦。要上生产再单独配-keepclassmembers规则。4.2 权限、签名与首次启动AndroidManifest.xml里三处必须配uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / application android:name.BrowserApp android:usesCleartextTraffictrue android:networkSecurityConfigxml/network_security_config android:hardwareAcceleratedtrue activity android:name.ui.BrowserActivity android:exportedtrue android:configChangesorientation|screenSize|keyboardHidden intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity /applicationusesCleartextTraffictrue是为了让 http 请求能发出去Android 9 之后默认禁止明文流量。如果只想放行特定域名用network_security_config.xml精确配置比全局放开安全network-security-config base-config cleartextTrafficPermittedfalse / domain-config cleartextTrafficPermittedtrue domain includeSubdomainstrue192.168.1.100/domain /domain-config /network-security-configconfigChanges那段别省。浏览器旋转屏幕时如果不拦下配置变更Activity 会重建WebView 跟着销毁用户输入的地址、页面滚动位置全丢。加上之后由onConfigurationChanged自己处理体验接近原生浏览器。编译命令就两条# 生成 debug 包直接可安装 ./gradlew :app:assembleDebug # 装到已连接的真机 adb install -r app/build/outputs/apk/debug/app-debug.apk4.3 真机验证跨域是否生效装完先别急着打开网页用adb logcat盯住 WebView 的 console 输出adb logcat -s chromium:V WebViewFactory:V然后在 App 里加载一个会发跨域请求的测试页页面上放一段fetch(https://httpbin.org/get) .then(r r.json()) .then(d console.log(CORS_OK, JSON.stringify(d))) .catch(e console.log(CORS_FAIL, e.message));日志里出现CORS_OK就说明拦截链路通了。出现CORS_FAIL的话按顺序查三件事setAllowUniversalAccessFromFileURLs开了没、shouldInterceptRequest有没有真的被调到加一行Log.d验证、返回的mimeType是不是带了charset参数导致解析失败。调试期间强烈建议开WebView.setWebContentsDebuggingEnabled(true)然后 PC 上 Chrome 打开chrome://inspect能看到 App 里 WebView 的完整 DevTools 面板Network、Console、Elements 全都可用。这是排查跨域问题最直接的手段比看 logcat 猜快十倍。5. 避坑排查跨域、Cookie 与混合内容的翻车记录这一章全是实际遇到的坑按「现象 → 原因 → 解决」记遇到对得上的直接抄。现象一本地 HTML 用fetch调接口控制台报TypeError: Failed to fetch没有任何 CORS 提示。原因Android 5.0 起setAllowUniversalAccessFromFileURLs默认是falsefile://页面发起的跨源请求在源头就被掐了连请求都没发出去所以看不到 CORS 错误。解决在WebViewInitializer里显式调用settings.setAllowUniversalAccessFromFileURLs(true)和setAllowFileAccessFromFileURLs(true)两个都要开。只开后者只能访问同目录下的文件跨不了源。现象二拦截重写响应后页面样式全丢JS 不执行Network 面板里资源状态是 200。原因Content-Type直接透传成application/json; charsetutf-8WebResourceResponse的 mimeType 参数只接受纯 MIME 类型带参数会让 WebView 无法识别资源类型。解决contentType.split(;)[0].trim()取纯类型编码单独解析。还有一种情况是Content-Encoding: gzip没处理OkHttp 默认会自动解压但如果你手动透传了这个头WebView 会二次解压失败。解决重写响应头时把Content-Encoding和Content-Length删掉。现象三登录接口返回 200Set-Cookie 也在响应头里但后续请求全是 401。原因shouldInterceptRequest里自己用 OkHttp 发的请求响应里的Set-Cookie只存在于你构造的WebResourceResponse头里没有同步给CookieManagerWebView 后续请求自然带不上。解决在拿到响应后手动提取Set-Cookie并写入CookieManager.getInstance().setCookie(url, cookie)再调用flush()。另一个原因是跨域请求默认不带第三方 Cookie需要CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true)。现象四POST 请求经过拦截后变成 GET请求体丢失。原因shouldInterceptRequest的设计只给 URL 和请求头拿不到请求体这是 API 层面的硬限制不是配置问题。解决POST 不要走拦截直接返回null让 WebView 自己发确实需要处理跨域 POST就在 JS 层 hookXMLHttpRequest和fetch把请求转发给JsBridge由原生代发。这也是为什么 3.2 的代码里第一行就判断GET.equalsIgnoreCase(request.getMethod())。现象五页面白屏logcat 干干净净什么异常都没有。原因JS 运行时错误被 WebView 吞了或者 WebView 的数据目录损坏多见于反复安装卸载、多进程共用 WebView。解决先开chrome://inspect看 Console确认不是 JS 问题后在Application的onCreate里加WebView.setDataDirectorySuffix(browser)给 WebView 独立数据目录避免和其他进程冲突。这招在集成第三方 SDK很多 SDK 内部也用 WebView时特别管用。注意Access-Control-Allow-Origin: *和Access-Control-Allow-Credentials: true同时出现浏览器规范上是拒绝的。需要带凭证的跨域Origin必须回填成请求来源的具体值不能通配。6. 二次开发把跨域能力抽成独立模块跑通之后想复用到别的项目最直接的做法是把web和bridge两个包抽成:browser-core模块App 只依赖接口。改造分三步。第一步定义能力接口把「配什么」和「怎么配」解耦public interface WebEngine { void attach(WebView webView); void loadUrl(String url); void setCorsInterceptor(CorsInterceptor interceptor); void destroy(); }第二步把WebViewInitializer和CorsWebViewClient收进BrowserEngineImpl对外只暴露WebEngine。这样宿主 App 不直接碰WebSettings升级 WebView 配置时只改一处。第三步用WebViewAssetLoader替掉file://。本地页面统一走https://appassets.androidplatform.net/assets/源是正常 https跨域规则和线上一致setAllowUniversalAccessFromFileURLs这类危险开关就可以关掉安全性上一个台阶。WebViewAssetLoader loader new WebViewAssetLoader.Builder() .addPathHandler(/assets/, new WebViewAssetLoader.AssetsPathHandler(context)) .build(); webView.setWebViewClient(new WebViewClient() { Override public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { return loader.shouldInterceptRequest(request.getUrl()); } });改造完怎么验证拿同一份 H5 资源分别用file://和老配置、https://appassets和新配置各跑一遍对比 Network 面板里每个请求的Origin头和响应头。两边行为一致才算真的抽干净了。我自己的习惯是任何 WebView 相关的改动先写一个最小复现页就一个fetch加一个按钮在改动前后各跑一次确认控制台输出没变才提交。跨域这东西玄学成分太高靠肉眼读代码判断不了只能靠可复现的验证。从那以后我每次改WebSettings或者shouldInterceptRequest都强制走一遍这个最小页翻车的次数确实少了很多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑