本文引用了封宇《JWT技术解决IM系统的认证痛点》一文的部分内容,即时通讯网重新整理、增补和修订,感谢原作者的无私分享。
随着瓜子二手车相关业务的发展,公司有多个业务线都接入了IM系统,IM系统中的Socket长连接的安全问题变得越来越重要。本次分享正是基于此次解决Socket长连接身份安全认证的实践总结而来,方案可能并不完美,但愿能起到抛砖引玉的作用,希望能给您的IM系统开发带来启发。
封宇:瓜子二手车技术专家,中国计算机学会专业会员。主要负责瓜子即时消息解决方案及相关系统研发工作。曾供职于58同城、华北计算技术研究所,参与到家消息系统、58爬虫系统以及多个国家级军工科研项目的架构及研发工作。
封宇同时还分享了其它IM方面的技术实践和总结,您可能也会感兴趣:
《从零开始搭建瓜子二手车IM系统(PPT) [附件下载]》 《一套海量在线用户的移动端IM架构设计实践分享(含详细图文)》 《一个低成本确保IM消息时序的方法探讨》 《移动端IM中大规模群消息的推送如何保证效率、实时性?》
本文是IM通讯安全知识系列文章中的第7篇,总目录如下: 《即时通讯安全篇(一):正确地理解和使用Android端加密算法》 《即时通讯安全篇(二):探讨组合加密算法在IM中的应用》 《即时通讯安全篇(三):常用加解密算法与通讯安全讲解》 《即时通讯安全篇(四):实例分析Android中密钥硬编码的风险》 《即时通讯安全篇(五):对称加密技术在Android上的应用实践》 《即时通讯安全篇(六):非对称加密技术的原理与应用实践》 《即时通讯安全篇(七):用JWT技术解决IM系统Socket长连接的身份认证痛点》(本文)
4、我们面临的技术痛点
针对我们IM系统中的Socket长连接的身份认证安全问题,瓜子有统一登录认证系统SSO(即单点登陆系统,原理详见《IM开发基础知识补课(一):正确理解前置HTTP SSO单点登陆接口的原理》)。
我们的IM长连接通道也利用这个系统做安全认证,结构如下图:
如上图所示,整个认证步骤如下:
1)用户登录App,App从业务后台拿到单点登陆系统SSO颁发的token;
2)当App需要使用IM功能时,将token传给IM客服端SDK;
3)客服端SDK跟IM Server建立长连接的时候用token进行认证;
4)IM Server请求SSO单点登陆系统,确认token合法性。
* 补充:如您对SSO单点登陆系统的了解知之甚少,请务必先阅读《IM开发基础知识补课(一):正确理解前置HTTP SSO单点登陆接口的原理》。
咋一看,这个过程没有什么问题,但是IM(尤其是移动端IM)业务的特殊性,这个流程结构并不好。
为什么说上面的流程结构对于移动端的IM来说并不好呢?原因如下:
1)网络不稳定:手机(移动端)的网络很不稳定,进出地铁可能断网,挪动位置也可能换基站;
2)长连接频繁建立和释放:正因为1)中的原因,在一个聊天会话过程中,会经常重新建立长连接,从而导致上图里的第3步会被频繁执行,进而第4步也会频繁执行;
3)系统压力会增大:鉴于2)中的表现,将大大增加了SSO单点登陆系统的压力(因为IM实例需要频繁的调用SSO系统,从而完全客户端长连接的身份合法性检查);
4)用户体验也不好:长连接建立过程中,因SSO单点登陆系统并不属于IM服务端实例范围之内,IM服务端实例与SSO系统的通信等,带来的额外通信链路延迟对于用户的体验也是一种伤害(而且SSO系统也可能短暂开小差)。
如果不通过上图中的第4步就能完成IM长连接的身份合法性验证,那这个痛点会得到极大缓解。于是,我们便想到了JWT技术。
* 题外话:如果您对移动端弱网络的物理特性还不了解,那么下面的文章有助于您建立起这方面的认知:
《现代移动端网络短连接的优化手段总结:请求速度、弱网适应、安全保障》
《移动端IM开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”》
《移动端IM开发者必读(二):史上最全移动弱网络优化方法总结》
JSON Web Token(简称JWT),是一个开放安全的行业标准(详见RFC7519),可以用于多个系统之间传递安全可靠的信息(也包括本文中将要用到的传递身份认证信息的场景)。
一个完整的JWT的token字符串是什么样子的结构?
▲ JWT说到底也是一个token字符串,它由三部分组成:头部、载荷与签名
正如上图中所示,一个JWT的token字符串组成如下:
1)红色的为Header:指定token类型与签名类型;
2)紫色的为载荷(playload):存储用户id等关键信息;
3)蓝色的为签名:保证整个信息的完整性、可靠性(这个签名字符串,相当于是一段被加密了的密文文本,安全性就是由它来决定的)。
5.2解密JWT的头部(Header)
JWT的头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。
这可以被表示成一个JSON对象:
{ "typ": "JWT", "alg": "HS256" }
▲ 在这个头信息里,标明了这是一个JWT字符串,并且所用的签名算法是HS256算法
对它进行Base64编码,之后的字符串就成了JWT的Header(头部),也就是你在5.1节中看到的红色部分:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
(你可以自已试着进行Base64的加密和解决,比如用这个在线工具:http://tool.oschina.net/encrypt?type=3)
在载荷(playload)中可以定义以下属性:
1)iss: 该JWT的签发者;
2)sub: 该JWT所面向的用户;
3)aud: 接收该JWT的一方;
4)exp(expires): 什么时候过期,这里是一个Unix时间戳;
5)iat(issued at): 在什么时候签发的。
上面的信息也可以用一个JSON对象来描述,将上面的JSON对象进行base64编码,可以得到下面的字符串。
这个字符串我们将它称作JWT的Payload(载荷),以下字串样例就是你在5.1节中看到的紫色部分:
eyJpc3MiOiIyOWZmMDE5OGJlOGM0YzNlYTZlZTA4YjE1MGRhNTU0NC1XRUIiLCJleHAiOjE1MjI0OTE5MTV9
(你可以自已试着进行Base64的加密和解决,比如用这个在线工具:http://tool.oschina.net/encrypt?type=3)
JWT的签名部分,在官方文档中是如下描述的:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
上述伪码的意义,即如下操作:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIyOWZmMDE5OGJlOGM0YzNlYTZlZTA4YjE1MGRhNTU0NC1XRUIiLCJleHAiOjE1MjI0OTE5MTV9
▲ 将上面的两个base64编码后的字符串都用句号‘.’连接在一起(头部在前),就形成了如下字符串
最后,我们将上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。
那么,按照RFC7519上描述的方法,就可以得到我们加密后的内容:
P-k-vIzxElzyzFbzR4tUxAAET8xT9EP49b7hpcPazd0
▲ 这个就是我们需要的JWT的签名部分了
生成JWT的token字符串的最后一步签名过程,实际上是对头部以及载荷内容进行加密。
一般而言:加密算法对于不同的输入产生的输出总是不一样的。所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。
换句话说:你的JWT字符串的安全强度,基本上就是由这个签名部分来决定的。
使用时:服务器端在接受到JWT的token字符串后,会首先用开发者指明的secret(可以理解为密码)对头部和载荷的内容用同一算法再次签名。那么服务器应用是怎么知道我们用的是哪一种算法呢?别忘了,我们在JWT的头部中已经用alg字段指明了我们的加密算法了。
如果服务器端对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们应该拒绝这个JWT Token,返回一个HTTP 401 Unauthorized响应。
JWT是一个怎样的流程? 先上个官方文档的图:
如上图所示,整个应用流程描述如下:
1)客户端使用账户密码请求登录接口;
2)登录成功后服务器使用签名密钥生成JWT ,然后返回JWT给客户端;
3)客户端再次向服务端请求其他接口时带上JWT;
4)服务端接收到JWT后验证签名的有效性.对客户端做出相应的响应。
JWT的整个技术原理,就是一个很典型的对称加密应用过程,通俗的说也就是用开发者在服务端保存的密码,对用户的id等信息进行加密并按照JWT的规则(见5.1节)组成字符串返回给用户。用户在使用时将这个字符串提交给对应的服务端,服务端取出JWT字串的头信息、载荷,用开发者指明的密码试着进行加密并得到一个字符串(即合法的JWT token),两相比较,相同则认为用户提交上来的JWT合法,否则不合法。这就是JWT的全部原理,相当简单易懂。
JWT技术的价值不在于具体的技术实现,而在于它的思想本身,尤其在异构系统、分布式系统方面,可以极大的简化安全认证的成本,包括简化架构复杂性、降低使用门槛等,因为JWT的技术原理决定了认证的过程不需要其它系统的参与,由当前实例自已就可以完成,而成认证代码极小(就是一个加密字符串的比较而已)。
它的技术思路在当前的各种开发系统中应用广泛,比如下图中微信公众号的服务接口配置里,也用到了类似的思想:
另外,苹果著名的APNs推送服务,也支持JWT技术,详见《基于APNs最新HTTP/2接口实现iOS的高性能消息推送(服务端篇)》第6.2节:
▲ 上述截图内容摘录自苹果官方开发者文档
上一章节,我们详细理解了JWT技术的原理,那么回到本文的初衷:我们该如何使用JWT技术来解决上面所提到的通点呢?
我们采用JWT验证IM的Socket长连接流程如下:
如上图所示,整个验证过程描述如下:
1)用户登录App(使用IM客服端SDK),App从业务后台拿到SSO单点登陆系统颁发的token(注意:此token还不是JWT的token,它将在第3)步中被使用并生成真正的JWT token);
2)当App需要使用IM功能时,将token传给IM客服端SDK(这是在客户端完成的,即当App的功能调用IM客服端SDK时传入);
3)IM客服端SDK将用户名及第2步中得到的token发给后台的JWT Server(签发JWT token的模块),请求JWT token;
4)收到第3)步中提交过来的token后,JWT Server会通过RPC等技术向SSO系统提交验证此token的合法性,如果合法,将用跟IM Server约定的Secret(你可以理解为这就是一个固定的密码而已),根据业务需要签发JWT token,并最终返回给IM客服端SDK(即完成第3步中的请求)。
5)后绪,IM客服端SDK将使用得到的JWT token请求IM Server验证长连接,IM Server根据约定的算法(不依赖其他系统直接用JWT的规则,加上第4)步中与JWT Server 约定的Secret)即可完成jwttoken合法性验证。
通过上述努力,移动端在弱网情况下的频繁建立长连接的身份验证痛点得到了解决。
当然,我们之所以选择JWT技术,主要看重的还是它简单易用,但或许正因为如此,某种程度上来说这也恰是居致它的缺点的原因所在。
JWT技术的缺点及建议的解决方法主要有:
1)JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消token或更改token的权限。也就是说,一旦JWT签发,在有效期内将会一直有效;
2)JWT本身包含认证信息(即你在第5.1节中看到的头信息、负载信息),因此一旦信息泄露,任何人都可以获得token的所有权限。为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行进行身份验证;
3)为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS(SSL)协议进行传输。
以下这篇文章列了一些适用JWT的应用场景,仅供参考:
https://www.jianshu.com/p/af8360b83a9f
JWT其实是一项比较有争议的技术,夸它的人会说它简单易用、成本低,极度贬低它的人会说它的安全性就像一层窗户纸——捅一下就破了。
不可否认,跟当前流行的非对称加密技术(大家最熟悉的HTTPS协议就是一个典型的非对称加密应用场景)相比,JWT技术的安全系数确实相对要低一些,因为JWT技术的本质就是对称加密技术的应用,而非对称加密技术出现的原因也就是为了提升对称加密技术所不具有的一些安全性。
但非对称加密技术这么好,也并不意味着对称加密技术就一无是处,因为并不是所有场景都需要用性能、架构的复杂性、运维成本来换取高安全性,还是那句话:“安全这东西,够用就行”,而这也正是JWT这种技术仍然有其价值的原因所在。
非对称加密技术虽然安全,但也并非理论上的无懈可击,这世上还没有绝对安全的算法,总之,不苛责级极致安全的情况下,够用便好,你说呢?
如果您对对称加密和非对称加密技术的还不是太了解,可以阅读以下文章: