0%

基于Token的身份认证

从最终用户的角度来看,用户能提供身份信息的方式有:

  • 用户名/密码
  • 证书(基于x509标准)
  • 各种硬件设备(U盾、RSA密码生成器等)
  • 生物识别(指纹、虹膜、人脸等)

以上这些技术方案中,基于用户名/密码的方案是最普及、也是最广为接受的一种方案。但这种方案的安全性也是最低的。因此,在互联网安全日益严峻的当下,多因素验证(Multi-factor authentication,MFA)作为一种增强的安全措施,也正在被广泛使用,例如:

  • 银行交易需要额外的短信验证码以及各种密码器
  • 登录重要网站时的二次验证码(使用类似Google Authenticator之类的App来生成)

除此之外,登录页面使用HTTPS加密传输也是大型互联网服务商的必备项。

在今天的互联网世界里,大多数的互联网应用都是基于HTTP协议。HTTP是一种没有状态的协议,也就是它并不知道是谁在访问应用。因此在一个典型的需要身份认证的应用里,客户端(可以是移动App、Web浏览器或者常规的桌面程序)在访问服务端时,每一次都要带上身份信息。对于大部分的应用而言,这个身份信息就是用户名/密码。但是,如果每次访问都带上用户名/密码的话,会有以下缺点:

  • 网站或者App需要保存用户名/密码,这会导致安全隐患
  • 增加了被窃取的风险,黑客只要截获了任意一次请求,就可以得到用户名/密码相关的信息(当然稍微有点安全意识的都会加密密码)
  • 服务端收到用户名/密码后都要做一次校验,一般是去数据库中比对,这会导致额外的开销
    Basic authentication 使用的就是这种方案,它仅仅对用户名和密码做Base64编码。这种方案适用于对安全系数要求不高的场景,例如,家用路由器的身份认证。

基于Session/Cookie的身份认证

为了解决上述问题,传统的做法是在用户做登录操作时,我们在服务器上生成一条记录(Session),这条记录会存储当前用户的相关信息,我们把这个记录的Id(Session Id)发送给客户端,客户端收到这个Id后,会把它存储在Cookies中。在以后的访问中,客户端每次带上这个Id,服务端收到这个Id后就可以从Session集合中得到当前访问用户的信息了。Session可以存储在服务器的内存中、磁盘里或者数据库中。Session一般都会设置有效期,服务器端程序需要定期去清理过期的Session。虽然这种技术方案使用了很多年,但依旧有一些缺陷:

  • 存储Session会导致额外的开销
  • 如果Session存储在服务器的内存或硬盘上,那么这将不利于服务的水平扩展。要知道,很多大型的互联网后台服务不可能由一台机器来提供,目前都是采用分布式的架构,部署在成百上千台的机器上
  • 如果Session存储在数据库中,那就回到了原始的情形,需要额外的一次数据库查询比对

基于Token的身份认证

目前主流的技术方案是使用基于token的认证方式,token可以直译为令牌。在用户做登录操作时,相比基于Session/Cookie的方案,服务端校验完用户名/密码后,给用户签发一个token, 一般token里面会包含用户的基本信息以及过期时间,客户端收到token后需要自己保存起来。对于Web浏览器,可以保存到Cookie或者LocalStorage中;对于移动App或者桌面应用,可以将其保存到本地文件中。在后续的访问中,客户端带上这个token,服务端对token进行解析,就知道当前用户是谁了。这种技术方案最大的优势是服务端不需要存储任何与登录用户相关的信息,非常便于服务的水平扩展。我们常说的无状态服务就是这个意思。

JWT

基于Token的身份认证有很多不同的实现方式,JWT(JSON Web Token)是比较流行的解决方案。
JWT的格式为:xxxxx.yyyyy.zzzzz, 分别为Header、Payload和Signature。Header和Payload都是Base64编码的字符串,Signature是对Header和Payload的签名。JWT使用HS512(HMAC using SHA-512)加密算法生成签名,秘钥存储在服务端。客户端如果对token做了任何的篡改,token在服务端校验时就会被判定为无效。Payload部分用来存储用户的基本信息以及过期时间,因为是Base64编码的,所以客户端也有能力不通过其他的API来得到当前用户的部分信息,例如Id、名称、角色以及一些设置信息。这部分的内容是可以与服务端的开发人员进行协商的。

Token的有效期

上面的论述中,我多次提到了有效期或者过期时间。你可以先问问自己一个问题:为什么我们的学生证、身份证、银行卡等等证件信息上都标注了有效期?如果你能想明白这个问题,那么对于token需要携带有效期这个问题也就很容易理解了。答案是尽可能的提高安全性。
想象一下,如果token是永久有效的,有一次你的手机在公共场所偶然连到了黑客架设的一个WiFi,他就可以拿到了你在使用某款App或者某个网站所对应的token,这之后黑客就可以使用这个token一直来冒充你,而你和服务器都却浑然不知。如果此时token包含了有效期,那么黑客只能在token剩下的有效期内冒充你,从而将安全损失降到最低。从这一点考虑,给token设置的有效期不能太长,否则就失去了意义。

Refresh token

事物从来都是具有两面性的。为token加上有效期,提高了安全性,必然会降低用户的体验。因为当token过期之后,用户需要重新被认证才能继续访问服务。难道用户又需要重新登录?没错,你可能会注意到当你打开手机里很久不用的一个App或者登录一个很久没访问过的网站后,你被重新引导到登录页面。但是一个设计良好的App或者网站,它很少会要求用户反复的重新登录。例如,你还记得上次手动登录微信的时间吗?所以,微信或者其他App是如何做到安全性和使用体验之间的平衡呢?

这就需要引入refresh token(微信是否使用refresh token我无从得知,但使用refresh token可以实现类似良好的登录体验)。客户端需要在适当的时候使用refresh token来获得新的token,从而刷新token。需要注意的是refresh token也是有有效期的且一般比token的有效期要长,只要客户端保持一定的活跃度,如此循环往复,客户端就会始终保持一个有效的token,从而实现不需要用户再次登录。

以iOS的App为例

假定我们的需求是只要用户在某个时间点使用了App,那么在这之后的7天之内,用户都不需要再次登录。即活跃度为至少每7天内打开过App。

我们拿iOS的App来举例说明。当用户第一次打开App时,引导用户到登录页面,输入正确的用户名/密码,服务端返回token,refresh token以及过期时间,token过期时间设置为7天。这之后App访问服务器,只会传输token,refresh token 则保存在手机中。在后续的App使用过程中:

  1. 在App的使用过程中,在每次调用服务端的API前,需要判断token是否过期,如果过期则使用refresh token来获取新的token
  2. 在App从前台切换到后台的事件(applicationWillResignActive)的回调函数中,记录当前的时间作为用户最后一次操作App的时间
  3. 在App启动的事件(didFinishLaunchingWithOptions)和从后台切换到前台的事件(applicationDidBecomeActive)的回调函数中,检查token是否超过了过期时间,分下面几种情况:
    • token还在有效期内,则不作任何操作
    • token已经超出了有效期
      • 最后一次使用App的时间距现在不超过7天,则使用refresh token来获取新的token
      • 最后一次使用App的时间距现在已超过7天,重新引导用户登录

一个极端的情形是,客户端的token还差1秒过期,此时用户退出App,再过7天打开App,此时为保证refresh token是有效的,其有效期至少为14天。

上述方案中,第一条可能不太容易实现。因为它需要使用AOP(Aspect-Oriented Programming,面向切面编程)的思想来截获整个程序范围内对服务端API的调用。我们可以做一个折中的设计,假定用户连续操作App的最长时间为6小时,即对于iOS而言,始终让App处于活动状态中,且不让iOS锁屏。方案可以修改为:

  1. 在App从前台切换到后台的事件(applicationWillResignActive)的回调函数中,记录当前的时间作为用户最后一次操作App的时间
  2. 在App启动的事件(didFinishLaunchingWithOptions)和从后台切换到前台的事件(applicationDidBecomeActive)的回调函数中,检查token是否超过了过期时间,分下面几种情况:
    • token还在有效期内
      • 还剩下不到6小时的有效期,则使用refresh token来获取新的token
      • 有效期还很长,则不作任何操作
    • token已经超出了有效期
      • 最后一次使用App的时间距现在不超过7天,则使用refresh token来获取新的token
      • 最后一次使用App的时间距现在已超过7天,重新引导用户登录

App开发人员也可以不用关心活跃度的问题。

在App启动的事件(didFinishLaunchingWithOptions)和从后台切换到前台的事件(applicationDidBecomeActive)的回调函数中,直接检查token的有效期即可:

  • token还在有效期内
    • 还剩下不到6小时的有效期,则使用refresh token来获取新的token
    • 有效期还很长,则不作任何操作
  • token已经超出了有效期
    • refresh token还没有过期,则使用refresh token来获取新的token
    • refresh token也过期了,重新引导用户登录

需要强调的是,因为refresh token能获取新的token,所以要务必保管好refresh token。同时由于refresh token的使用次数很少,这大大降低了它在网络传输过程中被黑客截获的可能性。

以上的这种不断的获取新的token的过程,我们可以称之为token的滑动刷新,类似于TCP协议里的滑动窗口的概念。

OAuth2

关于登录与授权,其实业界已经有标准了——OAuth(Open Authorization),目前已经推出了第二版,简称OAuth2。OAuth只是一套关于授权的开放网络标准,可以自己实现,也可以直接使用第三方类库或组件。例如,微软的ASP.NET OWIN组件就已经包含对了OAuth2的支持。OAuth2中明确定义了access token(表示访问令牌,用来访问资源服务器上需要认证的资源)以及refresh token(表示更新令牌,用来获取下一次的访问令牌)。

相关链接