php网站开发岗位要求乐山电商网站开发
php网站开发岗位要求,乐山电商网站开发,阿里云网站建设的步骤过程,舟山公司注册实战复盘#xff1a;在Android TV项目中#xff0c;我是如何用OkHttp与自定义WebViewClient巧妙绕过CORS限制的
最近在为一个Android TV平台开发概念验证#xff08;POC#xff09;应用时#xff0c;我遇到了一个颇为棘手的问题#xff1a;应用内嵌的WebView需要从远程AP…实战复盘在Android TV项目中我是如何用OkHttp与自定义WebViewClient巧妙绕过CORS限制的最近在为一个Android TV平台开发概念验证POC应用时我遇到了一个颇为棘手的问题应用内嵌的WebView需要从远程API服务器加载数据但浏览器严格的同源策略CORS像一堵墙将所有请求都挡在了外面。服务器端无法为我们这个临时的POC项目修改响应头而市面上常见的WebSettings配置方案又统统失效。经过一番摸索和试错我最终找到了一条结合OkHttp进行请求拦截与重写的路径成功解决了问题。这篇文章我将完整还原当时的思考过程、技术选型与具体实现希望能为遇到类似困境的开发者提供一种切实可行的思路。1. 理解Android WebView中的CORS本质为何常规配置无效在Web开发中CORS跨源资源共享是一种由浏览器强制执行的安全机制。当WebView作为浏览器运行时它同样遵循这套规则。很多开发者遇到CORS问题时第一反应是去搜索WebView.getSettings().setXXX系列方法期望通过某个“开关”来关闭限制。我也曾尝试过将GitHub上能找到的相关配置几乎全部复制过来写了长长一串但结果令人沮丧——没有一个真正起效。为什么这些设置不起作用根本原因在于CORS的核心控制权在于服务器返回的响应头如Access-Control-Allow-Origin。WebSettings中那些看似相关的标志位例如setAllowUniversalAccessFromFileURLs其设计初衷是为了处理本地file://协议页面访问其他来源资源的历史遗留问题并且在高版本API中已被标记为废弃。它们并非设计用来绕过现代基于HTTP(S)的、由服务器策略控制的CORS限制。试图通过配置客户端来绕过服务端安全策略这本身就是一个方向性的误区。更深入一层Android官方文档明确指出对于加载本地资源如file:///android_asset/WebView会将其视为一个不透明源。这意味着即使是从本地HTML发起的、目标为远程HTTP(S)服务器的XMLHttpRequest或Fetch请求也会触发完整的CORS预检流程。如果服务器没有返回正确的CORS头请求就会被浏览器引擎即WebView阻止。注意从Android API Level 30开始setAllowFileAccessFromFileURLs和setAllowUniversalAccessFromFileURLs已被废弃。官方推荐使用WebViewAssetLoader来安全地加载本地资源它通过将本地文件映射到虚拟的https://appassets.androidplatform.net/域名下巧妙地解决了协议一致性带来的部分同源问题但这依然无法处理对第三方远程服务器的CORS请求。2. 核心思路从拦截到代理构建请求“中转站”既然客户端配置无法解决问题而服务器端又不可控那么思路就需要转向请求代理。简单来说就是不让WebView直接向目标服务器发起请求而是由我们的Android应用代码Native层作为中间人代为发起请求然后将获取到的数据“包装”成符合WebView预期的格式返回给它。这个思路的关键在于WebViewClient.shouldInterceptRequest(WebView view, WebResourceRequest request)方法。这个方法允许我们拦截WebView发出的每一次资源请求包括主文档、脚本、样式、图片、XHR/Fetch等并提供一个自定义的WebResourceResponse作为响应。方案对比几种代理方式的权衡方案原理优点缺点适用场景直接注入CORS头拦截响应在返回给WebView前手动添加Access-Control-Allow-Origin: *等头信息。实现相对简单对WebView透明。需要能成功获取到服务器原始响应。如果请求因CORS在浏览器层就被阻止则拦截不到。服务器响应可获取但缺少CORS头。Native层完全代理在shouldInterceptRequest中用OkHttp/HttpURLConnection重新发起请求将响应体、状态码、头信息全部封装返回。完全绕过浏览器的CORS检查成功率最高。实现稍复杂需要正确处理所有资源类型MIME类型和流。服务器完全不支持CORS或预检请求失败。修改请求源使用WebViewAssetLoader等工具将页面和资源都置于同一个虚拟源下。符合Web标准无安全隐患。仅适用于控制所有资源本地远程的场景无法解决访问独立第三方API的问题。混合加载本地与可控远程资源。对于我面临的场景——访问一个无法修改的第三方APINative层完全代理是唯一可靠的选择。其核心流程如下WebView尝试加载页面页面内的JavaScript发起一个跨域XHR/Fetch请求。该请求被我们自定义的WebViewClient.shouldInterceptRequest方法捕获。在Native代码中我们使用OkHttp库根据拦截到的请求信息URL、方法、头、体重新构造一个HTTP请求并发送给目标服务器。OkHttp收到服务器的原始响应此时不受浏览器CORS限制。我们将OkHttp的响应数据状态码、头、响应体流封装成一个新的WebResourceResponse对象。将这个WebResourceResponse返回给WebView。对于WebView而言它认为这个响应来自它最初请求的“源”因此不会触发CORS错误。3. 分步实现构建自定义的WebViewClient下面我将结合代码详细拆解实现过程。我们创建一个名为CorsBypassWebViewClient的类它继承自WebViewClientCompat以提供更好的兼容性。3.1 基础架构与依赖首先确保在项目的build.gradle文件中添加OkHttp依赖。dependencies { implementation(com.squareup.okhttp3:okhttp:4.12.0) // 使用稳定版本 }然后开始构建我们的核心类import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView import androidx.webkit.WebViewClientCompat import okhttp3.Headers import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.ResponseBody import java.io.InputStream import java.util.concurrent.TimeUnit class CorsBypassWebViewClient : WebViewClientCompat() { // 使用OkHttp客户端可配置超时、拦截器等 private val okHttpClient: OkHttpClient by lazy { OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .build() } // 预定义的CORS响应头用于“欺骗”WebView private val corsHeaders: MapString, String mapOf( Access-Control-Allow-Origin to *, Access-Control-Allow-Methods to GET, POST, PUT, DELETE, OPTIONS, Access-Control-Allow-Headers to Content-Type, Authorization, X-Requested-With, Access-Control-Max-Age to 86400 ) }这里我们初始化了一个OkHttpClient实例并定义了一组CORS响应头。这些头信息会在我们构建WebResourceResponse时添加进去让WebView认为服务器是允许跨域请求的。3.2 实现请求拦截与转发逻辑核心在于shouldInterceptRequest方法。我们需要区分不同类型的请求本地资源、图片、API请求等并分别处理。override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest ): WebResourceResponse? { val url request.url.toString() val method request.method // 1. 处理本地资源可选如果使用了WebViewAssetLoader可以在此处委托给它 // if (url.startsWith(https://appassets.androidplatform.net/)) { // return assetLoader.shouldInterceptRequest(request.url) // } // 2. 判断是否需要代理通常我们只代理向特定API域名或非本地资源的请求 if (shouldProxyRequest(url)) { return proxyHttpRequest(request) } // 3. 对于其他请求如图片、CSS等可以选择性代理或交由系统默认处理 // 返回null表示由WebView自行处理 return null } private fun shouldProxyRequest(url: String): Boolean { // 这里可以根据你的需求定义规则例如代理所有非本地、非静态资源的请求 val noProxyPrefixes listOf(file://, content://, android.resource://) val isLocal noProxyPrefixes.any { url.startsWith(it) } // 示例代理所有HTTP/HTTPS请求且不是图片等静态资源可根据后缀过滤 return !isLocal (url.startsWith(http://) || url.startsWith(https://)) } private fun proxyHttpRequest(webRequest: WebResourceRequest): WebResourceResponse? { return try { // 将WebResourceRequest转换为OkHttp的Request val okHttpRequest buildOkHttpRequest(webRequest) // 同步执行网络请求注意在后台线程执行更好此处为简化演示 val response okHttpClient.newCall(okHttpRequest).execute() // 将OkHttp的Response转换为WebResourceResponse convertToWebResourceResponse(response) } catch (e: Exception) { e.printStackTrace() // 发生异常时可以返回一个错误响应或者返回null让WebView处理错误 createErrorResponse(e.message ?: Network error) } }关键点解析同步与异步shouldInterceptRequest方法运行在WebView的后台线程但执行阻塞式网络调用call.execute()仍可能在某些情况下影响性能。对于生产环境可以考虑使用协程或异步任务来执行OkHttp请求但需要小心处理线程同步和响应返回的时机。请求过滤shouldProxyRequest函数至关重要。盲目代理所有请求如图片、样式表会显著增加应用复杂度和性能开销。最佳实践是只代理那些确实会触发CORS错误的API请求。3.3 构建请求与转换响应接下来我们实现两个关键的转换函数。private fun buildOkHttpRequest(webRequest: WebResourceRequest): Request { val builder Request.Builder() .url(webRequest.url.toString()) .method(webRequest.method, null) // 请求体处理见下文 // 复制原始请求头注意浏览器自动添加的头如Origin可能不需要 webRequest.requestHeaders?.forEach { (key, value) - // 过滤掉一些可能由浏览器自动添加且不适合传递给服务器的头 if (!key.equals(Origin, ignoreCase true) !key.equals(Referer, ignoreCase true)) { builder.addHeader(key, value) } } // 如果需要处理POST等带有请求体的方法需要额外处理 // 可以从webRequest中获取请求体如果API支持但通常shouldInterceptRequest不提供请求体。 // 对于复杂请求如POST with body此方案可能不适用需要考虑其他方法如注入JS桥接。 return builder.build() } private fun convertToWebResourceResponse(okHttpResponse: okhttp3.Response): WebResourceResponse { val body: ResponseBody okHttpResponse.body ?: throw IllegalStateException(Response body is null) val inputStream: InputStream body.byteStream() // 确定MIME类型优先使用服务器返回的其次根据URL或内容推断 val mimeType okHttpResponse.header(Content-Type)?.split(;)?.first() ?: guessMimeTypeFromUrl(okHttpResponse.request.url.toString()) ?: application/octet-stream val encoding okHttpResponse.header(Content-Encoding) ?: UTF-8 val statusCode okHttpResponse.code val reasonPhrase okHttpResponse.message // 合并服务器响应头和我们自定义的CORS头 val responseHeaders mutableMapOfString, String() okHttpResponse.headers.toMultimap().forEach { (key, values) - // 移除可能引起问题的原有CORS头如果有 if (!key.startsWith(Access-Control-)) { responseHeaders[key] values.joinToString(, ) } } // 添加我们的CORS头 responseHeaders.putAll(corsHeaders) return WebResourceResponse( mimeType, encoding, statusCode, reasonPhrase, responseHeaders, inputStream ) } private fun guessMimeTypeFromUrl(url: String): String? { return when { url.contains(.json) - application/json url.contains(.png) - image/png url.contains(.jpg) || url.contains(.jpeg) - image/jpeg url.contains(.js) - application/javascript url.contains(.css) - text/css url.contains(.html) - text/html else - null } } private fun createErrorResponse(errorMsg: String): WebResourceResponse { // 返回一个简单的错误响应例如一个包含错误信息的JSON val errorJson {error: true, message: $errorMsg} val inputStream errorJson.byteInputStream(Charsets.UTF_8) return WebResourceResponse( application/json, UTF-8, 500, Internal Proxy Error, corsHeaders, inputStream ) }关于MIME类型和请求体的重要提示MIME类型WebResourceResponse必须指定正确的MIME类型否则WebView可能无法正确解析内容例如将JSON当作JavaScript执行会报错。上面的guessMimeTypeFromUrl是一个简单的后备方案最好依赖服务器返回的Content-Type。请求体标准的shouldInterceptRequest方法在Android API 21及以上版本提供的WebResourceRequest对象不包含请求体。这意味着对于POST、PUT等带有请求体的复杂请求此方法无法直接代理。这是该方案的一个主要局限。对于此类请求可能需要在页面加载前通过注入JavaScript代码将这类请求改写为通过Native桥接如JavascriptInterface发起。或者确保你的POC或应用场景以GET请求为主。4. 高级优化与生产环境考量上述方案足以应对大多数POC或简单场景。但如果要将此方案用于更严肃的项目以下几个方面的优化至关重要。4.1 异步请求处理与线程安全在shouldInterceptRequest中执行同步网络请求会阻塞WebView的IO线程。虽然对于少量请求问题不大但为了更好的性能和响应性应该使用异步调用。// 示例使用协程处理异步需在项目中配置协程支持 private suspend fun proxyHttpRequestAsync(webRequest: WebResourceRequest): WebResourceResponse? withContext(Dispatchers.IO) { try { val okHttpRequest buildOkHttpRequest(webRequest) val response okHttpClient.newCall(okHttpRequest).await() // 使用OkHttp的协程扩展或自己封装 convertToWebResourceResponse(response) } catch (e: Exception) { createErrorResponse(e.message ?: Async network error) } } // 注意shouldInterceptRequest本身不是挂起函数调用异步方法需要更复杂的同步机制。 // 一种常见做法是使用WebViewClientCompat.onReceivedError配合自定义协议或标记来处理异步结果。 // 这超出了本文基础范围实现起来更为复杂。更实用的生产级做法可能是结合Service Worker的概念在Native层维护一个请求队列和回调映射但这会大大增加架构复杂度。对于Android TV应用如果请求不是极端频繁同步方式在严格过滤后通常是可以接受的。4.2 缓存策略与性能OkHttp本身拥有强大的缓存机制。我们可以配置一个缓存目录让代理层也能缓存响应减少不必要的网络请求提升加载速度。private val okHttpClient: OkHttpClient by lazy { val cacheDir File(context.cacheDir, okhttp_proxy_cache) val cacheSize 10L * 1024 * 1024 // 10 MB val cache Cache(cacheDir, cacheSize) OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .cache(cache) // 添加缓存 .addInterceptor { chain - val request chain.request() // 可以在这里添加统一的请求头或修改缓存策略 val modifiedRequest request.newBuilder() .header(User-Agent, MyApp-WebView-Proxy/1.0) .build() chain.proceed(modifiedRequest) } .build() }在buildOkHttpRequest函数中你还可以根据原始Web请求的缓存头来精细控制OkHttp请求的缓存行为。4.3 安全性增强这个代理方案本质上是一个“中间人”它必须被谨慎使用证书验证确保OkHttpClient配置了正确的证书验证防止中间人攻击。在开发环境中可能会使用UnsafeOkHttpClient绕过验证但绝不要在发布版本中这样做。请求过滤shouldProxyRequest函数必须严格限制可代理的URL范围防止应用成为开放代理被用来访问恶意或非法网站。敏感信息拦截所有请求意味着可以窥见所有通过WebView发送的数据包括潜在的认证令牌。确保你的应用代码本身是安全的并且不会记录或泄露这些敏感信息。4.4 处理预检请求Preflight Requests对于非简单请求例如使用了自定义头Authorization或方法DELETE浏览器会先发送一个OPTIONS方法的预检请求。我们的代理也需要正确处理它private fun buildOkHttpRequest(webRequest: WebResourceRequest): Request { val builder Request.Builder() .url(webRequest.url.toString()) .method(webRequest.method, null) // ... 复制头信息 // 如果是OPTIONS预检请求我们直接返回一个成功的CORS响应而不真正转发到服务器 // 但更常见的做法是让这个OPTIONS请求也到达服务器由服务器响应。 // 这里演示的是“本地响应”模式 if (webRequest.method OPTIONS) { // 实际上我们可能不需要真正发起网络请求。 // 但为了通用性这里仍然构建请求在convert阶段可以特殊处理。 } return builder.build() } private fun convertToWebResourceResponse(okHttpResponse: okhttp3.Response): WebResourceResponse { // ... 原有逻辑 // 对于OPTIONS请求即使服务器没返回CORS头我们也强制添加 if (okHttpResponse.request.method OPTIONS) { responseHeaders.putAll(corsHeaders) // 确保包含Access-Control-Allow-Headers其值应来自请求的Access-Control-Request-Headers } // ... }5. 在Android TV项目中的集成与测试将定制好的CorsBypassWebViewClient应用到你的WebView上非常简单。class MyActivity : AppCompatActivity() { private lateinit var webView: WebView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) webView findViewById(R.id.webview) val webSettings webView.settings // 必要的WebView基础设置 webSettings.javaScriptEnabled true webSettings.domStorageEnabled true // 如果需要本地存储 webSettings.allowFileAccess false // 基于安全考虑通常关闭 // 应用我们的CORS代理客户端 webView.webViewClient CorsBypassWebViewClient() // 加载你的目标页面 webView.loadUrl(https://your-app-domain.com/index.html) // 或者加载本地HTML // webView.loadUrl(file:///android_asset/web/index.html) } // 处理返回键确保WebView可以后退 override fun onBackPressed() { if (webView.canGoBack()) { webView.goBack() } else { super.onBackPressed() } } }测试要点网络权限别忘了在AndroidManifest.xml中添加uses-permission android:nameandroid.permission.INTERNET /。HTTPS确保你的服务器支持HTTPS并且证书有效。Android P及以上版本对非加密流量的限制更严格。日志调试在proxyHttpRequest和convertToWebResourceResponse方法中添加详细的日志记录拦截的URL、状态码和错误这对排查问题至关重要。模拟CORS错误可以先注释掉代理逻辑确认CORS错误会发生。然后启用代理观察错误是否消失以及数据是否正确加载。在Android TV的大屏设备上进行测试时还需要关注焦点控制、遥控器按键事件处理等TV特有的交互确保WebView内的内容可以正常导航和操作。整个方案实施下来虽然需要一定的Native开发工作量但它提供了最高的灵活性和控制力。它让我在服务器端不可控的约束下依然能够顺利推进Android TV上POC应用的开发验证了核心功能的可行性。当然这终究是一个客户端侧的“变通”方案。在长期的产品规划中与后端团队协作在服务器端正确配置CORS才是符合Web标准、一劳永逸的解决方案。但在时间紧迫、资源有限的探索阶段这个基于OkHttp和自定义WebViewClient的拦截代理策略无疑是一把斩断CORS枷锁的利刃。