概述
在android 中 4.2之前addJavaScriptInterface(),提供给js调用native的方法,存在安全隐患,具体怎么发生的请看这里addJavaScriptInterface 隐患
android 现在基本借助webViewClient中的 shouldOverrideUrlLoading(url)和WebChromeClient 中的onJsPrompt(url)函数来解决问题.而由onJsPrompt(url)出现的解决方案有一个比较成熟的框架WebViewJsBridge. 今天我要讲的就是这个框架,因为本人也搞OC开发,也做了一段时间了,正好看到这一块了,就做一个OC版本的分析,原理基本一致.
原理
分析一个东西的一种方式可以先从结果入手,知道怎么用,再从使用的入口,逐一深入挖掘,从点到面,这是我一贯的做事风格,好了,不说了先看使用. 假如H5端想打开native的相册界面,以下是OC端调用代码.
self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.webView];
[self.bridge setWebViewDelegate:self];
/* JS调用OC的API:访问相册,这是核心代码 registerHandler方法 */
[self.bridge registerHandler:@"openCamera" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"需要%@图片", data[@"count"]);//data 是js端返回回来的数据. responseCallback是OC端希望返回给js的数据.
responseCallback(@"我收到消息了");
UIImagePickerController *imageVC = [[UIImagePickerController alloc] init];
imageVC.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
[self presentViewController:imageVC animated:YES completion:nil];
}];
我们进入registerHanlder函数
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中调用registerHandler函数后的数据都存放在messaeHandlers字典中.我们知道key是上面提到的@"openCamera" 字符串.
接下来我们出发一个H5中的动作,打开native相册界面
转到H5中,提供主要代码.
<body>
<h2>JS调用OC中的方法</h2>
<button id="btn">访问OC相册</button>
</body>
<script>
// 这段代码是固定的,必须要放到js中
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方法都要在这里注册,才能让OC和JS之间相互调用
setupWebViewJavascriptBridge(function(bridge) {
/* OC给JS提供公开的API, 在JS中可以手动调用此API, 并且可以接收OC中传过来的参数,同时可回调OC */
// 调用OC中的打开相册方法
document.getElementById('btn').onclick = function () {
bridge.callHandler('openCamera', {'count':'10张'}, function responseCallback(responseData) {
console.log("OC中返回的参数:", responseData)
});
};
</script>
页面加载完时会触发setUpWebViewJavascripBridge(callabck)方法,此时webview中的shouldStartLoadWithRequest方法会被触发,我们看看实现.
#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]) {
//页面初次加载完毕后会执行该代码.
if ([_base isBridgeLoadedURL:url]) {
//将一对象注入到H5页面中
[_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;
}
}
//当传过来的是以wvjbscheme和https开头的url时返回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];
}
//这个表示url 包含wvjbscheme字符串.
- (BOOL)isBridgeLoadedURL:(NSURL*)url {
NSString* host = url.host.lowercaseString;
return [self isSchemeMatch:url] && [host isEqualToString:kBridgeLoaded];
}
从上的[_base injectJavascriptFile];我们转进去看看发生了什么.
- (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];
}
}
}
//我们再看看webView 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端注册了一个webViewJavaScriptBridge对象.该对象有callHanlder和register方法,并且有sendMessageQueue 消息队列.
点击打开相册按钮
bridge.callHandler('openCamera', {'count':'10张'}, function responseCallback(responseData) {
console.log("OC中返回的参数:", 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;
}
我们惊讶的发现会执行_doSend方法创建一条消息,加入到消息队列,该消息包含{handlerName:'openCamera',data:{'count':'10张'},callbackId:'xxxx'};messageingIframe.src= wvjbscheme://wvjb_queue_message,到这里会触发webView的shouldStartRequest方法最终执行
- (NSString *)webViewJavascriptFetchQueyCommand {
return @"WebViewJavascriptBridge._fetchQueue();";
}
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
//messageQueueString 是啥,我们继续转到jsbridge对象
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
//你会发现这个原来就是消息队列json字符串.每条消息是啥上面已经说过了.
[_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中的消息得知这个callbackId是存在的.
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
//当我们这样调用responseData(...)时会触发下面的方法.
[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;
}
//注册的那个block最终在这里接收了传值.
handler(message[@"data"], responseCallback);
}
}
}
兜兜转转又回到了起点,从上面我们可以得知其native端和h5端含有相同的handlerName为了找到对应的回调方法.在native端registerHandler 就必须在H5端callHandler,callHandler触发了向OC端发送获取消息的协议方法,之后OC端从H5端拿到了messageQueue,OC端最终解析messageQueue找到messageHandlers中的block 回调模块WVJhandler, 之后block(data,responseCallback)得到了响应的结果,但是这个responseCallback 又是怎么回事呢?别急,还记得registerhandler中的responseCallback("我收到消息了");
分析responseCallback做了什么?
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
//WVJBMessage 是一个宏定义的字典.
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
//当我们这样调用responseData(...)时会触发下面的方法.
[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];
});
}
}
上面的代码挺简单的,我不想多解释太多 ,最终会执行下面的代码.
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
//接下来转入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);
}
还记得当初的callHandler('openCamera',{"count":10},function(data){
}); function(data)就是responseCallback回调.用于接收OC端的回传值.
在OC中responseCallback(@"我收到消息了")最终触发了OC中的_dispatchMessage 方法,该方法进而触发H5中的_dispatchMessageFromObjC方法,最终通过callbackId找到对应的callback,实现回传值.
结语
我这里是单向从H5端触发事件到回传值得整个过程,还有从OC端触发H5端的,这里就不分析了,原理一致.
最后 ,12点了还在码字,打把王者,睡了.
1、Android4.2以下,addJavascriptInterface方法有安全漏洞,js代码可以获取到Java层的运行时对象,来伪造当前用户执行恶意代码。
2、ios7以下,JavaScript无法调用native代码。
3、通过js声明的对象,是通过loadUrl注入到页面中的,所以这个对象是js对象,而不是Java对象,没有getClass等Object方法,因此也无法获得Runtime对象,避免了恶意代码的注入。
4、JSBridge采用URL解析的交互方式,是一套成熟的解决方案,便于拓展,无重大安全性问题。