نحوه intercept کردن ریکوئست ها در android webview

هنگامی که یک آدرس در webview لود می شود، دو نوع ریکوئست ممکن است در آن آدرس وجود داشته باشد:ریکوئست های لود یک آدرسریکوئست های از جنس XHR که درون آن وبسایت وجود داردگاهی اوقات نیاز داریم این ریکوئست ها را intercept کنیم و به آنها هدر اضافه کنیم؛ در این مقاله قصد دارم انواع مختلف این کار را از ابتدایی ترین روش تا پیشرفته ترین بررسی کرده و مزایا و معایب هرکدام را نیز بیان کنیم.روش اول (پایه ای ترین روش)ابتدایی ترین روش برای این کار افزودن هدر در هنگام لود کردن یک آدرس در Webview می باشد:val header = mutableMapOf<String,String>() header[TOKEN_KEY] = TOKEN webView.loadUrl(url,header)این روش فقط به همان آدرس اولیه ای که webview لود می کند، هدر اضافه می کند و کاری به بقیه ریکوئست ها ندارد.روش دوم (همه یا آدرس هایی با مشخصه خاص)در روش قبل اگر بخواهیم برای همه دامنه ها یا آدرس هایی با مشخصه خاص intercept کنیم، میتوانیم این کار را در کلاینتی که برای webview تعریف میکنیم انجام دهیم.class CustomViewClient:WebViewClient() { override fun shouldInterceptRequest(webView: WebView, request: WebResourceRequest): WebResourceResponse? { return if (request.url.toString().contains(SPECIFIC_REGEX)){ addCustomHeader(webView,request) }else super.shouldInterceptRequest(webView, request) } private fun addCustomHeader(webView: WebView, request: WebResourceRequest): WebResourceResponse? { return try { val okhttp: OkHttpClient = OkHttpClient.Builder().build() val okHttpRequest = Request.Builder().also { it.addHeader(TOKEN_KEY, TOKEN) it.url(request.url.toString()) } val response = okhttp.newCall(okHttpRequest.build()).execute() WebResourceResponse(&quot &quot, UTF-8, response.body?.byteStream()) }catch (e:Exception){ super.shouldInterceptRequest(webView, request) } } }سپس این فایل را در فرگمنت به عنوان کلاینت webview معرفی می کنیم:webView.webViewClient = CustomViewClient()این روش دو ایراد اساسی دارد: به تمامی ریکوئست هایی که توسط webview لود می شوند هدر داده شده را اضافه میکند و این کار باعث افزودن هدر به ریکوئست هایی می شود که نیازی به هدر ندارند که نتیجه آن کاهش سرعت اپلیکیشن مخصوصا برای آدرس هایی که ریکوئست های زیادی دارند، است. در این روش ما به هدر های ریکوئست اولیه و حتی بدنه ریسپانس دسترسی نداریم و برای ریکوئست هایی به متد POST قابل استفاده نیست.همچنین بطور کلی ساز و کار تابع shouldInterceptRequest به این شکل است که ریکوئست ارسال می شود، زمانی که نتیجه آن آماده میشود قبل از ارسال جواب به webview، این تابع فراخوانی می شود و در صورتی که این تابع override نشده باشد و یا مقدار بازگشتی null باشد، اتفاقی نمیفتد در غیر اینصورت ابجکتی که از جنس که WebResourceResponse ساخته ایم را به webview می فرستد.روش سوم (نوع خاصی از ریکوئست ها)در روش قبلی اگر بخواهیم برای ریکوئست هایی که از نوع AJAX هستند، intercept را انجام دهیم، می توانیم قبل از این کار ابتدا نوع ریکوئست را تعیین کرده و سپس intercept کنیم.class CustomViewClient:WebViewClient() { override fun shouldInterceptRequest(webView: WebView, request: WebResourceRequest): WebResourceResponse? { return if (getMimeType(request.url.toString()) == null){ addCustomHeader(webView,request) }else super.shouldInterceptRequest(webView, request) } private fun addCustomHeader(webView: WebView, request: WebResourceRequest): WebResourceResponse? { return try { val okhttp: OkHttpClient = OkHttpClient.Builder().build() val okHttpRequest = Request.Builder().also { it.addHeader(TOKEN_KEY, TOKEN) it.url(request.url.toString()) } val response = okhttp.newCall(okHttpRequest.build()).execute() WebResourceResponse(&quot &quot, UTF-8, response.body?.byteStream()) }catch (e:Exception){ super.shouldInterceptRequest(webView, request) } } private fun getMimeType(url: String?): String? { var type: String? = null val extension = MimeTypeMap.getFileExtensionFromUrl(url) if (extension != null) { when (extension) { js -> { return &quottext/javascript&quot } woff -> { return &quotapplication/font-woff&quot } woff2 -> { return &quotapplication/font-woff2&quot } ttf -> { return &quotapplication/x-font-ttf&quot } eot -> { return &quotapplication/vnd.ms-fontobject&quot } &quotsvg&quot -> { return &quotimage/svg+xml&quot } else -> type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) } } return type } } اگر نوع بازگشتی تابع getMimeType از نوع null باشد، ریکوئست ما AJAX می باشد و می توانیم آن را intercept کنیم. سرعت این روش نسبت به روش های قبلی بیشتر است اما دو ایراد کلی دارد:اگر توکنی داشته باشیم که یکبار مصرف باشد، چون هر ریکوئست دوبار ارسال می شود، آن توکن از بین می رود و دیگر قابل استفاده نیست. فقط مشکل اول روش قبل را حل کرده و همچنان ریکوئست هایی با متد POST را پوشش نمی دهد.مشکل اول را می توان با استفاده لیستی شامل دامنه هایی که نباید intercept شوند مدیریت کرد؛ مشکل دوم را در روش بعدی توضیح می دهم.روش چهارم (متد POST)اگر در بین ریکوئست هایی که نیاز به intercept دارند، متد برخی از آنها POST باشد و نیاز داشته باشیم مقادیری که در بدنه ریسپانس گرفته ایم را در ریسپانس ارسالی به webview نیز داشته باشیم، بصورت زیر میتوان آن را هندل کرد:در ایتدا یک فایل جاوا اسکریپت در پوشه asset به نام دلخواه برای دریافت بدنه ریسپانس می سازیم؛ در اینجا نام آن را requestHeader.js می گذارم:XMLHttpRequest.prototype.origOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url, async, user, password) { // these will be the key to retrieve the payload this.recordedMethod = method; this.recordedUrl = url; this.origOpen(method, url, async, user, password); }; XMLHttpRequest.prototype.origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function(body) { // interceptor is a Kotlin interface added in WebView if(body) recorder.recordPayload(this.recordedMethod, this.recordedUrl, body); this.origSend(body); };سپس در فرگمنت آن را به webview اینجکت می کنیم:webView.evaluateJavascript( requireActivity().assets.open(requestHeader.js).reader().readText(), null)حال نیاز به اینترفیسی داریم که بتوانیم تابع درون فایل جاوا اسکریپتی را فراخوانی کنیم:class GetResponseBody { private val bodyMap: MutableMap<String, String> = mutableMapOf() @JavascriptInterface fun recordPayload( method: String, url: String, payload: String ) { bodyMap[$method-$url] = payload } fun getPayload( method: String, url: String ): String? = bodyMap[$method-$url] }سپس درون کلاینت از آن استفاده می کنیم:class CustomViewClient( val webView: WebView ):WebViewClient() { val recorder = GetResponseBody() init { webView.addJavascriptInterface(recorder, recorder) } override fun shouldInterceptRequest(webView: WebView, request: WebResourceRequest): WebResourceResponse? { return if (getMimeType(request.url.toString()) == null){ val payload = recorder.getPayload(request.method, request.url.toString()) addCustomHeader(webView,request, payload) }else super.shouldInterceptRequest(webView, request) } private fun addCustomHeader(webView: WebView, request: WebResourceRequest, body:String?): WebResourceResponse? { return try { val okhttp: OkHttpClient = OkHttpClient.Builder().build() val okHttpRequest = Request.Builder().also { it.addHeader(TOKEN_KEY, TOKEN) it.method(request.method, body?.toRequestBody(application/json; charset=utf-8.toMediaType())) it.url(request.url.toString()) } val response = okhttp.newCall(okHttpRequest.build()).execute() WebResourceResponse(&quot &quot, UTF-8, response.body?.byteStream()) }catch (e:Exception){ super.shouldInterceptRequest(webView, request) } } private fun getMimeType(url: String?): String? { var type: String? = null val extension = MimeTypeMap.getFileExtensionFromUrl(url) if (extension != null) { when (extension) { js -> { return &quottext/javascript&quot } woff -> { return &quotapplication/font-woff&quot } woff2 -> { return &quotapplication/font-woff2&quot } ttf -> { return &quotapplication/x-font-ttf&quot } eot -> { return &quotapplication/vnd.ms-fontobject&quot } svg -> { return &quotimage/svg+xml&quot } else -> type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) } } return type } }این روش تمامی مشکلاتی که در روش های قبلی گفته شد را حل می کند اما همچنان نیاز داریم که از ریکوئست مجدد به وسیله okhttp استفاده کنیم و نیز سربار تشخیص نوع ریکوئست ها وجود دارد.روش آخر (بهینه ترین روش)تمامی کارهایی که در روش های قبلی در اپلیکیشن انجام شد، می تواند مستقیما از سمت Webview به وسیله جاوا اسکریپت انجام شود. برای این کار ابتدا فایل جاوا اسکریپتی را بصورت زیر تغییر می دهیم:XMLHttpRequest.prototype.origOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.wrappedSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.setRequestHeader = function (header, value) { // Call the wrappedSetRequestHeader function first // so we get exceptions if we are in an erronous state etc. this.wrappedSetRequestHeader(header, value); // Create a headers map if it does not exist if (!this.headers) { this.headers = {}; } // Create a list for the header that if it does not exist if (!this.headers[header]) { this.headers[header] = []; } // Add the value to the header this.headers[header].push(value); } XMLHttpRequest.prototype.open = function (method, url, async, user, password) { // these will be the key to retrieve the payload this.recordedMethod = method; this.recordedUrl = url; this.origOpen(method, url, async, user, password); }; XMLHttpRequest.prototype.origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function (body) { // interceptor is a Kotlin interface added in WebView this.setRequestHeader('TOKEN_KEY', token[&quotTOKEN_KEY&quot]); this.origSend(body); }; const constantMock = window.fetch; window.fetch = function () { this.setRequestHeader('TOKEN_KEY', token[&quotTOKEN_KEY&quot]); return constantMock.apply(this, arguments) }در کلاس بالا از متغیری به نام token، توکن ها را دریافت می کند ولی همانگونه که می بینید در این فایل چنین متغیری تعریف نشده است و باید در کلاس کلاینت این متغیر با سینتکس جاوا اسکریپت نوشته شود و به فایل بالا اضافه شده سپس به webview اینجکت شود:class CustomViewClient( ):WebViewClient() { override fun onPageStarted(webView: WebView?, url: String?, favicon: Bitmap?) { val token = &quotlet token = {\n&quot + &quot \&quotTOKEN_KEY\&quot: \&quotTOKEN\&quot\n&quot + &quot}&quot webView?.evaluateJavascript( &quot${token}\n${webView.context.assets.open(&quotrequestHeader.js&quot).reader().readText()}&quot, null ) super.onPageStarted(webView, url, favicon) } }در این روش تمامی مشکلات روش های قبل برطرف شده و افزایش سرعت قابل توجهی را برای ما به همراه می آورد و باعث می شود نه نیاز به ریکوئست مجدد داشته باشیم و نه تشخیص نوع ریکوئست در سمت کلاینت.البته باید به این نکته توجه کرد که اینجکت کردن جاوا اسکریپت به مرورگر از نظر امنیتی دارای ایراد است؛ البته باید این نکته را نیز در نظر گرفت که مثلا در روش دوم نیز ما با استفاده از okhttp می توانیم ریسپانس دلخواه خود را ساخته و به webview پاس بدهیم که این نیز خود، مشکل امنیتی است؛ این امکان تا کنون توسط مرورگر ها از جمله گوگل محدود نشده است اما اگر در ادامه این امکان از بین برود، باید دنبال راه های جایگزینی برای این کار بود.برای اطلاعات بیشتر راجب مشکلات امنیتی که می تواند با استفاده از هرکدام از روش های بالا به وجود بیاید، می توانید لینک های زیر را مطالعه کنید: https://support.google.com/faqs/answer/9095419?hl=en https://support.google.com/faqs/answer/9084685?hl=en