iOS: HTTPS 與自簽名證書
不是每個公司都會以數百美金一年的代價向CA購買SSL證書。在企業應用中,付費的SSL證書經常被自簽名證書所替代。當然,對於自簽名證書iOS是沒有能力驗證的。Safari遇到這種無法驗證的自簽名證書的唯一處理方法,就是將問題扔給使用者,讓使用者決定是否應該相信此類證書。它提供了兩個按鈕,一個“繼續”按鈕和一個“取消”按鈕。當你點選“取消”按鈕,則你將無法訪問所請求的資源。 當你點選“繼續”按鈕,則Safari會認為使用者授權它放棄對該伺服器的驗證,所產生的風險由使用者自己承擔。 當然,HTTPS傳輸仍然是加密的。
一、配置HTTPS伺服器
我們將使用Tomcat來配置HTTPS伺服器。關於Tomcat在mac下的安裝,請參考《 安裝Tomcat到Mac OSX 》一文。如果你安裝了Eclipse,則很可能Eclipse IDE中已經配置過Tomcat。帶開Eclipse的“偏好設定”,在Server->RuntimeEnvironments中可以看到已安裝好的Tomcat伺服器:
點選“Edit…”按鈕,你可以找到Tomcat安裝路徑:
開啟“終端”,進入Tomcat安裝目錄:
cd /Library/Tomcat/apache-tomcat-7.0.14
執行以下命令:
keytool -genkey -v -alias tomcat-keyalg RSA -storetype JKS -keystore tomcat.keystore -dname"CN=www.handtimes.com,OU=ipcc,O=雲電同方,L=昆明,ST=雲南,C=中國" -storepass 123456 -keypass 123456
這將在Tomcat安裝路徑下生成伺服器證書及金鑰庫(庫名:tomcat,檔名:tomcat.keystore),證書是自簽名的,證書機構採用域名。金鑰庫密碼和私鑰密碼都是123456。預設過期時間為3個月(90天)。
注意:-storetype JKS指定了金鑰庫的型別為java key store。這很重要,如果你指定為其他型別如PKCS12,則Tomcat會報"Invalidkeystore format"錯誤。
開啟tomcat目錄下的server.xml,你可以直接從Finder中開啟它,或者在Eclipse的Servers專案中編輯這個檔案 。
查詢 <Connector port="8443",將此段程式碼的註釋取消:<Connectorport="8443" protocol="HTTP/1.1" SSLEnabled="true"
maxThreads="150" scheme="https
clientAuth="false" sslProtocol="TLS" />
在其中加入兩個屬性:
keystoreFile="/Library/Tomcat/apache-tomcat-7.0.14/tomcat.keystore"
keystorePass="123456"
注意,本文中的keytool工具使用的是sun/oracle JDK的版本。如果你的機器上安裝的是GNU jvm,則keytool應該是GNU的版本,則上述server.xml程式碼中還應該加入以下屬性:
keystoreType="gkr"algorithm="JessieX509"
在Eclipse中編輯完server.xml,然後使用“Run As->Run Configurations…->Run”使修改生效,Eclipse會自動重啟Tomcat,但控制檯輸出如下內容時,表明https服務已經啟動:
資訊: StartingProtocolHandler ["http-bio-8443"]
2012-7-9 11:29:01org.apache.coyote.AbstractProtocolHandler start
此時,使用HTTPS埠8443隨便訪問一個頁面,例如: https://localhost:8443/AnyMail/table_css_test.html
此時safari會彈出一個視窗提示使用者,接收伺服器的證書:
只有點選“繼續”,使用者才可以訪問該頁面。
如果你使用FireFox,則需要將此頁面新增到例外。
二、iOS 訪問HTTPS
新建Single View Application。在ViewController.xib上拖入一個按鈕,一個UILabel和一個UIWebView,並連線到原始碼。
開啟ViewController.h,宣告如下成員:
NSURLRequest* _request;
NSURLConnection * connection;
NSString* filePath;
NSOutputStream * fileStream;
NSURL* url,*baseUrl;
NSStringEncoding enc;
NSURLAuthenticationChallenge *_challenge;
_request和connection物件不用多說,我們準備使用URLRequest和URLConnection來請求HTTPS。
程式中將向HTTPS伺服器請求一個html檔案,這個檔案會以臨時檔案的形式儲存到filePath的路徑。fileStream則是檔案輸出流。
url和baseUrl分別指定這個html頁面的url地址和base url地址。
enc是伺服器頁面編碼,本例中將使用GBK編碼。
由於HTTPS伺服器採用了自簽名的證書,iOS無法驗證此類證書, 所以客戶端會向用戶索要一個憑據(即Credential,使用者對此證書採取什麼樣的信任策略)。在Safari中,是通過前圖所示的那個“伺服器證書”視窗來進行的。而在我們的程式中,這個視窗會用一個AlertView替代,伺服器詢問時的相關內容會封裝在一個NSURLAuthenticationChallenge物件中(包括伺服器證書),我們以_challenge成員retain它。
使用NSURLConnection請求一個網頁資源很簡單,這個過程在按鈕的觸控事件中觸發:
- (IBAction)goAction:(id)sender {
_challenge=nil;
filePath = [[[AppDelegatesharedAppDelegate] pathForTemporaryFileWithPrefix:@"Get"]retain];
NSLog(@"filePath=%@",filePath);
fileStream = [[NSOutputStreamalloc]initToFileAtPath:filePathappend:NO];
assert(fileStream != nil);
[fileStreamopen];
_request = [NSURLRequestrequestWithURL:url];
assert(_request != nil);
connection = [NSURLConnectionconnectionWithRequest:_requestdelegate:self];
}
在方法中我先打開了NSOutputStream 物件,以便將網頁寫入到臨時檔案中。這裡本來想實現一種快取機制,不過由於時間原因,我沒有再深究下去,導致多次請求後在tmp資料夾中留下了一堆的臨時檔案。臨時檔案的檔名是以Get+UUID命名的,我把命名方法寫在了AppDelegate裡,希望你能找到並解決這個我遺留下來的問題:
重要的是[NSURLConnectionconnectionWithRequest:delegate:]方法的呼叫,我們獲取了一個NSURLConnection物件並將它的delegate設定為self。
self在實現NSULConnectionDelegate方法時,特別實現了connection:canAuthenticateAgainstProtectionSpace:方法,以及connection:didReceiveAuthenticationChallenge:方法:
- (BOOL)connection:(NSURLConnection *)conncanAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
NSLog(@"authenticatemethod:%@",protectionSpace.authenticationMethod);
return [protectionSpace.authenticationMethodisEqualToString:
NSURLAuthenticationMethodServerTrust];
}
- (void)connection:(NSURLConnection *)conndidReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
_challenge=[challenge retain];
UIAlertView* alertView = [[[UIAlertViewalloc] initWithTitle:@"伺服器證書"
message:@"這個網站有一個伺服器證書,點選“接受”,繼續訪問該網站,如果你不確定,請點選“取消”。"
delegate:self
cancelButtonTitle:@"接受"
otherButtonTitles:@"取消", nil] autorelease];
[alertView show];
}
canAuthenticateAgainstProtectionSpace:方法在連線到一些有安全限制的網站時呼叫,例如:伺服器信任、客戶端證書、HTTP表單驗證等。但URLConnection不知道也沒有強制程式設計師必需處理哪些安全問題,因此它把一個NSURLProtectionSpace物件作為引數傳遞,如果程式設計師想響應某一類安全問題,那麼在這個方法最後就返回YES。你要明白程式設計師可以處理哪些安全問題,你可以檢視NSURLProtectionSpace的authenticationMethod屬性。這是一個NSString屬性,可能取值包括以下常量:
NSString *NSURLAuthenticationMethodDefault;
NSURLAuthenticationMethodHTTPBasic;
NSURLAuthenticationMethodHTTPDigest;
NSURLAuthenticationMethodHTMLForm;
NSURLAuthenticationMethodNegotiate;
NSURLAuthenticationMethodNTLM;
NSURLAuthenticationMethodClientCertificate;
NSURLAuthenticationMethodServerTrust;
當然在這裡,我們只處理“伺服器信任”的安全問題。
didReceiveAuthenticationChallenge方法則緊接第一個方法之後呼叫。如果第一個方法中返回true,那麼URLConnection接下來就呼叫delegate的第二個方法(NO則跳過第二個方法)。
在這裡,我們彈出了一個UIAlertView,提示使用者進行處理。
接下來實現UIAlertView的delegate方法:
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
// Accept=0,Cancel=1;
if(buttonIndex==0){
NSURLCredential * credential;
NSURLProtectionSpace * protectionSpace;
SecTrustReftrust;
NSString * host;
SecCertificateRefserverCert;
assert(_challenge !=nil);
protectionSpace = [_challengeprotectionSpace];
assert(protectionSpace != nil);
trust = [protectionSpaceserverTrust];
assert(trust != NULL);
credential = [NSURLCredentialcredentialForTrust:trust];
assert(credential != nil);
host = [[_challengeprotectionSpace] host];
if (SecTrustGetCertificateCount(trust) > 0) {
serverCert = SecTrustGetCertificateAtIndex(trust, 0);
} else {
serverCert = NULL;
}
[[_challengesender] useCredential:credential forAuthenticationChallenge:_challenge];
}
}
這個方法中,如果使用者選擇“接受”,則我們從NSURLAuthenticationChallenge物件中獲取伺服器證書,並將該證書應用於URLConnection,接下來會繼續呼叫URLConnection的其他delegate方法。如果使用者選擇“取消”,則會導致伺服器返回一個錯誤,這會呼叫connection:didFailedWithError:方法。
其它方法請自行參考原始碼:資源下載