服務端配置
nginx關鍵配置如下:
listen 443;server_name localhost;ssl on;ssl_certificate /usr/local/opt/nginx/certificates/server.cer;ssl_certificate_key /usr/local/opt/nginx/certificates/server.key.pem;ssl_client_certificate /usr/local/opt/nginx/certificates/ca.cer;ssl_verify_client on;
ssl開啟https
ssl_certificate是服務端認證的路徑,ssl_certificate_key是服務端私密金鑰的路徑
ssl_verify_client是配置雙向認證(client certificate)
ssl_client_certificate是簽發用戶端認證的根憑證
為什麼是根憑證,因為可以簽發很多用戶端認證,只要是由該根憑證簽發的,服務端都視為認證通過
配置完成以後,一般需要把80連接埠的http請求跳轉到443連接埠,否則使用者可以通過80連接埠以http方式訪問,就失去了安全保護的意義
用戶端代碼
在網上找了很多ios client certificate的文章,要麼是代碼不全,要麼是很老的文章,還是用的NSURLConnection,delegate method都deprecated了,最後找了一個程式碼片段,改了一下
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil]; NSURL *url = [NSURL URLWithString:@"https://localhost/svc/portal/setting"]; NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];[request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];[request setHTTPShouldHandleCookies:NO];[request setTimeoutInterval:30];[request setHTTPMethod:@"GET"]; NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { NSString *message = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"%@", message);}]; [task resume];
上面的程式碼片段,是用NSURLSessionDataTask發起https請求。在ssl握手階段,會調用2次下面的delegate method
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler{ NSString *method = challenge.protectionSpace.authenticationMethod; NSLog(@"%@", method); if([method isEqualToString:NSURLAuthenticationMethodServerTrust]){ NSString *host = challenge.protectionSpace.host; NSLog(@"%@", host); NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; completionHandler(NSURLSessionAuthChallengeUseCredential, credential); return; } NSString *thePath = [[NSBundle mainBundle] pathForResource:@"client" ofType:@"p12"]; NSData *PKCS12Data = [[NSData alloc] initWithContentsOfFile:thePath]; CFDataRef inPKCS12Data = (CFDataRef)CFBridgingRetain(PKCS12Data); SecIdentityRef identity; // 讀取p12認證中的內容 OSStatus result = [self extractP12Data:inPKCS12Data toIdentity:&identity]; if(result != errSecSuccess){ completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); return; } SecCertificateRef certificate = NULL; SecIdentityCopyCertificate (identity, &certificate); const void *certs[] = {certificate}; CFArrayRef certArray = CFArrayCreate(kCFAllocatorDefault, certs, 1, NULL); NSURLCredential *credential = [NSURLCredential credentialWithIdentity:identity certificates:(NSArray*)CFBridgingRelease(certArray) persistence:NSURLCredentialPersistencePermanent]; completionHandler(NSURLSessionAuthChallengeUseCredential, credential);}-(OSStatus) extractP12Data:(CFDataRef)inP12Data toIdentity:(SecIdentityRef*)identity { OSStatus securityError = errSecSuccess; CFStringRef password = CFSTR("the_password"); const void *keys[] = { kSecImportExportPassphrase }; const void *values[] = { password }; CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL); CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL); securityError = SecPKCS12Import(inP12Data, options, &items); if (securityError == 0) { CFDictionaryRef ident = CFArrayGetValueAtIndex(items,0); const void *tempIdentity = NULL; tempIdentity = CFDictionaryGetValue(ident, kSecImportItemIdentity); *identity = (SecIdentityRef)tempIdentity; } if (options) { CFRelease(options); } return securityError;}
上面的程式碼片段,即拷即用,希望能有所協助。這個方法會被調用2次,第一次是ios app驗證server的階段,第二次是server驗證ios app的階段,即client certificate。關鍵是每個階段的校正完成以後,要調用completionHandler方法。網上的大部分文章都是
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
這行代碼是無效的