JSBridge 실현 메커니즘

20761 단어
개술
안 드 로 이 드 에서 4.2 이전에 addJavaScriptInterface () 는 js 에 native 를 호출 하 는 방법 을 제공 합 니 다. 안전 위험 이 존재 합 니 다. 구체 적 으로 어떻게 발생 하 는 지 보 세 요. addJavaScriptInterface 위험 안 드 로 이 드 는 현재 webView Client 의 shouldOverrideUrl Loading (url) 과 WebChromeClient 의 onJSPrompt (url) 함수 로 문 제 를 해결 하고 있 습 니 다.나타 난 해결 방안 은 비교적 성숙 한 프레임 워 크 인 WebViewJS Bridge 가 있 습 니 다. 오늘 제 가 말씀 드 리 고 싶 은 것 은 바로 이 프레임 워 크 입 니 다. 본인 도 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 로 이동 하여 주요 코드 를 제공 합 니 다.

        

JS OC

// , 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) }); };

페이지 를 불 러 올 때 setUpWebView JavascripBridge (callabck) 방법 이 실 행 됩 니 다. 이 때 webview 의 shouldStart Load With Request 방법 이 실 행 됩 니 다. 실현 을 보 겠 습 니 다.
#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

위의 NSString * js = WebView JavascriptBridgejs(); [self evaluate Javascript: js] 에 서 는 H5 단 에 웹 뷰 JavaScriptBridge 대상 을 등록 한 것 을 알 수 있 습 니 다. 이 대상 은 call Hanlder 와 register 방법 이 있 고 sendmessage Queue 메시지 큐 가 있 습 니 다.
클릭 하여 앨범 열기 버튼
 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 방법 으로 메 시 지 를 만 들 고 메시지 큐 에 추가 합 니 다. 이 메 시 지 는 {handler Name: '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: message QueueString]
- (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);
        }
    }
}

포켓 회전 이 다시 시작 점 으로 돌 아 왔 습 니 다. 위 에서 네 이 티 브 엔 드 와 h5 엔 드 가 같은 handler Name 을 포함 하고 있 음 을 알 수 있 습 니 다. 네 이 티 브 엔 드 registerHandler 는 H5 엔 드 callHandler 에 있어 야 합 니 다. callHandler 는 OC 엔 드 에 메 시 지 를 보 내 는 프로 토 콜 방법 을 실 행 했 습 니 다. 이후 OC 엔 드 는 H5 엔 드 에서 message Queue 를 받 았 습 니 다.OC 엔 드 에서 message Queue 를 최종 분석 하여 message Handlers 의 block 리 셋 모듈 WVJhandler 를 찾 았 습 니 다. 그 후에 block (data, responseCallback) 이 응답 한 결 과 를 얻 었 습 니 다. 그런데 이 responseCallback 은 어떻게 된 일 입 니까?서 두 르 지 마 세 요. registerhandler 의 response Callback ("메 시 지 를 받 았 습 니 다") 을 기억 하 세 요.
response Callback 분석 뭐 했 어 요?
 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:@"
" withString:@"\
"]; 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 중의 를 촉발 합 니 다.dispatchMessage FromObjC 방법 은 최종 적 으로 callbackId 를 통 해 대응 하 는 callback 을 찾 아 리 턴 값 을 실현 합 니 다.
결어
저 는 단 방향 으로 H5 단 트리거 사건 부터 리 턴 까지 전체 과정 이 가치 가 있 습 니 다. 그리고 OC 단 에서 H5 단 을 촉발 한 것 입 니 다. 여 기 는 분석 하지 않 고 원리 가 일치 합 니 다. 마지막 으로 12 시 에 도 글 자 를 외 우 며 왕 자 를 때 리 고 잤 습 니 다.
1. Android 4.2 이하, addJavascriptInterface 방법 에 보안 구멍 이 있 습 니 다. js 코드 는 자바 층 의 운행 대상 을 가 져 와 현재 사용자 가 악성 코드 를 실행 하 는 것 을 위조 할 수 있 습 니 다.2, ios 7 이하, JavaScript 는 native 코드 를 호출 할 수 없습니다.3. js 성명 의 대상 은 loadUrl 을 통 해 페이지 에 주입 되 기 때문에 이 대상 은 자바 대상 이 아니 라 js 대상 이 고 getClass 등 Object 방법 이 없 기 때문에 Runtime 대상 을 얻 을 수 없고 악성 코드 의 주입 을 피 할 수 있 습 니 다.4. JSBridge 는 URL 해석 의 상호작용 방식 으로 성숙 한 해결 방안 으로 확대 하기 쉽 고 중대 한 안전성 문제 가 없다.

좋은 웹페이지 즐겨찾기