前言

一般来说,客户端 App 与服务器端是通过接口进行交互,来互相传递数据的,而为了保证数据的安全性,一般都会专门设计一个签名规则。

三种安全强度的签名设计

第一种:客户端加密(对称加密,如 AES),服务端解密,配合 token 和流水号

优点:能够保持用户登录状态、区分用户,相对于不返回任何信息的登录要安全了一些。

缺点:如果通过网络嗅探器(例如:青花瓷)可以获取到http链接,会造成信息泄露,并且还能被伪造请求。

第二种:客户端加密(非对称加密,如 RSA),服务端解密,配合 token 和流水号

采用 RSA 非对称加密。具体流程如下:

  • 客户端向服务器第一次发起登录请求(不传输用户名和密码)。

  • 服务器利用RSA算法产生一对公钥和私钥。并保留私钥, 将公钥发送给客户端。

  • 客户端收到公钥后, 加密用户密码, 向服务器发起第二次登录请求(传输用户名和加密后的密码)。

  • 服务器利用保留的私钥对密文进行解密,得到真正的密码。

第三种:非对称加密 + token + 流水号

前两种方法如果 token 被截获了,那么他人完全就能够模拟出用户的请求,所以在服务器向客户端发送的 token 数据,也需要加密。

具体流程如下:

  • 客户端向服务器第一次发起登录请求(不传输用户名和密码),服务器利用RSA算法产生一对公钥和私钥,并保留私钥,将公钥发送给客户端。

  • 客户端收到公钥后,加密用户密码,向服务器发送用户名和加密后的用户密码; 同时另外产生一对公钥和私钥,自己保留私钥, 向服务器发送公钥,于是第二次登录请求传输了用户名和加密后的密码以及客户端生成的公钥。

  • 服务器利用保留的私钥对密文进行解密,得到真正的密码。经过判断, 确定用户可以登录后,生成sessionId和token,同时利用客户端发送的公钥,对token进行加密。最后将sessionId和加密后的token返还给客户端。

  • 客户端利用自己生成的私钥对token密文解密,得到真正的token。

实践

从签名强度来看,上面方法第三种 > 第二种 > 第一种,所以此处省略第一二种实践方式,主要看看第三种。

流程

具体的实践分为登录(拿 token) 和与用户信息相关的接口加密(AES) ,比上文第三种签名方式,多出一步 AES 加密:

  • 1.客户端向服务器第一次发起登录请求(注意这里是登录请求,不是调登录接口)。

  • 2.服务器生成公私钥对(公钥1、私钥1),将公钥1发送给客户端。

  • 3.客户端收到公钥1后,加密用户密码,同时本地生成公私钥对(公钥2、私钥2),调用登录接口,将公钥2和用公钥1加密过后的用户密码发给服务端。

  • 4.服务器用私钥1对密文进行解密,得到真正的密码。经过判断,确定用户可以登录后,生成 sessionId 和 token,同时用客户端发送的公钥2,对 token 加密,最后将 sessionId 和加密后的 token 以及 AES 密钥返给客户端。

  • 5.客户端用私钥2解密,得到 token 、sessionId、AES 密钥。

  • 6.登录流程结束,本地存储有公钥1、公钥2、私钥2、AES 密钥、sessionId 和 token。

密钥传递与处理

首先要明确 RSA 公私钥密文格式,这里建议客户端和服务端统一使用字符串,即公私钥在客户端是以字符串的形式传递的。

其次,本地存储的公钥1、公钥2、私钥2、AES 密钥、sessionId 和 token,建议统一放到 keychain 中去,防止泄露。

密钥生成

利用 OpenSSL 在客户端生成 RSA 密钥对时,并没找到方法将密钥(SecKeyRef)转成字符串(若您找到了,请告知于我,不胜感激),所以在这用了一个取巧的方法:将公私钥保存为本地文件,然后再从这个文件中读取出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#pragma mark - 本地生成 RSA 公私钥对
+ (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];
}

最后

以上只是较常用三种安全强度的签名设计,真正 App 签名设计远远不止这三种,比这些安全强度高的也有许多,继续学习。