51

有很多问题会问这个问题:我可以UIWebView查看自签名的 HTTPS 网站吗?

答案总是涉及:

  1. 使用私有 api 调用NSURLRequestallowsAnyHTTPSCertificateForHost
  2. 改为使用NSURLConnection委托canAuthenticateAgainstProtectionSpace

对我来说,这些都行不通。
(1) - 表示我无法成功提交到应用商店。
(2) - 使用 NSURLConnection 意味着在收到初始 HTML 页面后必须从服务器获取的 CSS、图像和其他内容不加载。

请问有谁知道如何使用 UIWebView 查看自签名的 https 网页,不涉及上述两种方法?

或者——如果 usingNSURLConnection实际上可以用来渲染一个包含 CSS、图像和其他所有内容的网页——那就太好了!

干杯,
舒展。

4

9 回答 9

76

最后我得到了它!

你可以做的是:

UIWebView使用正常发起您的请求。然后 - 在webView:shouldStartLoadWithRequest- 我们回复NO,而是用相同的请求启动一个 NSURLConnection 。

使用NSURLConnection,您可以与自签名服务器通信,因为我们有能力通过额外的委托方法来控制身份验证,而UIWebView. 因此,connection:didReceiveAuthenticationChallenge我们可以使用自签名服务器进行身份验证。

然后,在 中connection:didReceiveData,我们取消NSURLConnection请求,并使用UIWebView- 再次启动相同的请求,这将起作用,因为我们已经通过了服务器身份验证:)

下面是相关的代码片段。

注意:您将看到的实例变量属于以下类型:
UIWebView *_web
NSURLConnection *_urlConnection
NSURLRequest *_request

(我使用实例 var,_request因为在我的情况下,它是一个包含大量登录详细信息的 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];
}

我希望这可以帮助其他人解决我遇到的同样问题!

于 2012-07-26T07:06:54.770 回答
65

Stretch 的答案似乎是一个很好的解决方法,但它使用了已弃用的 API。所以,我认为它可能值得升级代码。

对于此代码示例,我将例程添加到包含我的 UIWebView 的 ViewController。我将 UIViewController 设为 UIWebViewDelegate 和 NSURLConnectionDataDelegate。然后我添加了 2 个数据成员:_Authenticated 和 _FailedRequest。这样,代码如下所示:

-(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];
}

当我加载视图并且不重置它时,我将 _Authenticated 设置为 NO。这似乎允许 UIWebView 向同一个站点发出多个请求。我没有尝试切换站点并尝试返回。这可能会导致需要重置_Authenticated。此外,如果您要切换站点,您应该为 _Authenticated 保留一个字典(每个主机一个条目)而不是 BOOL。

于 2013-02-25T18:59:13.797 回答
17

这就是灵丹妙药!


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.
}
于 2014-02-12T14:28:00.703 回答
7

如果您想访问具有自签名证书的私有服务器只是为了进行测试,您不必编写代码。您可以手动执行系统范围的证书导入。

为此,您需要使用移动 safari 下载服务器证书,然后提示导入。

这将在以下情况下可用:

  • 测试设备数量少
  • 您信任服务器的证书

如果您无权访问服务器证书,则可以使用以下方法从任何 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

请注意,根据 OpenSSL 版本,文件中的证书可能会加倍,因此最好使用文本编辑器查看它。将文件放在网络上的某个位置或使用

python -m SimpleHTTPServer 8000

在 http://$your_device_ip:8000/server.pem 从您的移动 Safari 访问它的快捷方式。

于 2014-10-18T10:14:48.420 回答
4

这是一个聪明的解决方法。然而,一个可能更好(虽然代码更密集)的解决方案是使用 NSURLProtocol,如 Apple 的 CustomHTTPProtocol 示例代码中所示。从自述文件:

“CustomHTTPProtocol 展示了如何使用 NSURLProtocol 子类来拦截由不会暴露其网络连接的高级子系统发出的 NSURLConnections。在这种特定情况下,它会拦截 Web 视图发出的 HTTPS 请求并覆盖服务器信任评估,允许您浏览其证书默认不受信任的站点。”

查看完整示例: https ://developer.apple.com/library/ios/samplecode/CustomHTTPProtocol/Introduction/Intro.html

于 2013-12-02T19:07:24.223 回答
3

这是一个适用于我的 swift 2.0 兼容等效项。我还没有将此代码转换为使用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!)
}
于 2015-10-08T01:18:32.750 回答
2

这里是 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)
                }
}
于 2016-01-07T07:27:46.667 回答
1

To build off of @spirographer's answer, I put something together for a Swift 2.0 use case with 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). 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. I do have Allow Arbitrary Loads set in Info.plist. What confuses me is why NSURLConnection would work (seemingly the same idea), but not NSURLSession.

于 2016-05-31T16:36:10.670 回答
0

第一件事UIWebView已弃用

改为使用WKWebView(可从 iOS8 获得)

webView.navigationDelegate = self

实施

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))
    }

}

并将其添加到您要允许的域的 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>
于 2019-03-26T16:11:30.173 回答