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. 暂未找到需求逻辑最佳解决方案,待后续处理补充。
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值