iOS网络请求安全(JWT,RSA)

1,444 阅读8分钟

在网络世界中,安全是一个很重要的问题,以往的HTTP请求已经不能承担这个安全任务,抓包工具一抓,你的所有网络请求全都曝光。当然,你可能会采用加密算法来加密数据,但是这仍然不够。

在移动端和服务器的通信过程中,有两种认证方式:token和session。

Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在数据库和内存中,而随着认证用户的增多,服务端的开销会明显增大。

扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。

__CSRF: __因为是基于cookie来进行用户识别的, ~~cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。~~CSRF攻击是源于WEB的隐式身份验证机制!WEB的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的!

基于token的鉴权机制 基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

流程上是这样的:

  • 用户使用用户名密码来请求服务器
  • 服务器进行验证用户的信息
  • 服务器通过验证发送给用户一个token
  • 客户端存储token,并在每次请求时附送上这个token值
  • 服务端验证token值,并返回数据

这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *

那么我们现在回到JWT的主题上。

JWT是啥?

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

JWT长什么样?

JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHQiOjE0ODg1NTM3OTk5NzIsImlwIjoiMTkyLjE2OC4xMDIuMTk1IiwidGVsIjoiMTMxMjM0NTY3ODkiLCJ0eXBlIjoiMiIsImRldmljZSI6ImVjMGEwOWZhOWRiOTNjNDQ1Mzk1YzcyNmI2OTUyM2YzIiwiaWF0IjoxNDg4NTI0OTk5OTcyfQ.9rl2XwKIMnVCVVKv9GvhTify7P2xhxIITfaSX4tm_78

JWT的构成

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).

Header

JWT的头部承载两部分信息:

声明类型,这里是JWT 声明加密的算法 通常直接使用 HMAC SHA256 完整的头部就像下面这样的JSON:

{
  'typ': 'JWT',
  'alg': 'HS256'
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.

Playload

Token的第二部分是负载,它包含了claim, Claim是一些实体(通常指的用户)的状态和额外的元数据,有三种类型的claim: reserved , public 和 private .

  • Reserved claims: 这些claim是JWT预先定义的,在JWT中并不会强制使用它们,而是推荐使用,常用的有 iss(签发者) , exp(过期时间戳) , sub(面向的用户) , aud(接收方) , iat(签发时间) 。

  • Public claims:根据需要定义自己的字段,注意应该避免冲突

  • Private claims:这些是自定义的字段,可以用来在双方之间交换信息

负载使用的例子:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

上述的负载需要经过 Base64Url编码后作为JWT结构的第二部分。

Signature

创建签名需要使用编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。例如如果希望使用HMAC SHA256算法,那么签名应该使用下列方式创建:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)  

签名用于验证消息的发送者以及消息是没有经过篡改的。

完整的JWT

JWT格式的输出是以 . 分隔的三段Base64编码,与SAML等基于XML的标准相比,JWT在HTTP和HTML环境中更容易传递。

下列的JWT展示了一个完整的JWT格式,它拼接了之前的Header, Payload以及秘钥签名:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJleHQiOjE0ODg1NTM3OTk5NzIsImlwIjoiMTkyLjE2OC4xMDIuMTk1IiwidGVsIjoiMTMxMjM0NTY3ODkiLCJ0eXBlIjoiMiIsImRldmljZSI6ImVjMGEwOWZhOWRiOTNjNDQ1Mzk1YzcyNmI2OTUyM2YzIiwiaWF0IjoxNDg4NTI0OTk5OTcyfQ.
9rl2XwKIMnVCVVKv9GvhTify7P2xhxIITfaSX4tm_78

如何应用

[AFHTTPSessionManager manager].requestSerializer setValue:JWTToken forHTTPHeaderField:@"token"];

在这次demo中我用的是AFN3.0,Xcoede8。

    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];//服务器返回的是字符串
    NSMutableDictionary *para = [[NSMutableDictionary alloc] init];
    [para setValue:userPhoneNumber forKey:@"ac_id"];//用户手机号
    [para setValue:dToken forKey:@"token"];//UUID    
    
    NSURLSessionDataTask *task =  [self httpRequestWithMethod:@"POST" pathUrl:USER_GET_NEW_SESSION_ID_URL parameters:para success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        
    NSString *resonseString = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];//服务器给我们的是一个二进制流字符串,先将其转化成NSString
        
    NSString *decodeStr = [RSACode decryptString:resonseString publicKey:PUBLICKEY];//服务器给我的数据经过了RSA加密,这里用公钥进行解密
    NSDictionary *dicObject = [RSACode dictionaryWithJsonString:decodeStr];//解密后得到的是一个字符串,将其转换成字典
    NSMutableArray *resultJson = [dicObject objectForKey:@"result"];

    [[NSUserDefaults standardUserDefaults] setValue:[[resultJson firstObject]objectForKey:@"jwtToken"] forKey:RME_DEVICE_JWT];//通过NSUserDefaults将token进行永久化存储
    [self.requestSerializer setValue:[[resultJson firstObject]objectForKey:@"jwtToken"] forHTTPHeaderField:@"token"];//将token放到请求头      
  
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

    }];

上面讲完了JWT,接下来就是RSA加密。

__RSA算法__是目前最流行的公钥密码算法,它使用长度可以变化的密钥。RSA是第一个既能用于数据加密也能用于数字签名的算法。 RSA算法的原理如下: 1.随机选择两个大质数pqp不等于q,计算N=pq; 2.选择一个大于1小于N的自然数ee必须与(p-1)×(q-1)互素。 3.用公式计算出d:d×e = 1 (mod (p-1)×(q-1)) 。 4.销毁pq。 最终得到的Ne就是“公钥”,d就是“私钥”,发送方使用N去加密数据,接收方只有使用d才能解开数据内容。 RSA的安全性依赖于大数分解,小于1024位的N已经被证明是不安全的,而且由于RSA算法进行的都是大数计算,使得RSA最快的情况也比DES慢上好几倍,这也是RSA最大的缺陷,因此它通常只能用于加密少量数据或者加密密钥。需要注意的是,RSA算法的安全性只是一种计算安全性,绝不是无条件的安全性,这是由它的理论基础决定的。因此,在实现RSA算法的过程中,每一步都应尽量从安全性方面考虑。

首先通过Mac终端生成公钥和私钥。

RSA公钥私钥.png

红线部分是需要输入的指令,生成的文件在当前目录下。

openssl
genrsa -out rsa_private_key.pem 1024
pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt
rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem

生成了公钥和私钥之后,我们APP端只要持有公钥即可,私钥放于服务端。这样在和服务器进行通信的时候,敏感信息服务器可以用私钥将其加密,即使他人拿到数据也没有用。而我们传给服务器的数据,也可以通过公钥去加密,由于公钥加密的数据只有对应的私钥才能解开,而私钥只有服务器才持有,这样就保证了只有服务器才能解开加密。这样就会使我们和服务器的通信更加安全可靠。


Session Token
状态存储位置 服务器存储所有的Session状态信息 每个客户端存储自己的token状态
扩展性 即使是分布式的布局方式,也需要到存储当前Session的服务器获取数据 任意一台服务器均可解析Token
分享 无法分享和授权给其他应用 可以很方便的分享和授权给其他应用,而且很安全,无法伪造
依赖性 通常需要借助Cookie来实现 没有依赖,加密方式安全就安全