使用jsbridge的原因
原生webview h5调用native方法使用addJavaScriptInterface,然而4.2以下系统没有对注册java类的方法做限制,导致攻击者可以利用反射调用其他未注册的任何java类。出于安全性考虑使用jsbridge来处理h5和native的交互。
项目地址
https://github.com/lzyzsd/JsBridge
使用方法/两端增加的额外代码比较少
1.native注册方法供h5调用
1 2 3 webView.registerHandler("submitFromWeb" ) { data, function -> function .onCallBack("submitFromWeb exe, response data 中文 from Java" ) }
2.native调用h5方法
1 2 3 webView.callHandler("functionInJs" , Gson() .to Json(user ) ) { data -> Log . e("处理结果" , data) }
3.h5注册方法供native调用
1 2 3 4 5 6 bridge.registerHandler("functionInJs" , function (data , responseCallback ) { if (responseCallback) { let responseData = "Javascript Says Right back aka!" ; responseCallback(responseData ) ; } })
4.h5调用native的方法
1 2 3 4 5 6 7 window .WebViewJavascriptBridge.callHandler( 'submitFromWeb' , {'param' : '中文测试' } , function (responseData ) { document .getElementById("show" ).innerHTML = "send get responseData from java, data = " + responseData } );
源码解析
从上面的使用中可以看到window.WebViewJavascriptBridge和bridge对象,window中好像没有这些对象?WebViewJavascriptBridge从哪里来?
答案在BridgeWebViewClient的onPageFinished中
onPageFinished中有这么一段代码
1 BridgeUtil . webViewLoadLocalJs(view , BridgeWebView.toLoadJs ) ;
跟进BridgeUtil
1 2 3 4 public static void webViewLoadLocalJs(WebView view , String path ) { String jsContent = assetFile2Str(view .getContext () , path); view.loadUrl("javascript:" + jsContent ) ; }
h5页面加载完成之后执行assets下面的WebViewJavascriptBridge.js,然后将WebViewJavascriptBridge.js里面的一系列方法加入到window中。我们可以看WebViewJavascriptBridge.js中的代码。
1 2 3 4 5 6 7 8 window.WebViewJavascriptBridge = { init: init, send: send, registerHandler: registerHandler, callHandler: callHandler, _fetchQueue: _fetchQueue, _handleMessageFromNative: _handleMessageFromNative };
这样WebViewJavascriptBridge对象就绑定到window中了。这是最重要的一步。接下来我们分两端来分析整个交互过程。
native注册方法供h5调用
注册方法
1 2 3 webView.registerHandler("submitFromWeb" ) { data, function -> function .onCallBack("submitFromWeb exe, response data 中文 from Java" ) }
跟进registerHandler函数
1 2 3 4 5 6 7 8 Map <String , BridgeHandler> messageHandlers = new HashMap<String , BridgeHandler>();public void registerHandler (String handlerName, BridgeHandler handler ) { if (handler != null ) { messageHandlers.put(handlerName, handler); } }
将注册的方法添加到messageHandlers中,handlerName是方法的名字,BridgeHandler是个接口
1 2 3 public interface BridgeHandler { void handler (String data, CallBackFunction function) ; }
用来回调h5传送的值和处理回传h5结果
h5端调用刚注册的submitFromWeb方法。趁热!!!
1 2 3 4 5 6 7 window .WebViewJavascriptBridge.callHandler( 'submitFromWeb' , {'param' : '中文测试' } , function (responseData ) { document .getElementById("show" ).innerHTML = "send get responseData from java, data = " + responseData } );
跟进到WebViewJavascriptBridge.js中看callHandler
1 2 3 4 5 6 function callHandler(handlerName, data , responseCallback) { _doSend({ handlerName: handlerName, data : data }, responseCallback); }
跟进_doSend
1 2 3 4 5 6 7 8 9 10 11 12 13 var CUSTOM_PROTOCOL_SCHEME = 'yy' ;var QUEUE_HAS_MESSAGE = '__QUEUE_MESSAGE__/' ;function _doSend (message, responseCallback ) { if (responseCallback) { var callbackId = 'cb_' + (uniqueId++) + '_' + new Date ().getTime(); responseCallbacks[callbackId] = responseCallback; message.callbackId = callbackId; } sendMessageQueue.push(message); messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; }
分为有回调结果和没回调结果两种,有callback在message对象添加callbackId,然后将message对象添加到sendMessageQueue数组中,然后通过改变消息列表iframe的src触发shouldOverrideUrlLoading。消息列表iframe的创建代码也在WebViewJavascriptBridge.js中。
1 2 3 4 5 6 7 8 function _createQueueReadyIframe(doc ) { messagingIframe = doc.createElement('iframe ') ; messagingIframe.style.display = 'none'; doc.documentElement.appendChild(messagingIframe ) ; } var doc = document;_createQueueReadyIframe(doc ) ;
接下来就看BridgeWebViewClient中shouldOverrideUrlLoading回调处理了。
1 2 3 4 5 6 7 8 9 10 String YY_OVERRIDE_SCHEMA = "yy://" ;String YY_RETURN_DATA = YY_OVERRIDE_SCHEMA + "return/" ; if (url .startsWith(BridgeUtil.YY_RETURN_DATA)) { webView.handlerReturnData(url ); return true ; } else if (url .startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { webView.flushMessageQueue(); return true ; }
第一个分支是处理返回数据,当前我们要看的是第二个分支,跟进flushMessageQueue代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 重点关注String JS_FETCH_QUEUE_FROM_JAVA = "javascript:WebViewJavascriptBridge._fetchQueue();" void flushMessageQueue ( ) { loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction ( ) { } }public void loadUrl (String jsUrl, CallBackFunction returnCallback ) { this .loadUrl(jsUrl); responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback); }
第一步调用请求数据的js方法,第二步把回调处理添加到responseCallbacks Map中。
继续跟到WebViewJavascriptBridge.js中
1 2 3 4 5 6 7 8 var CUSTOM_PROTOCOL_SCHEME = 'yy' ;function _fetchQueue ( ) { var messageQueueString = JSON .stringify(sendMessageQueue); sendMessageQueue = []; if (messageQueueString !== '[]' ) { bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent (messageQueueString); } }
序列化消息队列,改变消息体的iframe的src将消息体回调到BridgeWebViewClient的shouldOverrideUrlLoading中,消息体添加在url中。消息体的iframe创建与消息列表的类似。
继续跟到shouldOverrideUrlLoading中
1 2 3 4 5 6 7 8 9 10 String YY_OVERRIDE_SCHEMA = "yy://" ;String YY_RETURN_DATA = YY_OVERRIDE_SCHEMA + "return/" ; if (url .startsWith(BridgeUtil.YY_RETURN_DATA)) { webView.handlerReturnData(url ); return true ; } else if (url .startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { webView.flushMessageQueue(); return true ; }
调用了handlerReturnData方法
1 2 3 4 5 6 7 8 9 10 void handlerReturnData(String url ) { String functionName = BridgeUtil . getFunctionFromReturnUrl(url ) ; CallBackFunction f = responseCallbacks.get(functionName); String data = BridgeUtil . getDataFromReturnUrl(url ) ; if (f != null) { f.onCallBack(data ) ; responseCallbacks.remove(functionName); return; } }
从responseCallbacks中取出对应的回调处理数据,具体消息从url过滤中json对象解析得来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 if (!TextUtils . isEmpty(callbackId ) ) { responseFunction = new CallBackFunction() { @Override public void onCallBack(String data ) { Message responseMsg = new Message() ; responseMsg.setResponseId(callbackId ) ; responseMsg.setResponseData(data ) ; queueMessage(responseMsg ) ; } }; }else { responseFunction = new CallBackFunction() { @Override public void onCallBack(String data ) { } }; } BridgeHandler handler; if (!TextUtils . isEmpty(m .getHandlerName () )) { handler = messageHandlers.get(m.getHandlerName() ); } else { handler = defaultHandler; } if (handler != null){ handler.handler(m.getData() , responseFunction); }
分为需要告诉h5执行结果和不需要两种,不需要告诉的什么都不做就不需要说了。需要告诉的设置message中的responseId和data,执行queueMessage。
1 2 3 4 5 6 7 private void queueMessage (Message m ) { if (startupMessage != null ) { startupMessage.add (m); } else { dispatchMessage(m); } }
startupMessage在onPageFinished中会置为null,具体的作用后面会讲到。这边会执行dispatchMessage方法。
1 2 3 4 5 6 7 8 9 10 11 String JS_HANDLE_MESSAGE_FROM_JAVA = "javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');" ; void dispatchMessage(Message m ) { String messageJson = m.to Json() ; String javascriptCommand = String . format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson); if (Thread . currentThread() == Looper . getMainLooper() .getThread() ) { this.loadUrl(javascriptCommand ) ; } }
又回到了WebViewJavascriptBridge.js的_handleMessageFromNative中,把结果塞回到html中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function _handleMessageFromNative(messageJSON ) { if (receiveMessageQueue) { receiveMessageQueue.push(messageJSON); } _dispatchMessageFromNative(messageJSON ) ; } function _dispatchMessageFromNative(messageJSON ) { setTimeout(function () { var message = JSON . parse(messageJSON); var responseCallback; if (message.responseId) { responseCallback = responseCallbacks[message .responseId ] ; if (!responseCallback) { return; } responseCallback(message .responseData ) ; delete responseCallbacks[message .responseId ] ; } } }
为了看得更清楚我画了一张图
h5注册方法供native调用
因为h5页面加载完成的时候WebViewJavascriptBridge对象还没有添加到window中,所以无法调用WebViewJavascriptBridge.registerHandler方法。这里有一个巧妙的做法。看下面的代码。
WebViewJavascriptBridge.js中创建了一个叫WebViewJavascriptBridgeReady自定义事件,加载到的时候立马执行。
1 2 3 4 var doc = document ; var readyEvent = doc .createEvent('Events' ); readyEvent.initEvent('WebViewJavascriptBridgeReady' );doc .dispatchEvent(readyEvent);
html页面中添加了WebViewJavascriptBridgeReady事件的监听
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function connectWebViewJavascriptBridge (callback ) { if (window .WebViewJavascriptBridge) { callback(WebViewJavascriptBridge) } else { document .addEventListener( 'WebViewJavascriptBridgeReady' , function ( ) { callback(WebViewJavascriptBridge) }, false ); } } connectWebViewJavascriptBridge(function (bridge ) { bridge.registerHandler("functionInJs" , function (data, responseCallback ) { document .getElementById("show" ).innerHTML = ("data from Java: = " + data); if (responseCallback) { var responseData = "Javascript Says Right back aka!" ; responseCallback(responseData); } }); })
也就是说增加了一个钩子,WebViewJavascriptBridge.js执行完成就能通知到html,随后就可以注册事件了,这边注册了一个叫functionInJs的事件。
native调用该方法
1 2 3 webView.callHandler("functionInJs" , Gson() .to Json(user ) ) { data -> Log . e("处理结果" , data) }
跟进BridgeWebview的callHandler方法
1 2 3 public void callHandler(String handlerName , String data , CallBackFunction callBack ) { do Send(handlerName , data , callBack ) ; }
跟进doSend
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private void do Send(String handlerName , String data , CallBackFunction responseCallback ) { Message m = new Message() ; if (!TextUtils . isEmpty(data ) ) { m.setData(data ) ; } if (responseCallback != null) { String callbackStr = String . format(BridgeUtil.CALLBACK_ID_FORMAT, ++uniqueId + (BridgeUtil.UNDERLINE_STR + SystemClock . currentThreadTimeMillis() )); responseCallbacks.put(callbackStr, responseCallback); m.setCallbackId(callbackStr ) ; } if (!TextUtils . isEmpty(handlerName ) ) { m.setHandlerName(handlerName ) ; } queueMessage(m ) ; }
组装Message,有responseCallback把responseCallback添加到responseCallbacks
跟进queueMessage
1 2 3 4 5 6 7 private void queueMessage (Message m ) { if (startupMessage != null ) { startupMessage.add (m); } else { dispatchMessage(m); } }
这边又看到了startupMessage,startupMessage就是在这里用的。这边startupMessage不为null,把消息添加到了startupMessage中。执行是在onPageFinished中。
1 2 3 4 5 6 if (webView.getStartupMessage() != null) { for (Message m : webView.getStartupMessage() ) { webView.dispatchMessage(m ) ; } webView.setStartupMessage(null ) ; }
走到了dispatchMessage
1 2 3 4 5 6 7 8 9 10 11 String JS_HANDLE_MESSAGE_FROM_JAVA = "javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');" ; void dispatchMessage(Message m ) { String messageJson = m.to Json() ; String javascriptCommand = String . format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson); if (Thread . currentThread() == Looper . getMainLooper() .getThread() ) { this.loadUrl(javascriptCommand ) ; } }
跟进WebViewJavascriptBridge.js的_handleMessageFromNative
1 2 3 4 5 6 7 function _handleMessageFromNative(messageJSON ) { if (receiveMessageQueue) { receiveMessageQueue.push(messageJSON); } _dispatchMessageFromNative(messageJSON ) ; }
走到_dispatchMessageFromNative
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function _dispatchMessageFromNative (messageJSON ) { setTimeout (function ( ) { var message = JSON .parse(messageJSON); if (message.callbackId) { var callbackResponseId = message.callbackId; responseCallback = function (responseData ) { _doSend({ responseId : callbackResponseId, responseData : responseData }); }; } var handler = WebViewJavascriptBridge._messageHandler; if (message.handlerName) { handler = messageHandlers[message.handlerName]; } try { handler(message.data, responseCallback); } catch (exception) { if (typeof console != 'undefined' ) { console .log("WebViewJavascriptBridge: WARNING: javascript handler threw." , message, exception); } } } }
有回调的情况组装responseCallback,从messageHandlers找到对应的handler处理数据。回调数据在_doSend中发送
1 2 3 4 5 6 7 8 9 10 11 12 13 var CUSTOM_PROTOCOL_SCHEME = 'yy' ; var QUEUE_HAS_MESSAGE = '__QUEUE_MESSAGE__/' ; function _doSend (message, responseCallback ) { if (responseCallback) { var callbackId = 'cb_' + (uniqueId++) + '_' + new Date ().getTime(); responseCallbacks[callbackId] = responseCallback; message.callbackId = callbackId; } sendMessageQueue.push(message); messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; }
改变消息体iframe的src,将回调数据返回到shouldOverrideUrlLoading中
1 2 3 4 5 6 7 8 9 10 void handlerReturnData(String url ) { String functionName = BridgeUtil . getFunctionFromReturnUrl(url ) ; CallBackFunction f = responseCallbacks.get(functionName); String data = BridgeUtil . getDataFromReturnUrl(url ) ; if (f != null) { f.onCallBack(data ) ; responseCallbacks.remove(functionName); return; } }
从responseCallbacks拿到对应的CallBackFunction回调数据,至此回调结果页拿到了。
其他
1.添加默认处理
1 2 3 4 5 6 7 8 9 10 11 12 13 function init (messageHandler) { WebViewJavascriptBridge._messageHandler = messageHandler; } bridge.init(function (message, responseCallback) { var data = { 'Javascript Responds' : '测试中文!' }; if (responseCallback) { responseCallback(data); } });
2._dispatchMessageFromNative为什么要异步执行
总结
h5发送数据给native通过改变iframe的src回调到shouldOverrideUrlLoading
native发送消息给h5通过loadurl