概述
在android 中 4.2之前addJavaScriptInterface()迄靠,提供給js調(diào)用native的方法,存在安全隱患,具體怎么發(fā)生的請(qǐng)看這里addJavaScriptInterface 隱患
android 現(xiàn)在基本借助webViewClient中的 shouldOverrideUrlLoading(url)和WebChromeClient 中的onJsPrompt(url)函數(shù)來解決問題.而由onJsPrompt(url)出現(xiàn)的解決方案有一個(gè)比較成熟的框架WebViewJsBridge. 今天我要講的就是這個(gè)框架,因?yàn)楸救艘哺鉕C開發(fā)锦聊,也做了一段時(shí)間了,正好看到這一塊了箩绍,就做一個(gè)OC版本的分析孔庭,原理基本一致.
原理
分析一個(gè)東西的一種方式可以先從結(jié)果入手,知道怎么用材蛛,再?gòu)氖褂玫娜肟谠驳剑鹨簧钊胪诰颍瑥狞c(diǎn)到面卑吭,這是我一貫的做事風(fēng)格芽淡,好了,不說了先看使用. 假如H5端想打開native的相冊(cè)界面豆赏,以下是OC端調(diào)用代碼.
self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.webView];
[self.bridge setWebViewDelegate:self];
/* JS調(diào)用OC的API:訪問相冊(cè),這是核心代碼 registerHandler方法 */
[self.bridge registerHandler:@"openCamera" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"需要%@圖片", data[@"count"]);//data 是js端返回回來的數(shù)據(jù). responseCallback是OC端希望返回給js的數(shù)據(jù).
responseCallback(@"我收到消息了");
UIImagePickerController *imageVC = [[UIImagePickerController alloc] init];
imageVC.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
[self presentViewController:imageVC animated:YES completion:nil];
}];
我們進(jìn)入registerHanlder函數(shù)
typedef void (^WVJBResponseCallback)(id responseData);
typedef void (^WVJBHandler)(id data, WVJBResponseCallback responseCallback);
@property (strong, nonatomic) NSMutableDictionary* messageHandlers;
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
不難看出我們所有在OC中調(diào)用registerHandler函數(shù)后的數(shù)據(jù)都存放在messaeHandlers字典中.我們知道key是上面提到的@"openCamera" 字符串.
接下來我們出發(fā)一個(gè)H5中的動(dòng)作挣菲,打開native相冊(cè)界面
轉(zhuǎn)到H5中,提供主要代碼.
<body>
<h2>JS調(diào)用OC中的方法</h2>
<button id="btn">訪問OC相冊(cè)</button>
</body>
<script>
// 這段代碼是固定的掷邦,必須要放到j(luò)s中
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
// 與OC交互的所有JS方法都要在這里注冊(cè),才能讓OC和JS之間相互調(diào)用
setupWebViewJavascriptBridge(function(bridge) {
/* OC給JS提供公開的API, 在JS中可以手動(dòng)調(diào)用此API, 并且可以接收OC中傳過來的參數(shù),同時(shí)可回調(diào)OC */
// 調(diào)用OC中的打開相冊(cè)方法
document.getElementById('btn').onclick = function () {
bridge.callHandler('openCamera', {'count':'10張'}, function responseCallback(responseData) {
console.log("OC中返回的參數(shù):", responseData)
});
};
</script>
頁(yè)面加載完時(shí)會(huì)觸發(fā)setUpWebViewJavascripBridge(callabck)方法白胀,此時(shí)webview中的shouldStartLoadWithRequest方法會(huì)被觸發(fā),我們看看實(shí)現(xiàn).
#define kOldProtocolScheme @"wvjbscheme"
#define kNewProtocolScheme @"https"
#define kQueueHasMessage @"__wvjb_queue_message__"
#define kBridgeLoaded @"__bridge_loaded__"
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if (webView != _webView) { return YES; }
NSURL *url = [request URL];
__strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
if ([_base isWebViewJavascriptBridgeURL:url]) {
//頁(yè)面初次加載完畢后會(huì)執(zhí)行該代碼.
if ([_base isBridgeLoadedURL:url]) {
//將一對(duì)象注入到H5頁(yè)面中
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];
} else {
[_base logUnkownMessage:url];
}
return NO;
} else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
} else {
return YES;
}
}
//當(dāng)傳過來的是以wvjbscheme和https開頭的url時(shí)返回YES.
- (BOOL)isWebViewJavascriptBridgeURL:(NSURL*)url {
if (![self isSchemeMatch:url]) {
return NO;
}
return [self isBridgeLoadedURL:url] || [self isQueueMessageURL:url];
}
- (BOOL)isSchemeMatch:(NSURL*)url {
NSString* scheme = url.scheme.lowercaseString;
return [scheme isEqualToString:kNewProtocolScheme] || [scheme isEqualToString:kOldProtocolScheme];
}
- (BOOL)isQueueMessageURL:(NSURL*)url {
NSString* host = url.host.lowercaseString;
return [self isSchemeMatch:url] && [host isEqualToString:kQueueHasMessage];
}
//這個(gè)表示url 包含wvjbscheme字符串.
- (BOOL)isBridgeLoadedURL:(NSURL*)url {
NSString* host = url.host.lowercaseString;
return [self isSchemeMatch:url] && [host isEqualToString:kBridgeLoaded];
}
從上的[_base injectJavascriptFile];我們轉(zhuǎn)進(jìn)去看看發(fā)生了什么.
- (void)injectJavascriptFile {
NSString *js = WebViewJavascriptBridge_js();
[self _evaluateJavascript:js];
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
[self _dispatchMessage:queuedMessage];
}
}
}
//我們?cè)倏纯磜ebView JavaScriptBridge_js()
NSString * WebViewJavascriptBridge_js() {
#define __wvjb_js_func__(x) #x
// BEGIN preprocessorJSCode
static NSString * preprocessorJSCode = @__wvjb_js_func__(
;(function() {
if (window.WebViewJavascriptBridge) {
return;
}
if (!window.onerror) {
window.onerror = function(msg, url, line) {
console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
}
}
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
var messagingIframe;
var sendMessageQueue = [];
var messageHandlers = {};
var CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
var responseCallbacks = {};
var uniqueId = 1;
var dispatchMessagesWithTimeoutSafety = true;
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
function disableJavscriptAlertBoxSafetyTimeout() {
dispatchMessagesWithTimeoutSafety = false;
}
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;
}
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
}
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}
}
}
}
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);
registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i=0; i<callbacks.length; i++) {
callbacks[i](WebViewJavascriptBridge);
}
}
})();
); // END preprocessorJSCode
#undef __wvjb_js_func__
return preprocessorJSCode;
};
從上面的 NSString *js = WebViewJavascriptBridge_js();
[self _evaluateJavascript:js]中可知給H5端注冊(cè)了一個(gè)webViewJavaScriptBridge對(duì)象.該對(duì)象有callHanlder和register方法,并且有sendMessageQueue 消息隊(duì)列.
點(diǎn)擊打開相冊(cè)按鈕
bridge.callHandler('openCamera', {'count':'10張'}, function responseCallback(responseData) {
console.log("OC中返回的參數(shù):", responseData)
});
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
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;
}
我們驚訝的發(fā)現(xiàn)會(huì)執(zhí)行_doSend方法創(chuàng)建一條消息,加入到消息隊(duì)列耙饰,該消息包含{handlerName:'openCamera',data:{'count':'10張'},callbackId:'xxxx'};messageingIframe.src= wvjbscheme://wvjb_queue_message,到這里會(huì)觸發(fā)webView的shouldStartRequest方法最終執(zhí)行
- (NSString *)webViewJavascriptFetchQueyCommand {
return @"WebViewJavascriptBridge._fetchQueue();";
}
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
//messageQueueString 是啥纹笼,我們繼續(xù)轉(zhuǎn)到j(luò)sbridge對(duì)象
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
//你會(huì)發(fā)現(xiàn)這個(gè)原來就是消息隊(duì)列json字符串.每條消息是啥上面已經(jīng)說過了.
[_base flushMessageQueue:messageQueueString];
分析 [_base flushMessageQueue:messageQueueString]
- (void)flushMessageQueue:(NSString *)messageQueueString{
if (messageQueueString == nil || messageQueueString.length == 0) {
NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
return;
}
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
if (![message isKindOfClass:[WVJBMessage class]]) {
NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
continue;
}
[self _log:@"RCVD" json:message];
NSString* responseId = message[@"responseId"];
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
} else {
//顯然從上面分析可知.message不包含responseId
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
//從js中的消息得知這個(gè)callbackId是存在的.
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
//當(dāng)我們這樣調(diào)用responseData(...)時(shí)會(huì)觸發(fā)下面的方法.
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}
//還記得我們剛開始registerHandler的 "openCamera"么,
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
//注冊(cè)的那個(gè)block最終在這里接收了傳值.
handler(message[@"data"], responseCallback);
}
}
}
兜兜轉(zhuǎn)轉(zhuǎn)又回到了起點(diǎn),從上面我們可以得知其native端和h5端含有相同的handlerName為了找到對(duì)應(yīng)的回調(diào)方法.在native端registerHandler 就必須在H5端callHandler苟跪,callHandler觸發(fā)了向OC端發(fā)送獲取消息的協(xié)議方法廷痘,之后OC端從H5端拿到了messageQueue,OC端最終解析messageQueue找到messageHandlers中的block 回調(diào)模塊WVJhandler, 之后block(data,responseCallback)得到了響應(yīng)的結(jié)果件已,但是這個(gè)responseCallback 又是怎么回事呢笋额?別急,還記得registerhandler中的responseCallback("我收到消息了");
分析responseCallback做了什么?
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
//WVJBMessage 是一個(gè)宏定義的字典.
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
//當(dāng)我們這樣調(diào)用responseData(...)時(shí)會(huì)觸發(fā)下面的方法.
[self _queueMessage:msg];
};
- (void)_queueMessage:(WVJBMessage*)message {
if (self.startupMessageQueue) {
[self.startupMessageQueue addObject:message];
} else {
[self _dispatchMessage:message];
}
}
- (void)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
[self _log:@"SEND" json:messageJSON];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}
上面的代碼挺簡(jiǎn)單的篷扩,我不想多解釋太多 兄猩,最終會(huì)執(zhí)行下面的代碼.
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
//接下來轉(zhuǎn)入H5
function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
if (message.responseId) {
//WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
}
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}
}
}
}
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
還記得當(dāng)初的callHandler('openCamera',{"count":10},function(data){
}); function(data)就是responseCallback回調(diào).用于接收OC端的回傳值.
在OC中responseCallback(@"我收到消息了")最終觸發(fā)了OC中的_dispatchMessage 方法,該方法進(jìn)而觸發(fā)H5中的_dispatchMessageFromObjC方法鉴未,最終通過callbackId找到對(duì)應(yīng)的callback,實(shí)現(xiàn)回傳值.
結(jié)語(yǔ)
我這里是單向從H5端觸發(fā)事件到回傳值得整個(gè)過程枢冤,還有從OC端觸發(fā)H5端的,這里就不分析了铜秆,原理一致.
最后 淹真,12點(diǎn)了還在碼字,打把王者连茧,睡了.
1核蘸、Android4.2以下,addJavascriptInterface方法有安全漏洞啸驯,js代碼可以獲取到Java層的運(yùn)行時(shí)對(duì)象客扎,來偽造當(dāng)前用戶執(zhí)行惡意代碼。
2罚斗、ios7以下徙鱼,JavaScript無法調(diào)用native代碼。
3针姿、通過js聲明的對(duì)象袱吆,是通過loadUrl注入到頁(yè)面中的,所以這個(gè)對(duì)象是js對(duì)象搓幌,而不是Java對(duì)象杆故,沒有g(shù)etClass等Object方法,因此也無法獲得Runtime對(duì)象溉愁,避免了惡意代碼的注入处铛。
4、JSBridge采用URL解析的交互方式拐揭,是一套成熟的解決方案撤蟆,便于拓展,無重大安全性問題堂污。