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

xuewu1011 2024-09-14 15:03:01 阅读 79

需求,拦截WKWebview中的所有网络请求,并且对亲够Request的httpheader中添加字段token,等信息。实现技术,利用NSURLProtocol。首先实现一个继承自NSURLProtocol的自定义类:MYSchemeURLProtocol,完整的代码实现如下:

<code>//类文件

#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;

}

执行结果:

 所有请求都可以进行拦截,所有get请求结果拦截修改和加载表现正常post请求中的body体丢失, 这个问题暂时无法在所运用的场景中使用到,由于后期很多链接都有post请求转发,并且携带有body体,NSURLProtocol拦截之后body体丢失问题无法得到完整的解决,最后放弃了拦截修改。暂未找到需求逻辑最佳解决方案,待后续处理补充。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。