WKWebView的请求拦截和修改实现。技术方法:NSURLProtocol

  • 需求,拦截WKWebview中的所有网络请求,并且对亲够Request的httpheader中添加字段token,等信息。

  • 实现技术,利用NSURLProtocol。

  • 首先实现一个继承自NSURLProtocol的自定义类:MYSchemeURLProtocol,完整的代码实现如下:

    //类文件

    #import <Foundation/Foundation.h>

    NS_ASSUME_NONNULL_BEGIN

    #define MYSchemeURLHeaderTokenAdd 0

    FOUNDATION_EXTERN NSString *const URLLoadingNotification;

    FOUNDATION_EXTERN NSString *const HttpProtocolKey;
    FOUNDATION_EXTERN NSString *const HttpsProtocolKey;

    @interface MYSchemeURLProtocol : NSURLProtocol

    • (void)registSchemeURLProtocol;
    • (void)unregistSchemeURLProtocol;
      @end

    @interface NSMutableURLRequest (MYHeaderAppend)

    • (BOOL)appendRemoteAccessHeaders;
      @end

    NS_ASSUME_NONNULL_END

    //================.m ===================
    #import "MYSchemeURLProtocol.h"
    #import "JSON.h"

    NSString *const URLLoadingNotification = @"com.zspace.urlLoadingNotification";
    static NSString *kURLProtocolHandledKey = @"URLProtocolHandledKey";
    NSString *const HttpProtocolKey = @"http";
    NSString *const HttpsProtocolKey = @"https";

    @interface MYSchemeURLProtocol()<NSURLSessionDelegate>

    @property (atomic,strong,readwrite) NSURLSessionDataTask *task;
    @property (nonatomic,strong) NSURLSession *session;
    @property (nonatomic, strong) NSOperationQueue *queue;

    @end

    @implementation MYSchemeURLProtocol

    • (void)registSchemeURLProtocol {
      // 防止苹果静态检查 将 WKBrowsingContextController 拆分,然后再拼凑起来
      NSArray *privateStrArr = @[@"Controller", @"Context", @"Browsing", @"K", @"W"];
      NSString *className = [[[privateStrArr reverseObjectEnumerator] allObjects] componentsJoinedByString:@""];
      Class cls = NSClassFromString(className);
      SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");

      if (cls && sel) {
      if ([(id)cls respondsToSelector:sel]) {
      #pragma clang diagnostic push
      #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
      // 注册自定义协议
      // [(id)cls performSelector:sel withObject:@"CustomProtocol"];
      // 注册http协议
      [(id)cls performSelector:sel withObject:HttpProtocolKey];
      // 注册https协议
      // [(id)cls performSelector:sel withObject:HttpsProtocolKey];
      #pragma clang diagnostic pop
      }
      }
      // SechemaURLProtocol 自定义类 继承于 NSURLProtocol
      [NSURLProtocol registerClass:[MYSchemeURLProtocol class]];
      }

    • (void)unregistSchemeURLProtocol {
      [NSURLProtocol unregisterClass:[MYSchemeURLProtocol class]];
      }

    • (BOOL)canInitWithRequest:(NSURLRequest )request
      {
      NSString scheme = [[request URL] scheme];
      // 判断是否需要进入自定义加载器
      if ([scheme caseInsensitiveCompare:HttpProtocolKey] == NSOrderedSame
      /
      || [scheme caseInsensitiveCompare:HttpsProtocolKey] == NSOrderedSame
      /)
      {
      //看看是否已经处理过了,防止无限循环
      if ([NSURLProtocol propertyForKey:kURLProtocolHandledKey inRequest:request]) {
      NSLogInfo(@"hookRequest-canInitWithRequest NO %@",request.URL);
      return NO;
      }
      }
      NSLogInfo(@"hookRequest-canInitWithRequest YES %@",request.URL);
      return YES;
      }

    • (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request {

      NSMutableURLRequest *mutableReqeust = [request mutableCopy];
      // 执行自定义操作,例如添加统一的请求头等
      return mutableReqeust;
      }

    // 判重

    • (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
      {
      return [super requestIsCacheEquivalent:a toRequest:b];
      }
    • (void)startLoading
      {
      NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
      // 标示改request已经处理过了,防止无限循环
      [NSURLProtocol setProperty:@YES forKey:kURLProtocolHandledKey inRequest:mutableReqeust];

    #if MYSchemeURLHeaderTokenAdd
    // 处理请求 body
    if (self.request.HTTPBody && [self.request valueForHTTPHeaderField:@"Content-Type"]) {
    [mutableReqeust setHTTPBody:self.request.HTTPBody];
    [mutableReqeust setValue:[self.request valueForHTTPHeaderField:@"Content-Type"] forHTTPHeaderField:@"Content-Type"];
    }
    if([mutableReqeust.URL.absoluteString containsString:@"Users/authenticatebyname"]){
    NSLogInfo(@"跳过");
    }
    NSString *body = [NTYJSON parse:mutableReqeust.HTTPBody];
    NSLogInfo(@"self.HTTPBody:%@",body);
    NSLogInfo(@"self.HTTPBodyStream:%@",mutableReqeust.HTTPBodyStream);

      [mutableReqeust appendRemoteAccessHeaders];
    

    #endif

      // 通知更新URL
      [[NSNotificationCenter defaultCenter] postNotificationName:URLLoadingNotification object:mutableReqeust.URL.absoluteString];
      
      NSURLSessionConfiguration *configure = [NSURLSessionConfiguration defaultSessionConfiguration];
      self.session  = [NSURLSession sessionWithConfiguration:configure delegate:self delegateQueue:self.queue];
      self.task = [self.session dataTaskWithRequest:mutableReqeust];
      [self.task resume];
    

    }

    • (void)stopLoading
      {
      [self.session invalidateAndCancel];
      self.session = nil;
      [[NSNotificationCenter defaultCenter] postNotificationName:URLLoadingNotification object:@""];

    }

    #pragma mark - Getter

    • (NSOperationQueue *)queue
      {
      if (!_queue) {
      _queue = [[NSOperationQueue alloc] init];
      }
      return _queue;
      }
      @end

    @implementation MYSchemeURLProtocol(NSURLSessionDelegate)

    • (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
      {
      if (error != nil) {
      NSLogInfo(@"didComplete url:%@",task.originalRequest.URL.absoluteString);
      // 检查是否为 WKWebView 的网络请求
      [self.client URLProtocol:self didFailWithError:error];
      }else
      {
      [self.client URLProtocolDidFinishLoading:self];
      }
      }

    • (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
      didReceiveResponse:(NSURLResponse *)response
      completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
      {
      [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];

      completionHandler(NSURLSessionResponseAllow);
      }

    • (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
      {
      [self.client URLProtocol:self didLoadData:data];
      }

    • (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse * _Nullable))completionHandler
      {
      completionHandler(proposedResponse);
      }

    //TODO: 重定向

    • (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)newRequest completionHandler:(void (^)(NSURLRequest ))completionHandler
      {
      NSLogInfo(@"hookRequest-willPerformHTTP:%@",newRequest.URL);
      NSMutableURLRequest
      redirectRequest;
      redirectRequest = [newRequest mutableCopy];
      [[self class] removePropertyForKey:kURLProtocolHandledKey inRequest:redirectRequest];

    #if MYSchemeURLHeaderTokenAdd
    [redirectRequest appendRemoteAccessHeaders];
    #endif

      [[self client] URLProtocol:self wasRedirectedToRequest:redirectRequest redirectResponse:response];
      
      [self.task cancel];
      [[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]];
    

    }

    @end

    @implementation NSMutableURLRequest (MYHeaderAppend)
    /*
    还需要增加两个header:
    Nas-Neigh-Ip
    Nas-Neigh-Port
    分别是目标ip和目标端口,请注意port需要用字符串类型
    */

    • (BOOL)appendRemoteAccessHeaders {
      //MARK:MY_token 添加进请求头
      // 添加自定义的 HTTP 头
      NSString *tokenValue = [MYUserService shared].currentUser.token;
      NSString *holdToken = [self.allHTTPHeaderFields valueForKey:@"z-token"];
      if([self.URL.absoluteString containsString:@"Users/authenticatebyname"]){
      NSLogInfo(@"跳过");
      }
      if(isEmpty(holdToken)){
      NSString *body = [NTYJSON parse:self.HTTPBody];
      NSLogInfo(@"self.HTTPBody:%@ url:%@",body,self.URL.absoluteString);
      NSLogInfo(@"self.HTTPBodyStream:%@",self.HTTPBodyStream);
      NSURLComponents *remoteURLComs = [NSURLComponents componentsWithString:[MYUserService shared].latestRemoteAccessURL];
      NSString *portStr = STRING(@"%@",[remoteURLComs port]);
      NSString *hostStr = remoteURLComs.host;
      [self setValue:tokenValue forHTTPHeaderField:@"z-token"];
      [self setValue:hostStr forHTTPHeaderField:@"Nas-Neigh-Ip"];
      [self setValue:portStr forHTTPHeaderField:@"Nas-Neigh-Port"];
      NSLogInfo(@"remoteAccessURL&appendheaders::%@\n%@",self.URL,self.allHTTPHeaderFields);
      return YES;
      }
      return NO;
      }

    @end

  • 接下来,在需要进行拦截的WebViewController中,注册MYSchemeURLProtocol。核心代码示例如下:

    #import "MYWebviewController.h"
    #import "MYSchemeURLProtocol.h"

    @interface JKJWebviewController ()<WKNavigationDelegate,WKScriptMessageHandler,WKUIDelegate>

    @property (nonatomic, strong) WKWebView *webView;
    @property (nonatomic, strong) NSURLRequest *request;
    @end

    @implementation JKJWebviewController

    • (void)dealloc {
      [[NSNotificationCenter defaultCenter] removeObserver:self];
      #if JKJSchemeURLHeaderTokenAdd
      if(self.refModule.refModuleRequest == JKJRefModuleRequestRemoteAccessWebLoad){
      [JKJSchemeURLProtocol unregistSchemeURLProtocol];
      }
      #endif
      }

    • (void)viewDidLoad {
      [super viewDidLoad];
      //注册代码
      #if JKJSchemeURLHeaderTokenAdd
      if(self.refModule.refModuleRequest == JKJRefModuleRequestRemoteAccessWebLoad){
      [JKJSchemeURLProtocol registSchemeURLProtocol];
      }
      #endif

      // Do any additional setup after loading the view.
      self.title = self.webTitle;
      self.edgesForExtendedLayout = UIRectEdgeNone;

      WKWebViewConfiguration *configuration = [self configuration];
      WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
      webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
      webView.navigationDelegate = self;
      webView.UIDelegate = self;
      [webView.scrollView setShowsVerticalScrollIndicator:NO];
      [self.view addSubview:webView];

      if (self.needObserveTitle) {
      [webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:NULL];
      }

      self.webView = webView;

      NSMutableURLRequest *req = [[NSMutableURLRequest alloc] initWithURL:[NSURL fileURLWithPath:urlString]];

      [webView loadRequest:req];
      self.request = req;
      }

  • 执行结果:

  1. 所有请求都可以进行拦截,
  2. 所有get请求结果拦截修改和加载表现正常
  3. post请求中的body体丢失, 这个问题暂时无法在所运用的场景中使用到,由于后期很多链接都有post请求转发,并且携带有body体,NSURLProtocol拦截之后body体丢失问题无法得到完整的解决,最后放弃了拦截修改。
  4. 暂未找到需求逻辑最佳解决方案,待后续处理补充。
相关推荐
missmisslulu2 小时前
电容笔值得买吗?2024精选盘点推荐五大惊艳平替电容笔!
学习·ios·电脑·平板
GEEKVIP3 小时前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
GEEKVIP3 小时前
如何在 Windows 10 上恢复未保存/删除的 Word 文档
macos·ios·智能手机·电脑·word·笔记本电脑·iphone
奇客软件4 小时前
iPhone使用技巧:如何恢复变砖的 iPhone 或 iPad
数码相机·macos·ios·电脑·笔记本电脑·iphone·ipad
奇客软件1 天前
如何从相机的记忆棒(存储卡)中恢复丢失照片
深度学习·数码相机·ios·智能手机·电脑·笔记本电脑·iphone
GEEKVIP1 天前
如何修复变砖的手机并恢复丢失的数据
macos·ios·智能手机·word·手机·笔记本电脑·iphone
一丝晨光1 天前
继承、Lambda、Objective-C和Swift
开发语言·macos·ios·objective-c·swift·继承·lambda
GEEKVIP2 天前
iPhone/iPad技巧:如何解锁锁定的 iPhone 或 iPad
windows·macos·ios·智能手机·笔记本电脑·iphone·ipad
KWMax2 天前
RxSwift系列(二)操作符
ios·swift·rxswift
Mamong2 天前
Swift并发笔记
开发语言·ios·swift