简体   繁体   English

UIWebView可以查看自签名网站(没有私有api,没有NSURLConnection)-可以吗?

[英]UIWebView to view self signed websites (No private api, not NSURLConnection) - is it possible?

There's a load of questions which ask this: Can I get UIWebView to view a self signed HTTPS website? 有很多问题问这个问题:我可以获取UIWebView来查看自签名的HTTPS网站吗?

And the answers always involve either: 答案总是涉及以下两个方面:

  1. Use the private api call for NSURLRequest : allowsAnyHTTPSCertificateForHost NSURLRequest使用私有api调用: allowsAnyHTTPSCertificateForHost
  2. Use NSURLConnection instead and the delegate canAuthenticateAgainstProtectionSpace etc 使用NSURLConnection代替,并且委托可以canAuthenticateAgainstProtectionSpace

For me, these won't do. 对我来说,这些不会。
(1) - means I can't submit to the app store successfully. (1)-表示我无法成功提交到应用商店。
(2) - using NSURLConnection means the CSS, images and other things that have to be fetched from the server after receiving the initial HTML page do not load. (2)-使用NSURLConnection意味着在加载初始HTML页面后必须从服务器获取的CSS,图像和其他内容不会加载。

Does anyone know how to use UIWebView to view a self-signed https webpage please, which does not involve the two methods above? 有谁知道如何使用UIWebView来查看自签名的https网页,而这不涉及上述两种方法?

Or - If using NSURLConnection can in fact be used to render a webpage complete with CSS, images and everything else - that would be great! 或者-如果实际上可以使用NSURLConnection来渲染包含CSS,图像和其他所有内容的网页,那就太好了!

Cheers, 干杯,
Stretch. 伸展。

Finally I got it! 最后我明白了!

What you can do is this: 您可以执行以下操作:

Initiate your request using UIWebView as normal. 照常使用UIWebView发起您的请求。 Then - in webView:shouldStartLoadWithRequest - we reply NO , and instead start an NSURLConnection with the same request. 然后-在webView:shouldStartLoadWithRequest -我们回答NO ,而是使用相同的请求启动NSURLConnection。

Using NSURLConnection , you can communicate with a self-signed server, as we have the ability to control the authentication through the extra delegate methods which are not available to a UIWebView . 使用NSURLConnection ,您可以与自签名服务器通信,因为我们能够通过UIWebView不可用的额外委托方法来控制身份验证。 So using connection:didReceiveAuthenticationChallenge we can authenticate against the self signed server. 因此,使用connection:didReceiveAuthenticationChallenge我们可以针对自签名服务器进行身份验证。

Then, in connection:didReceiveData , we cancel the NSURLConnection request, and start the same request again using UIWebView - which will work now, because we've already got through the server authentication :) 然后,在connection:didReceiveData ,我们取消NSURLConnection请求,然后使用UIWebView再次启动相同的请求-现在可以使用,因为我们已经通过服务器身份验证了:)

Here are the relevant code snippets below. 以下是相关的代码段。

Note: Instance variables you will see are of the following type: 注意:您将看到的实例变量具有以下类型:
UIWebView *_web
NSURLConnection *_urlConnection
NSURLRequest *_request

(I use an instance var for _request as in my case it's a POST with lots of login details, but you could change to use the request passed in as arguments to the methods if you needed.) (我对_request使用实例var,因为在我的情况下,它是具有大量登录详细信息的POST,但是如果需要,您可以更改为使用传入的请求作为方法的参数。)

#pragma mark - Webview delegate

// Note: This method is particularly important. As the server is using a self signed certificate,
// we cannot use just UIWebView - as it doesn't allow for using self-certs. Instead, we stop the
// request in this method below, create an NSURLConnection (which can allow self-certs via the delegate methods
// which UIWebView does not have), authenticate using NSURLConnection, then use another UIWebView to complete
// the loading and viewing of the page. See connection:didReceiveAuthenticationChallenge to see how this works.
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
{
    NSLog(@"Did start loading: %@ auth:%d", [[request URL] absoluteString], _authenticated);

    if (!_authenticated) {
        _authenticated = NO;

        _urlConnection = [[NSURLConnection alloc] initWithRequest:_request delegate:self];

        [_urlConnection start];

        return NO;
    }

    return YES;
}


#pragma mark - NURLConnection delegate

- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
{
    NSLog(@"WebController Got auth challange via NSURLConnection");

    if ([challenge previousFailureCount] == 0)
    {
        _authenticated = YES;

        NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];

        [challenge.sender useCredential:credential forAuthenticationChallenge:challenge];

    } else
    {
        [[challenge sender] cancelAuthenticationChallenge:challenge];
    }
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
{
    NSLog(@"WebController received response via NSURLConnection");

    // remake a webview call now that authentication has passed ok.
    _authenticated = YES;
    [_web loadRequest:_request];

    // Cancel the URL connection otherwise we double up (webview + url connection, same url = no good!)
    [_urlConnection cancel];
}

// We use this method is to accept an untrusted site which unfortunately we need to do, as our PVM servers are self signed.
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
    return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}

I hope this helps others with the same issue I was having! 我希望这可以帮助其他人解决我遇到的同样的问题!

Stretch's answer appears to be a great workaround, but it uses deprecated APIs. Stretch的答案似乎是一个不错的解决方法,但它使用了已弃用的API。 So, I thought it might be worthy of an upgrade to the code. 因此,我认为可能值得对代码进行升级。

For this code sample, I added the routines to the ViewController which contains my UIWebView. 对于此代码示例,我将例程添加到包含UIWebView的ViewController中。 I made my UIViewController a UIWebViewDelegate and a NSURLConnectionDataDelegate. 我将UIViewController设置为UIWebViewDelegate和NSURLConnectionDataDelegate。 Then I added 2 data members: _Authenticated and _FailedRequest. 然后,我添加了2个数据成员:_Authenticated和_FailedRequest。 With that, the code looks like this: 这样,代码如下所示:

-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    BOOL result = _Authenticated;
    if (!_Authenticated) {
        _FailedRequest = request;
        [[NSURLConnection alloc] initWithRequest:request delegate:self];
    }
    return result;
}

-(void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        NSURL* baseURL = [_FailedRequest URL];
        if ([challenge.protectionSpace.host isEqualToString:baseURL.host]) {
            NSLog(@"trusting connection to host %@", challenge.protectionSpace.host);
            [challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
        } else
            NSLog(@"Not trusting connection to host %@", challenge.protectionSpace.host);
    }
    [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}

-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)pResponse {
    _Authenticated = YES;
    [connection cancel];
    [_WebView loadRequest:_FailedRequest];
}

I set _Authenticated to NO when I load the view and don't reset it. 加载视图且不重置时,我将_Authenticated设置为NO。 This seems to allow the UIWebView to make multiple requests to the same site. 这似乎允许UIWebView向同一站点发出多个请求。 I did not try switching sites and trying to come back. 我没有尝试切换站点并尝试返回。 That may cause the need for resetting _Authenticated. 这可能会导致需要重置_Authenticated。 Also, if you are switching sites, you should keep a dictionary (one entry for each host) for _Authenticated instead of a BOOL. 另外,如果要切换站点,则应保留_Authenticated的字典(每个主机一个条目),而不是BOOL。

This is the Panacea! 这是灵丹妙药!


BOOL _Authenticated;
NSURLRequest *_FailedRequest;

#pragma UIWebViewDelegate

-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request   navigationType:(UIWebViewNavigationType)navigationType {
    BOOL result = _Authenticated;
    if (!_Authenticated) {
        _FailedRequest = request;
        NSURLConnection *urlConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
        [urlConnection start];
    }
    return result;
}

#pragma NSURLConnectionDelegate

-(void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        NSURL* baseURL = [NSURL URLWithString:@"your url"];
        if ([challenge.protectionSpace.host isEqualToString:baseURL.host]) {
            NSLog(@"trusting connection to host %@", challenge.protectionSpace.host);
            [challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
        } else
            NSLog(@"Not trusting connection to host %@", challenge.protectionSpace.host);
    }
    [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}

-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)pResponse {
_Authenticated = YES;
    [connection cancel];
    [self.webView loadRequest:_FailedRequest];
}

- (void)viewDidLoad{
   [super viewDidLoad];

    NSURL *url = [NSURL URLWithString:@"your url"];
    NSURLRequest *requestURL = [NSURLRequest requestWithURL:url];
    [self.webView loadRequest:requestURL];

// Do any additional setup after loading the view.
}

If you want to access a private server with a self-signed certificate just for testing you don't have to write code. 如果您想使用自签名证书访问专用服务器以进行测试,则无需编写代码。 You can manually do a system-wide import of the certificate. 您可以手动在系统范围内导入证书。

To do this, you need to download the server certificate with mobile safari, which then prompts for an import. 为此,您需要使用移动浏览器下载服务器证书,然后提示输入。

This would be usable under the following circumstances: 在以下情况下可以使用:

  • the number of test devices is small 测试设备数量少
  • you're trusting the certificate of the server 您信任服务器的证书

If you don't have access to the server certificate, you can fallback to the following method for extracting it from any HTTPS-server (at least on Linux/Mac, windows guys will have to download an OpenSSL binary somewhere): 如果您无权访问服务器证书,则可以使用以下方法从任何HTTPS服务器提取证书(至少在Linux / Mac上,Windows人员必须在某个地方下载OpenSSL二进制文件):

echo "" | openssl s_client -connect $server:$port -prexit 2>/dev/null | sed -n -e '/BEGIN\ CERTIFICATE/,/END\ CERTIFICATE/ p' >server.pem

Note, that depending on the OpenSSL version, the certificate may be doubled in the file, so best have a look at it with a text editor. 请注意,根据OpenSSL版本的不同,证书可能会在文件中加倍,因此最好使用文本编辑器进行查看。 Put the file somewhere on the network or use the 将文件放在网络上的某个位置或使用

python -m SimpleHTTPServer 8000 python -m SimpleHTTPServer 8000

shortcut to access it from your mobile safari at http://$your_device_ip:8000/server.pem. 可以从移动浏览器访问它的快捷方式,网址为http:// $ your_device_ip:8000 / server.pem。

This is a clever workaround. 这是一个聪明的解决方法。 However, a possibly better (although more code intensive) solution would be to use an NSURLProtocol as demonstrated in Apple's CustomHTTPProtocol sample code. 但是,一个更好的解决方案(尽管需要更多代码)将使用NSURLProtocol,如Apple的CustomHTTPProtocol示例代码所示。 From the README: 从自述文件:

"CustomHTTPProtocol shows how to use an NSURLProtocol subclass to intercept the NSURLConnections made by a high-level subsystem that does not otherwise expose its network connections. In this specific case, it intercepts the HTTPS requests made by a web view and overrides server trust evaluation, allowing you to browse a site whose certificate is not trusted by default." “ CustomHTTPProtocol显示了如何使用NSURLProtocol子类来拦截由高级子系统建立的NSURLConnections,否则该子系统不会公开其网络连接。在这种特定情况下,它将拦截由Web视图发出的HTTPS请求并覆盖服务器信任评估,允许您浏览默认情况下不受信任的证书的网站。”

Checkout the full example: https://developer.apple.com/library/ios/samplecode/CustomHTTPProtocol/Introduction/Intro.html 检出完整的示例: https : //developer.apple.com/library/ios/samplecode/CustomHTTPProtocol/Introduction/Intro.html

This is a swift 2.0 compatible equivalent that works for me. 这是与我兼容的2.0兼容版本。 I have not converted this code to use NSURLSession instead of NSURLConnection , and suspect that it would add a lot of complexity to get it right. 我没有将此代码转换为使用NSURLSession而不是NSURLConnection ,并且怀疑这样做会增加很多复杂性。

var authRequest : NSURLRequest? = nil
var authenticated = false
var trustedDomains = [:] // set up as necessary

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
    if !authenticated {
        authRequest = request
        let urlConnection: NSURLConnection = NSURLConnection(request: request, delegate: self)!
        urlConnection.start()
        return false
    }
    else if isWebContent(request.URL!) { // write your method for this
        return true
    }
    return processData(request) // write your method for this
}

func connection(connection: NSURLConnection, willSendRequestForAuthenticationChallenge challenge: NSURLAuthenticationChallenge) {
    if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
        let challengeHost = challenge.protectionSpace.host
        if let _ = trustedDomains[challengeHost] {
            challenge.sender!.useCredential(NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!), forAuthenticationChallenge: challenge)
        }
    }
    challenge.sender!.continueWithoutCredentialForAuthenticationChallenge(challenge)
}

func connection(connection: NSURLConnection, didReceiveResponse response: NSURLResponse) {
    authenticated = true
    connection.cancel()
    webview!.loadRequest(authRequest!)
}

Here the working code of swift 2.0 这是swift 2.0的工作代码

var authRequest : NSURLRequest? = nil
var authenticated = false


func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
                if !authenticated {
                    authRequest = request
                    let urlConnection: NSURLConnection = NSURLConnection(request: request, delegate: self)!
                    urlConnection.start()
                    return false
                }
                return true
}

func connection(connection: NSURLConnection, didReceiveResponse response: NSURLResponse) {
                authenticated = true
                connection.cancel()
                webView!.loadRequest(authRequest!)
}

func connection(connection: NSURLConnection, willSendRequestForAuthenticationChallenge challenge: NSURLAuthenticationChallenge) {

                let host = "www.example.com"

                if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust &&
                    challenge.protectionSpace.host == host {
                    let credential = NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!)
                    challenge.sender!.useCredential(credential, forAuthenticationChallenge: challenge)
                } else {
                    challenge.sender!.performDefaultHandlingForAuthenticationChallenge!(challenge)
                }
}

To build off of @spirographer's answer , I put something together for a Swift 2.0 use case with NSURLSession . 打造关@ spirographer的答案 ,我把东西在一起的雨燕2.0用例NSURLSession However, this is still NOT working. 但是,这仍然无法正常工作。 See more below. 请参阅下文。

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
    let result = _Authenticated
    if !result {
        let sessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration()
        let session = NSURLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: NSOperationQueue.mainQueue())
        let task = session.dataTaskWithRequest(request) {
            (data, response, error) -> Void in
            if error == nil {
                if (!self._Authenticated) {
                    self._Authenticated = true;
                    let pageData = NSString(data: data!, encoding: NSUTF8StringEncoding)
                    self.webView.loadHTMLString(pageData as! String, baseURL: request.URL!)

                } else {
                    self.webView.loadRequest(request)
                }
            }
        }
        task.resume()
        return false
    }
    return result
}

func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
    completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!))
}

I will get back the initial HTML response, so the page renders the plain HTML, but there is no CSS styles applied to it (seems like the request to get CSS is denied). 我将返回初始的HTML响应,因此该页面呈现纯HTML,但是没有应用CSS样式(似乎获取CSS的请求被拒绝)。 I see a bunch of these errors: 我看到了很多这样的错误:

NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813)

It seems like any request made with webView.loadRequest is done not within the session, which is why the connection is rejected. 似乎用webView.loadRequest发出的任何请求都没有在会话中完成,这就是为什么连接被拒绝的原因。 I do have Allow Arbitrary Loads set in Info.plist . 我确实在Info.plist设置了Allow Arbitrary Loads What confuses me is why NSURLConnection would work (seemingly the same idea), but not NSURLSession . 令我困惑的是为什么NSURLConnection可以工作(看似相同的想法),而不是NSURLSession

First thing UIWebView is deprecated 不推荐使用UIWebView

use WKWebView instead (available from iOS8) 改用WKWebView (可从iOS8获得)

set webView.navigationDelegate = self 设置webView.navigationDelegate = self

implement 实行

extension ViewController: WKNavigationDelegate {

func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    let trust = challenge.protectionSpace.serverTrust!
    let exceptions = SecTrustCopyExceptions(trust)
    SecTrustSetExceptions(trust, exceptions)
        completionHandler(.useCredential, URLCredential(trust: trust))
    }

}

And add this in plist with domains you want to allow 并在plist中添加您要允许的域

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>localhost</key>
        <dict>
            <key>NSTemporaryExceptionAllowsInsecureHTTPSLoads</key>
            <false/>
            <key>NSIncludesSubdomains</key>
            <true/>
            <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSTemporaryExceptionMinimumTLSVersion</key>
            <string>1.0</string>
            <key>NSTemporaryExceptionRequiresForwardSecrecy</key>
            <false/>
        </dict>
    </dict>
</dict>

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM