不是每个公司都会以数百美金一年的代价向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项目中编辑这个文件 。
查找maxThreads="150" scheme="https"secure="true"
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;
NSString*NSURLAuthenticationMethodHTTPBasic;
NSString*NSURLAuthenticationMethodHTTPDigest;
NSString*NSURLAuthenticationMethodHTMLForm;
NSString*NSURLAuthenticationMethodNegotiate;
NSString*NSURLAuthenticationMethodNTLM;
NSString*NSURLAuthenticationMethodClientCertificate;
NSString*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:方法。
其它方法请自行参考源代码:资源下载