UIWebView для просмотра самозаверяющих веб-сайтов (нет частного API, не NSURLConnection) - это возможно?
Есть множество вопросов, которые задают это: Могу ли я получить UIWebView
просмотреть самозаверяющий сайт HTTPS?
И ответы всегда включают в себя либо:
- Используйте частный вызов API для
NSURLRequest
:allowsAnyHTTPSCertificateForHost
- использование
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>