前言
一般來說,客戶端 App 與服務(wù)器端是通過接口進(jìn)行交互,來互相傳遞數(shù)據(jù)的,而為了保證數(shù)據(jù)的安全性,一般都會(huì)專門設(shè)計(jì)一個(gè)簽名規(guī)則。
三種安全強(qiáng)度的簽名設(shè)計(jì)
第一種:客戶端加密(對(duì)稱加密,如 AES),服務(wù)端解密,配合 token 和流水號(hào)
優(yōu)點(diǎn):能夠保持用戶登錄狀態(tài)、區(qū)分用戶,相對(duì)于不返回任何信息的登錄要安全了一些。
缺點(diǎn):如果通過網(wǎng)絡(luò)嗅探器(例如:青花瓷)可以獲取到http鏈接,會(huì)造成信息泄露,并且還能被偽造請(qǐng)求。
第二種:客戶端加密(非對(duì)稱加密,如 RSA),服務(wù)端解密,配合 token 和流水號(hào)
采用 RSA 非對(duì)稱加密。具體流程如下:
客戶端向服務(wù)器第一次發(fā)起登錄請(qǐng)求(不傳輸用戶名和密碼)。
服務(wù)器利用RSA算法產(chǎn)生一對(duì)公鑰和私鑰。并保留私鑰, 將公鑰發(fā)送給客戶端。
客戶端收到公鑰后, 加密用戶密碼, 向服務(wù)器發(fā)起第二次登錄請(qǐng)求(傳輸用戶名和加密后的密碼)。
服務(wù)器利用保留的私鑰對(duì)密文進(jìn)行解密,得到真正的密碼。
第三種:非對(duì)稱加密 + token + 流水號(hào)
前兩種方法如果 token 被截獲了,那么他人完全就能夠模擬出用戶的請(qǐng)求,所以在服務(wù)器向客戶端發(fā)送的 token 數(shù)據(jù),也需要加密。
具體流程如下:
客戶端向服務(wù)器第一次發(fā)起登錄請(qǐng)求(不傳輸用戶名和密碼),服務(wù)器利用RSA算法產(chǎn)生一對(duì)公鑰和私鑰,并保留私鑰,將公鑰發(fā)送給客戶端。
客戶端收到公鑰后,加密用戶密碼,向服務(wù)器發(fā)送用戶名和加密后的用戶密碼; 同時(shí)另外產(chǎn)生一對(duì)公鑰和私鑰,自己保留私鑰, 向服務(wù)器發(fā)送公鑰,于是第二次登錄請(qǐng)求傳輸了用戶名和加密后的密碼以及客戶端生成的公鑰。
服務(wù)器利用保留的私鑰對(duì)密文進(jìn)行解密,得到真正的密碼。經(jīng)過判斷, 確定用戶可以登錄后,生成sessionId和token,同時(shí)利用客戶端發(fā)送的公鑰,對(duì)token進(jìn)行加密。最后將sessionId和加密后的token返還給客戶端。
客戶端利用自己生成的私鑰對(duì)token密文解密,得到真正的token。
實(shí)踐
從簽名強(qiáng)度來看,上面方法第三種 > 第二種 > 第一種,所以此處省略第一二種實(shí)踐方式,主要看看第三種。
流程
具體的實(shí)踐分為登錄(拿 token) 和與用戶信息相關(guān)的接口加密(AES) ,比上文第三種簽名方式,多出一步 AES 加密:
1.客戶端向服務(wù)器第一次發(fā)起登錄請(qǐng)求(注意這里是登錄請(qǐng)求,不是調(diào)登錄接口)。
2.服務(wù)器生成公私鑰對(duì)(公鑰1、私鑰1),將公鑰1發(fā)送給客戶端。
3.客戶端收到公鑰1后,加密用戶密碼,同時(shí)本地生成公私鑰對(duì)(公鑰2、私鑰2),調(diào)用登錄接口,將公鑰2和用公鑰1加密過后的用戶密碼發(fā)給服務(wù)端。
4.服務(wù)器用私鑰1對(duì)密文進(jìn)行解密,得到真正的密碼。經(jīng)過判斷,確定用戶可以登錄后,生成 sessionId 和 token,同時(shí)用客戶端發(fā)送的公鑰2,對(duì) token 加密,最后將 sessionId 和加密后的 token 以及 AES 密鑰返給客戶端。
5.客戶端用私鑰2解密,得到 token 、sessionId、AES 密鑰。
6.登錄流程結(jié)束,本地存儲(chǔ)有公鑰1、公鑰2、私鑰2、AES 密鑰、sessionId 和 token。
密鑰傳遞與處理
首先要明確 RSA 公私鑰密文格式,這里建議客戶端和服務(wù)端統(tǒng)一使用字符串,即公私鑰在客戶端是以字符串的形式傳遞的。
其次,本地存儲(chǔ)的公鑰1、公鑰2、私鑰2、AES 密鑰、sessionId 和 token,建議統(tǒng)一放到 keychain 中去,防止泄露。
密鑰生成
利用 OpenSSL 在客戶端生成 RSA 密鑰對(duì)時(shí),并沒找到方法將密鑰(SecKeyRef)轉(zhuǎn)成字符串(若您找到了,請(qǐng)告知于我,不勝感激),所以在這用了一個(gè)取巧的方法:將公私鑰保存為本地文件,然后再從這個(gè)文件中讀取出來。
#pragma mark - 本地生成 RSA 公私鑰對(duì)
+ (NSArray *)generateKeys {
RSA *rsa = NULL;
rsa = RSA_new();
rsa = RSA_generate_key(1024,0x10001,NULL,NULL);
// 路徑
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES)objectAtIndex:0];
//獲取公鑰字符串
NSString *pubPath = [documentsPath stringByAppendingPathComponent:@"PubFile.txt"];
FILE *pubWrite = NULL;
pubWrite = fopen([pubPath UTF8String],"wb");
if(pubWrite == NULL) {
NSLog(@"Read Filed.");
}else{
PEM_write_RSA_PUBKEY(pubWrite,rsa);
fclose(pubWrite);
}
NSString *publicStr = [NSString stringWithContentsOfFile:pubPath encoding:NSUTF8StringEncoding error:nil];
publicStr = [publicStr stringByReplacingOccurrencesOfString:@"-----BEGIN PUBLIC KEY-----"withString:@""];
publicStr = [publicStr stringByReplacingOccurrencesOfString:@"-----END PUBLIC KEY-----"withString:@""];
publicStr = [publicStr stringByReplacingOccurrencesOfString:@"\n"withString:@""];
//獲取私鑰字符串
NSString *priPath = [documentsPath stringByAppendingPathComponent:@"PriFile.txt"];
FILE *priWtire = NULL;
priWtire = fopen([priPath UTF8String],"wb");
EVP_PKEY *pkey = NULL;
if(priWtire == NULL) {
NSLog(@"Read Filed.");
}else{
pkey =EVP_PKEY_new();
EVP_PKEY_assign_RSA(pkey, rsa);
PEM_write_PKCS8PrivateKey(priWtire, pkey,NULL,NULL,0,0,NULL);
fclose(priWtire);
}
NSString *privateStr = [NSString stringWithContentsOfFile:priPath encoding:NSUTF8StringEncoding error:nil];
privateStr = [privateStr stringByReplacingOccurrencesOfString:@"-----BEGIN PRIVATE KEY-----"withString:@""];
privateStr = [privateStr stringByReplacingOccurrencesOfString:@"-----END PRIVATE KEY-----"withString:@""];
privateStr = [privateStr stringByReplacingOccurrencesOfString:@"\n"withString:@""];
return @[publicStr,privateStr];
}
最后
以上只是較常用三種安全強(qiáng)度的簽名設(shè)計(jì),真正 App 簽名設(shè)計(jì)遠(yuǎn)遠(yuǎn)不止這三種,比這些安全強(qiáng)度高的也有許多,繼續(xù)學(xué)習(xí)。