前不久在前后端分离的实践中提到了基于Token的认证,现在我们再深入一点。通常,当我们讨论一项技术时,我们会从一个问题开始。那么第一个问题:为什么要使用Token?回答这个问题很简单——因为它可以解决问题!它能解决什么问题?Token完全由应用程序管理,因此可以避免同源策略Token可以避免CSRF攻击(http://dwz.cn/7joLzx)Token可以是无状态的,可以在多个服务之间共享。令牌在服务器端生成。如果前端使用用户名/密码向服务器请求认证,服务器认证成功,则服务器返回Token给前端。前端每次请求都可以带上Token来证明自己的合法身份。如果这个Token持久化在服务器端(比如保存在数据库中),就是一个永久的身份令牌。于是又产生了一个问题:Token是否需要设置有效期?我需要设置到期日期吗?对于这个问题,我们不妨先看两个例子。比如登录密码,一般需要定期修改密码以防止泄露,所以密码是有有效期的;另一个例子是安全证书。SSL安全证书是有有效期的,目的是解决吊销问题,这个问题的详细内容可以看知乎的回答(http://dwz.cn/7joMhq)。所以无论是从安全性还是撤销的角度考虑,Token都需要有有效期。那么有效期是多久呢?只能说根据系统的安全需要,越短越好,但也不能太短——想象一下手机自动熄屏的时间。如果设置为10秒无任何操作,会自动熄屏,再次开启需要输入密码,会不会很抓狂?如果觉得不行,那就自己试试吧,设置成能设置的最短时间,坚持一个星期(不排除有人适应这个时间,毕竟手机厂商都是也与用户体验研究)。那么一个新的问题出现了。如果正常运行时用户的Token过期失效,需要用户重新登录……用户体验不是很差吗?为了解决用户在运行过程中感觉不到Token失效的问题,有一个解决方案将Token状态保存在服务器端,每次都会自动刷新(推迟)Token的过期时间用户操作——Session使用该策略维护用户的登录状态。但是,仍然存在这样的问题。在前后端分离、单页面应用的情况下,每秒可能会发起很多请求,每次都刷新过期时间会产生非常高的成本。如果将Token的过期时间持久化到数据库或者文件中,成本会更大。因此,为了提高效率和减少消耗,Token过期后会被存放在缓存或内存中。还有一种方案,使用RefreshToken,可以避免频繁的读写操作。在该方案中,服务端不需要刷新Token的过期时间。Token过期后会反馈给前端,前端使用RefreshToken申请新的Token继续使用。该方案在客户端请求更新Token时,服务端只需要检测一次RefreshToken的有效性,大大减少了更新有效期的操作,避免了频繁的读写。当然,RefreshToken也是有有效期的,但是这个有效期可以更长一些,比如以天为单位的时间。时序图可以看出,使用Token和RefreshToken的时序图如下:1)登录2)业务请求3)Token过期,上面RefreshToken的时序图并没有提到RefreshToken过期了要做什么。但是很明显,既然RefreshToken已经过期,应该要求用户重新登录。当然,这个机制可以设计得更复杂。比如每次使用RefreshToken,更新它的过期时间,直到超过它的创建时间很长一段时间(比如三个月),相当于让RefreshToken自动续期一个很长一段时间。至此,Token是有状态的,即需要在服务端保存记录相关的属性。那么无国籍呢?如何实现?StatelessToken如果我们把所有的状态信息附加到Token上,服务器就不需要保存它。但服务器仍然需要身份验证令牌有效。但是,只要服务端能够确认是自己颁发的Token,并且其信息没有被更改,那么这个Token就可以认为是有效的——“签名”可以保证这一点。通常所说的签名是一方签名,另一方验证,所以采用了非对称加密算法。但是这里,签名和验证是同一方,所以对称加密算法可以满足要求,而且对称算法比非对称算法快很多(差距高达几十倍)。进一步思考,对称加密算法除了加密之外,还有恢复加密内容的功能,而这个功能在Token签名时是不需要的——因为不需要解密,所以digest(hash)算法会更快。您可以指定密码的哈希算法,自然是HMAC。上面说了这么多,还需要自己去实现吗?不!JWT定义了详细的规范,并且有多种语言的多种实现。但是,当使用无状态令牌时,服务器端会发生一些变化。服务器端虽然不保存有效的token,但需要保存未过期但已取消的token。如果一个Token在过期前被用户主动注销,服务器端需要保存这个注销的Token,以便下次收到还有效的Token时失效。你觉得有点沮丧吗?在前端可控的情况下(比如前端和服务端在同一个项目组),可以协商:一旦前端退出成功,就会失去本地存储(比如保存在内存、LocalStorage等)Token和RefreshToken。基于这样的约定,服务端可以假设收到的Token一定不会被注销(因为注销后前端将不再使用)。如果前端不可控,上面的假设还是可以的,但是这种情况下需要尽量缩短Token的有效期,并且在用户主动登出的时候RefreshToken必须失效。这个操作存在一定的安全漏洞,因为用户会认为自己已经登出,但实际上他有一小段时间没有登出。如果这个漏洞没有对应用程序设计造成任何损失,那么这个策略是可行的。在使用statelessToken时,有两点需要注意:RefreshToken是长期有效的,所以在服务端应该是有状态的,以增强安全性,保证用户退出可控应该考虑使用二次认证来增强敏感度操作安全在这一点,关于Token的话题似乎差不多——其实不然。上面说的只是认证服务和业务服务融合的情况。如果分开了怎么办?在Token无状态时分离认证服务,单点登录变得容易。一旦前端获得有效的Token,就可以在同一系统的任何服务上对其进行身份验证——只要它们使用相同的密钥和算法来验证Token的有效性。就是这样:当然,如果token过期了,前端还是需要到认证服务去更新token:可见虽然认证和业务分离了,但其实区别不大。当然,这是建立在认证服务器信任业务服务器的前提下,因为认证服务器生成Token所用的密钥和算法与业务服务器认证Token所用的密钥和算法是一样的。也就是说,业务服务器也可以创建有效的令牌。业务服务器不可信怎么办?当一个不受信任的业务服务器遇到一个不受信任的业务服务器时,很容易想到使用不同的密钥。认证服务器使用密钥1进行签名,业务服务器使用密钥2进行验证——这是典型的非对称加密签名应用场景。认证服务器本身使用私钥对Token进行签名,并发布公钥。信任该认证服务器的业务服务器保存用于验证签名的公钥。幸运的是,JWT不仅可以使用HMAC签名,还可以使用RSA(一种非对称加密算法)签名。但是,当业务服务器不再受信任时,用户在多个业务服务器之间使用同一个Token是不安全的。因为任何拿到Token的服务器都可以冒充用户去别的服务器处理业务……悲剧随时可能发生。为了防止这种情况的发生,需要在认证服务器生成Token的时候,在Token中记录使用该Token的业务服务器的信息,这样其他业务服务器拿到Token的时候,发现不是什么它应该是。验证后的Token可以直接拒绝。现在,认证服务器不信任业务服务器,业务服务器之间互不信任,但是前端信任这些服务器——如果前端不信任,就不会使用Token请求验证。那为什么要信任呢?可能是因为这些是由同一家公司或同一项目提供的多种服务组成的服务系统。但是,前端信任不代表用户信任。如果Token不携带用户隐私(如姓名),那么用户就不会关心信任问题。但如果Token包含用户隐私,用户就不得不关心信任问题。这时候认证服务就得更冗长了。当用户请求一个Token的时候,最后一个问题就是,你真的要授权某个业务服务吗?而这个“某某”,用户怎么知道是真是假呢?“某某”怎么说?用户当然不知道,连认证服务都不知道,因为公钥已经公开了,任何商家都可以自称“某某某”。为了获得用户的信任,认证服务不得不帮助用户识别业务服务。因此认证服务器决定不公开公钥,而是要求业务服务先申请注册并通过审核。只有通过审核的业务服务器才能获得认证服务为其创建的公钥,仅供其使用。若业务服务泄露公钥带来风险,由业务服务自行承担。现在认证服务可以清楚地告诉用户“某某”服务是什么。如果用户还是不够信任,认证服务甚至可以询问,某业务服务需要请求A、B、C三个个人数据,其中A是必填项,否则不行,是否允许授权?如果你授权,我会把你授权的几项数据加密到Token中。。。废话这么多,是不是似曾相识。。。顺便说一句,这类似于一个开放API的认证过程。开发API大多使用OAuth认证,关于OAuth的讨论资源很多,这里不再深究。
