WKWebView 拦截 POST/PUT方法 丢失body的问题解决

因为采用了离线加载功能,导致近期很多页面上传图片、音频等流数据的接口出现问题,经过分析排查后,发现bodydata为空,但是js端调用Axios的时候,确实是传了的,但是APP拦截后查看NSURLRequest的httpBodyData为空!这就....

一、接口拦截加载的实现

objectivec 复制代码
@interface WKWebViewUrlSchemeHandler : NSObject <WKURLSchemeHandler>

@property (nonatomic, weak) WKWebView *webView;

//自定义处理拦截请求
@property (nonatomic, copy) BOOL (^ holdonSchemeBlock)(id <WKURLSchemeTask> urlSchemeTask);
@end
objectivec 复制代码
@interface WKWebViewUrlSchemeHandler ()


@end

@implementation WKWebViewUrlSchemeHandler

- (void)webView:(nonnull WKWebView *)webView startURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
   [self webRequestWithUrlSchemeTask:urlSchemeTask];

}

/** 停止加载 */
- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask
{
    [urlSchemeTask didFinish];
}

- (void)webRequestWithUrlSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask {
    if (!self.holdonSchemeBlock || !self.holdonSchemeBlock(urlSchemeTask))
    {
        NSURLRequest *originRequest = urlSchemeTask.request;
        
        // 补齐bodyData
        NSString *uploadId = originRequest.allHTTPHeaderFields[[WKWebViewUploadBodyKeys theField]];
        if (uploadId.length > 0) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                NSString *overrideContentType = nil;
                NSData *body = [[WKWebViewUploadBodyKeys sharedKeys] takeBodyForId:uploadId
                                                                   contentType:&overrideContentType
                                                                       timeout:15.0];
                NSMutableURLRequest *rebuilt = [originRequest mutableCopy];
                [rebuilt setValue:nil forHTTPHeaderField:uploadId];
                if (body) {
                    [rebuilt setHTTPBody:body];
                    if (overrideContentType.length > 0) {
                        [rebuilt setValue:overrideContentType forHTTPHeaderField:@"Content-Type"];
                    }
                }
                [self doReqeuset:rebuilt urlSchemeTask:urlSchemeTask];
            });
        } else {
            [self doReqeuset:request urlSchemeTask:urlSchemeTask]
        }
    }
}



- (void)doReqeuset:(NSURLRequest *)request urlSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask
{
//自定义处理协议,如SSE等
    if (!self.holdonSchemeBlock || !self.holdonSchemeBlock(urlSchemeTask))
    {
        NSString *fileUrlStr = request.URL.absoluteString;

        __weak typeof(self) weakSelf = self;
        NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
        NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

            if (!urlSchemeTask) {
                return;
            }

            NSString *rp = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            if (error) {
                [urlSchemeTask didFailWithError:error];
            } 
            else 
            {
                [urlSchemeTask didReceiveResponse:response];
                [urlSchemeTask didReceiveData:data];
                [urlSchemeTask didFinish];
            }

        }];
        [dataTask resume];
        [session finishTasksAndInvalidate];
    }
}
@end

二、注入js

目的是将POST及PUT请求方法的参数拿到APP运行时,补齐请求数据

objectivec 复制代码
@interface WKWebViewUploadBodyKeys : NSObject <WKScriptMessageHandler>

+ (instancetype)sharedKeys;

/// 注入到页面的 JS Hook 脚本(document start 注入)
+ (WKUserScript *)hookUserScript;

/// JS 回传 body 使用的 messageHandler 名称
+ (NSString *)messageHandlerName;

/// js请求数据写入的字段
+ (NSString *)theField;


- (nullable NSData *)takeBodyForId:(NSString *)uploadId
                       contentType:(NSString * _Nullable * _Nullable)contentType
                           timeout:(NSTimeInterval)timeout;

@end
objectivec 复制代码
static NSString *hookjavascript(void) {
    return
    @"(function(){"
    @"  if (window.hookjavascriptInstalled) { return; }"
    @"  window.hookjavascriptInstalled = true;"
    @"  var HEADER = 'X-web-Upload-Id';"
    @"  var NAME = 'HTTP-UploadBody';"
    @"  var seq = 0;"
    @"  function genId(){ seq += 1; return 'opAct-' + Date.now() + '-' + seq + '-' + Math.floor(Math.random()*1e9); }"
    @"  function needIntercept(m){ if(!m){return false;} m = ('' + m).toUpperCase(); return m==='POST'||m==='PUT'||m==='PATCH'; }"
    @"  function ab2b64(buf){"
    @"    var bytes = new Uint8Array(buf); var bin=''; var CH=0x8000;"
    @"    for (var i=0;i<bytes.length;i+=CH){ bin += String.fromCharCode.apply(null, bytes.subarray(i, i+CH)); }"
    @"    return btoa(bin);"
    @"  }"
    @"  function post(id, buf, ct){"
    @"    try{ window.webkit.messageHandlers[NAME].postMessage({ id:id, body:ab2b64(buf), contentType: ct||'' }); }catch(e){}"
    @"  }"
    @"  function serialize(url, body){"
    @"    var req = new Request(url || '/', { method:'POST', body: body });"
    @"    var ct = req.headers.get('content-type') || '';"
    @"    return req.arrayBuffer().then(function(buf){ return { buffer: buf, contentType: ct }; });"
    @"  }"
    @"  function isForm(b){ return (typeof FormData !== 'undefined') && (b instanceof FormData); }"
    @"  var proto = window.XMLHttpRequest && window.XMLHttpRequest.prototype;"
    @"  if (proto) {"
    @"    var rawOpen = proto.open, rawSend = proto.send, rawSet = proto.setRequestHeader;"
    @"    proto.open = function(method, url){ this.__opActM = method; this.__opActU = url; this.__opActHasCT = false; this.__opActAsync = (arguments.length<3)?true:arguments[2]; return rawOpen.apply(this, arguments); };"
    @"    proto.setRequestHeader = function(name, value){ try{ if(name && ('' + name).toLowerCase()==='content-type'){ this.__opActHasCT = true; } }catch(e){} return rawSet.apply(this, arguments); };"
    @"    proto.send = function(body){"
    @"      var self = this;"
    @"      if (!needIntercept(self.__opActM) || !body || self.__opActAsync===false){ return rawSend.apply(self, arguments); }"
    @"      var id = genId();"
    @"      try{ rawSet.call(self, HEADER, id); }catch(e){}"
    @"      var override = isForm(body) || !self.__opActHasCT;"
    @"      serialize(self.__opActU, body).then(function(r){ post(id, r.buffer, override ? r.contentType : ''); rawSend.call(self, body); }, function(){ rawSend.call(self, body); });"
    @"    };"
    @"  }"
    @"  if (window.fetch) {"
    @"    var rawFetch = window.fetch;"
    @"    window.fetch = function(input, init){"
    @"      init = init || {};"
    @"      var method = init.method || (input && input.method) || 'GET';"
    @"      var url = (typeof input === 'string') ? input : (input && input.url);"
    @"      var body = (init.body != null) ? init.body : (input && input.body);"
    @"      if (!needIntercept(method) || !body){ return rawFetch.apply(this, arguments); }"
    @"      var id = genId();"
    @"      var h = new Headers((init.headers) || (input && input.headers) || {});"
    @"      var override = isForm(body) || !h.has('content-type');"
    @"      return serialize(url, body).then(function(r){"
    @"        post(id, r.buffer, override ? r.contentType : '');"
    @"        h.set(HEADER, id);"
    @"        var ni = {}; for (var k in init){ ni[k]=init[k]; } ni.headers = h; ni.method = method; ni.body = body;"
    @"        return rawFetch.call(window, url, ni);"
    @"      }, function(){ return rawFetch.call(window, input, init); });"
    @"    };"
    @"  }"
    @"})();";
}

@implementation WKWebViewUploadBodyKeys {
    NSMutableDictionary<NSString *, NSData *> *_bodyMap;
    NSMutableDictionary<NSString *, NSString *> *_contentTypeMap;
    NSCondition *_condition;
}

+ (instancetype)sharedKeys
{
    static WKWebViewUploadBodyKeys *store = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        store = [[self alloc] init];
    });
    return store;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        _bodyMap = [NSMutableDictionary dictionary];
        _contentTypeMap = [NSMutableDictionary dictionary];
        _condition = [[NSCondition alloc] init];
    }
    return self;
}

+ (NSString *)messageHandlerName {
    return messageHandlerKeyName;
}

+ (NSString *)theField {
    return messageHeaderName;
}

+ (WKUserScript *)hookUserScript {
    return [[WKUserScript alloc] initWithSource:hookjavascript()
                                  injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                               forMainFrameOnly:NO];
}

#pragma mark - WKScriptMessageHandler

- (void)userContentController:(WKUserContentController *)userContentController
      didReceiveScriptMessage:(WKScriptMessage *)message {
    if (![message.name isEqualToString:messageHandlerKeyName]) {
        return;
    }
    NSDictionary *dict = message.body;
    if (![dict isKindOfClass:[NSDictionary class]]) {
        return;
    }
    NSString *uploadId = dict[@"id"];
    NSString *base64 = dict[@"body"];
    NSString *contentType = dict[@"contentType"];
    if (![uploadId isKindOfClass:[NSString class]] || uploadId.length == 0 ||
        ![base64 isKindOfClass:[NSString class]]) {
        return;
    }
    NSData *data = [[NSData alloc] initWithBase64EncodedString:base64 options:0] ?: [NSData data];

    [_condition lock];
    _bodyMap[uploadId] = data;
    if ([contentType isKindOfClass:[NSString class]] && contentType.length > 0) {
        _contentTypeMap[uploadId] = contentType;
    }
    [_condition broadcast];
    [_condition unlock];
}

#pragma mark - take

- (NSData *)takeBodyForId:(NSString *)uploadId
              contentType:(NSString * _Nullable * _Nullable)contentType
                  timeout:(NSTimeInterval)timeout {
    if (uploadId.length == 0) {
        return nil;
    }
    NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:timeout];
    [_condition lock];
    while (_bodyMap[uploadId] == nil) {
        // waitUntilDate 返回 NO 表示已超时
        if (![_condition waitUntilDate:deadline]) {
            break;
        }
    }
    NSData *data = _bodyMap[uploadId];
    NSString *ct = _contentTypeMap[uploadId];
    [_bodyMap removeObjectForKey:uploadId];
    [_contentTypeMap removeObjectForKey:uploadId];
    [_condition unlock];

    if (contentType) {
        *contentType = ct;
    }
    return data;
}

@end

三、使用过程

objectivec 复制代码
WKPreferences *preference = [[WKPreferences alloc]init];
preference.javaScriptEnabled = YES;
preference.javaScriptCanOpenWindowsAutomatically = YES;

WKWebViewConfiguration *config = [WKWebViewConfiguration new];
config.preferences = preference;
config.preferences.minimumFontSize = 10;
config.allowsInlineMediaPlayback = YES;

WKWebViewUrlSchemeHandler *scheme = [[WKWebViewUrlSchemeHandler alloc] init];
[config setURLSchemeHandler:scheme forURLScheme:@"http"];
[config setURLSchemeHandler:scheme forURLScheme:@"https"];

WKUserContentController *userContentController = WKUserContentController.new;
[userContentController addUserScript:[WKWebViewUploadBodyKeys hookUserScript]];
[userContentController addScriptMessageHandler:[WKWebViewUploadBodyKeys sharedKeys]
                                      name:[WKWebViewUploadBodyKeys messageHandlerName]];
config.userContentController = userContentController;

__weak typeof(self) weakSelf = self;
[scheme setHoldonSchemeBlock:^BOOL(id<WKURLSchemeTask>  _Nonnull urlSchemeTask) {
    NSURLRequest *request = urlSchemeTask.request;
    NSString *fileUrlStr = request.URL.absoluteString;
    if ([fileUrlStr containsString:@"open/ai/chat/stream"])
    {
    //自定义的请求过程
        weakSelf.sseRequest = [WKWebViewSchemeSSERequest sseRequest:request didReceiveRespone:^(NSURLResponse *response) {
            [urlSchemeTask didReceiveResponse:response];
        }
        receivedDataBlock:^(NSData *data) {
            @try {
                [urlSchemeTask didReceiveData:data];
                //NSLog(@"didReceiveData exception: null");
            } @catch (NSException *exception) {
                NSLog(@"didReceiveData exception: %@", exception);
            }
        } didFinisehdBlock:^(NSURLResponse *response) {
            @try {
                [urlSchemeTask didFinish];
            } @catch (NSException *exception) {
                NSLog(@"didFinisehdBlock exception: %@", exception);
            }
        } errorBlock:^(NSError *error) {
            @try {
                [urlSchemeTask didFailWithError:error];
            } @catch (NSException *exception) {
                NSLog(@"errorBlock exception: %@", exception);
            }
        }];
        return YES;
    }
    return NO;
}];
_webView = [[WKWebView alloc]initWithFrame:CGRectMake(0, 0, KScreenSize.width,  self.safeAreaHeight) configuration:config];
_webView.backgroundColor=[UIColor clearColor];
_webView.UIDelegate = self;
相关推荐
库奇噜啦呼14 小时前
【iOS】NSOperation与NSOperationQueue
macos·ios·cocoa
二流小码农15 小时前
鸿蒙开发:实现文本渐变效果
android·ios·harmonyos
WiresharkClub1 天前
抓包遇到私有协议全是乱码?抓包鹰自定义协议解码教程
ios
Lvan的前端笔记1 天前
ios:版本号和Build构建号
ios
smartpi_ai2 天前
离线语音识别的“数字变量“限制:为什么不能识别“20.5度“?
人工智能·语音识别·xcode
阿童木写作2 天前
Python批量翻译亚马逊商品图实战教程
开发语言·python·xcode
鹤卿1232 天前
「iOS」3GShare总结
macos·ios·objective-c·cocoa
Lvan的前端笔记2 天前
ios:区分 App Store / Ad Hoc / Development
ios
Lvan的前端笔记2 天前
ios:证书配置
ios