UIWebView для просмотра самозаверяющих веб-сайтов (нет частного API, не NSURLConnection) - это возможно?

Есть множество вопросов, которые задают это: Могу ли я получить UIWebView просмотреть самозаверяющий сайт HTTPS?

И ответы всегда включают в себя либо:

  1. Используйте частный вызов API для NSURLRequest: allowsAnyHTTPSCertificateForHost
  2. использование NSURLConnection вместо этого и делегат canAuthenticateAgainstProtectionSpace так далее

Для меня это не подойдет.
(1) - означает, что я не могу успешно отправить в магазин приложений.
(2) - использование NSURLConnection означает, что CSS, изображения и другие вещи, которые должны быть извлечены с сервера после получения начальной HTML-страницы, не загружаются.

Кто-нибудь знает, как использовать UIWebView для просмотра самозаверяющей https веб-страницы, пожалуйста, которая не включает в себя два метода выше?

Или - При использовании NSURLConnection фактически может использоваться для отображения веб-страницы с CSS, изображениями и всем остальным - это было бы здорово!

Ура,
Протяжение.

8 ответов

Решение

Наконец то я понял!

Что вы можете сделать, это:

Инициируйте ваш запрос, используя UIWebView как обычно. Затем в webView:shouldStartLoadWithRequest - мы отвечаем НЕТ, и вместо этого запускаем 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];
}

Я надеюсь, что это поможет другим с той же проблемой, что и я!

Ответ Stretch кажется хорошим решением, но он использует устаревшие API. Итак, я подумал, что это может быть достойно обновления кода.

Для этого примера кода я добавил подпрограммы в ViewController, который содержит мой UIWebView. Я сделал свой 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.

Это панацея!


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

Если вы хотите получить доступ к частному серверу с самозаверяющим сертификатом только для тестирования, вам не нужно писать код. Вы можете вручную выполнить импорт сертификата в масштабе всей системы.

Для этого вам необходимо скачать сертификат сервера с помощью мобильного сафари, которое затем запрашивает импорт.

Это может быть использовано при следующих обстоятельствах:

  • количество тестовых устройств мало
  • вы доверяете сертификату сервера

Если у вас нет доступа к сертификату сервера, вы можете использовать следующий метод для извлечения его с любого 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.

Это умный обходной путь. Однако, возможно, лучшим (хотя и более ресурсоемким) решением было бы использование NSURLProtocol, как продемонстрировано в примере кода Apple CustomHTTPProtocol. Из README:

"CustomHTTPProtocol показывает, как использовать подкласс NSURLProtocol для перехвата соединений NSURLConne, сделанных высокоуровневой подсистемой, которая иначе не представляет свои сетевые подключения. В этом конкретном случае он перехватывает запросы HTTPS, сделанные веб-представлением, и переопределяет оценку доверия сервера, что позволяет вам просматривать сайт, сертификат которого по умолчанию не является доверенным."

Ознакомьтесь с полным примером: https://developer.apple.com/library/ios/samplecode/CustomHTTPProtocol/Introduction/Intro.html

Это совместимый эквивалент 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!)
}

Вот рабочий код 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)
                }
}

Чтобы составить ответ @spirographer, я собрал что-то для сценария использования Swift 2.0 с NSURLSession, Тем не менее, это все еще не работает. Смотрите больше ниже.

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

Я вернусь к исходному HTML-ответу, поэтому страница отображает простой HTML-код, но к нему не применяются стили CSS (кажется, что запрос на получение CSS отклонен). Я вижу кучу этих ошибок:

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

Кажется, что любой запрос сделан с webView.loadRequest выполняется не в течение сеанса, поэтому соединение отклоняется. я должен Allow Arbitrary Loads установить в Info.plist, Что меня смущает, так это почему NSURLConnection будет работать (казалось бы, та же идея), но не NSURLSession,

Первым делом 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>
Другие вопросы по тегам