令牌Token和会话Session原理与攻略
本篇文章将从无到完整的登录框架或API详细讲述登录令牌原理、攻略等安全点。
有些协议或框架也喜欢把令牌叫票据(Ticket),不论是APP还是Web浏览器,很多框架或协议都用到了本文所说的这套类似的认证机制(客户端各种加密用户名密码当我没说),这里的以Asp.net core下Web登录和验证为例子进行讲述,但原理攻略和语言、框架都无关。
目录:
本片文章Demo:https://github.com/chaoyebugao/AcctAuthDemo
一、过程与原理
令牌机制简单过程(点击查看大图)
首先,这套机制使用场景是登录授权和身份验证,可以用在Web上,也可以用在API的访问控制上。这套机制其实和很多无状态框架登录/授权验证协议类似,这里讲的其实和OAuth2.0里面授权码模式的原理是一样的(authorization code),只不过我们在这里将其步骤拆分,了解其原理和实现,以后搭建项目应用才能庖丁解牛。还有一点,很多框架的授权机制都太繁重且并不能灵活应用,这时候就可以自己搭一个。
首先,用户使用终端向服务器提供可信凭证(一般登录是用户名密码,微信公众平台是appid+appsecret),服务端确认凭证正确,则返回授权的令牌(以下称Token)。这个Token是随机的字符串且与本次授权唯一相关。返回Token给终端的同时服务端也要一并保存Token,这样终端和服务端都只认Token,终端所有请求发送都需要携带此Token,服务端会验证和控制此Token。此时Token就有两个,一个是终端Token,一个是服务端Token,其中一个不对或没有,服务端都是拒绝的。
举个例子,你上12306购票,购买过程就是授权你Token的过程,你的纸质票就是Token,另外一半对应的Token保存在12306那的DB里头,所有门闸就是网关,当你过门闸时会验证你Token是否对应DB的Token。你下车后,12306就把DB的Token标记处理掉,这样服务端就不会再认你手上的纸质票,票也就作废了。
围绕这一机制,我们将讲述CAS单点登录、令牌授权与身份验证、Session实现、防重放攻击、登录唯一性、URL授权验证(用于验证邮箱等)等
二、Demo数据库结构
设备表:用于识别、记录不同的设备,同一设备应该有唯一的标记Id,下面详说
令牌表:用于持久化令牌,ExpireAt为过期时间,Token即令牌字符串,根据UserId与用户表相关联,根据DeviceId与设备表相关联
用户表:用户表,保存用户名密码等
设备表和设备标记(DeviceId)是可有可无的,可以根据实际业务来处理,有必要的话再增加其他相关联的数据和表。C/S或App的话DeviceId可以用系统的标识Id,像Web浏览器的话因为拿不到类似的东西,我们可以指定一个在Cookie里头,Demo就是这么干的。
三、Demo源码介绍
UserController - 用户注册、登录、注销登录
HomeController - Index - 默认启动页,Token验证页
四、构建与验证Token
构建Token
验证Token
Token的构建发生在用户提供的凭证(如用户名密码)被服务端确认无误之后。一次登录/授权的Token分两部分,服务端持有的我们叫数据库Token,用户端(Endpoint)持有的叫终端Token。终端Token可以是任意的随机字符串构成,所以这里最后要根据登录情况来求得哈希值即终端Token本身。因为后面要根据终端Token来查询处理数据库Token记录,所以他们必须有种关联,这种关联就是如上图所示,终端Token+设备Id得到的哈希值即数据库Token本身。
可以看出,整个生成过程是单向不可逆的,验证也只能是单向验证,所以生成关系是这样的:
授权Token构建关系图
这里有几点要注意的:
- 终端Token应该有足够的长度,且每次应随机生成,因此才有Guid.NewGuid()参与求值
- 终端Token参与生成的userId、name是起到了盐作用,让整个构建更加复杂(经提醒已经排除掉了密码的参与,哈希虽然很难破解但还是谨慎点好)
- 不论是终端Token还是数据库Token都不应该可逆加密处理任何内容,因为可解密的话不论是终端还是数据库数据泄露的,都有被破解的风险,所以用哈希求值是最合适的
- 构建数据库Token有deviceId参与,这样每次Token就只能是对应的deviceId才能被验证,这样就起了绑定作用。除了deviceId还可以绑定其他场景相关的,比如IP地址、终端类型
- 日志最好不要记录任何Token
两部分Token构建好之后,终端Token将被返回给终端,数据库Token持久化到服务端中。终端和数据库都要将各自的Token和场景信息持久化,Demo里面终端Token和deviceId放到了Cookie中。每次请求的终端都需要提交终端Token和绑定用的场景信息(deviceId),因为验证的时候数据库Token保存的是由它们哈希过来的值,因此验证的时候也是使用一样的构建过程(即Demo里面的BuildDatabaseToken方法),这样终端Token和数据库Token就有了对应关系。得到数据库Token就能在数据库里面查找了(即上图的loginTokenRepository.FindUser方法)。Demo的验证页面是Home/Index,里面使用了过滤器CheckLoginTokenActionFilterAttribute做验证,在需要验证的Controller或Action上做ServiceFilter属性标记处理即可。
这里有几点要注意的:
- 如果使用Http做接口且有App接入,不方便地支持Cookie机制的话可以改为放在请求头中
- 如果使用Http且为Web浏览器,终端Token保存的Cookie应该设为HttpOnly,让JS不可触碰
到这里童鞋们知道为什么Token拆成两部分了吗?整个Token授权过程是单向不可逆的,而且每个用户都有自己的哈希盐来生成Token,这样能避免哈希值被批量暴力破解,即使终端Token和数据库Token都泄露了你也对应不上。试想一下如果不是这样而是终端数据库的Token是相同的,那一旦数据库泄露那么黑客就能模拟Token进行登录/授权了。另外数据库Token哈希过后长度变短,查询性能也能提高,毕竟每个请求都需要进行验证,查询频率是很高的。
五、Token失效与登录唯一性
不论是终端Token还是服务端Token都要有失效机制,时间越短越安全,但也要结合使用场景需求来设定时长。终端Token如果是Cookie的话直接用Cookie的过期时间即可,并且要和数据库Token的过期时间一致。数据库Token生成的时候也要指定过期时间,Demo里面数据库保存的字段为ExpireAt。一般有以下几种失效情况:
- 到了过期时间
- 用户修改账户关键信息,服务端需要主动将旧的Token全部作废掉,如修改密码
- 用户注销登录
- 用户使用Token刷新机制
另外类似的,如果需求是只能一种终端一个登录,比如Web和App可以保持同时登录但App只能有一个登录,数据库Token还得绑定“终端类型”,这样在最新一次登录的时候把相同的终端类型的旧的数据库Token全部作废掉就好了。如果账号只能有一个登录,那什么都不绑定,同一时间只保持最新一个Token有效即可。
可以看出,服务端的保有的数据库Token可以有效控制其授权,达到访问控制的目的。
六、CAS/SSO单点登录
CAS即中央认证服务,SSO即单点登录。很多时候这两个会放在一起说,其实CAS是一套解决方案,SSO是一种机制描述。如果我们使用的是Http-Web那么我们如何实现我们自己的SSO呢?很简单,把Token和绑定的场景信息提升到同一个域下即可。比如有总部和门店两个系统分别使用了hq.xxxx.com/store.xxxx.com子域名,那不管从哪个系统登录,login_token和deviceId这两个Cookie放在顶级域.xxxx.com下即可,这样所有子系统都能访问得到它们,继而都保有登录/授权状态。有没有发现登录新浪微博后,输入weibo.com都会先跳转到sso然后再跳转回来,这个也差不多,这也是为什么你登录了新浪微博,你新浪博客也是登录了的状态。
七、URL授权验证与扫码登录
当我们需要进行邮箱验证的时候,有可能是用户登录和邮箱不是一个终端的,这时候我们就需要进行URL授权验证来避免用户再次进行登录。其原理很简单,在用户点击验证的链接上面附上URL授权令牌即可(下面简称URL Token),这个URL Token与登录Token不应该有关系所以应当单独保存。生成一个URL Token,服务端再对应保存类似的服务端Token,这样就有了【URL Token】 - 【服务端Token】 - 【用户】这样的对应关系。当用户在有效期内点击后,服务端获得URL Token也就能进行授权或验证。
扫码登录的场景复杂一些,终端生成的二维码其实就是一个Token(我们称之为QR Token)这个Token是和终端绑定的。用户拿App扫了QR码,其实就是在App内同时提交QR Token和用户信息,用户确认可以登录后服务端会颁发登录Token给终端,这样终端就是登录状态了,这一步也就是上面构建和验证登录Token的过程。实际扫码登录需要实现即时通讯,这样终端才能做出相应的反应。另外QR Token也是一样有过期时间的,因此那些扫码登录的页面会做二维码自动刷新的。
八、Session实现
其实有些童鞋会纳闷,完善的框架都会提供Session操作,其原理是一样的,那为什么我们还这么“造作”呢?原因有二,框架自带的可能过重,比如我就很不喜欢asp.net自带的授权认证机制,微软弄得一套一套的,简直就是全家桶,笨重,自己实现一个能定制化且轻量。第二,考虑类似上面的功能实现,自己做能更灵活地实现。
我们已经实现了登录/授权和验证,接下来我们只要想办法把一些数据和Token绑定在一起,并放在缓存中,这些数据就是Session了。我一般的做法是封装一个SessionService,然后定义一套Session接口。一个Session数据由TokenKey-Value组成,如果Token失效,则清理所有对应的TokenKey数据即可。就是这么简单粗暴,不同的缓存组件实现不尽相同。
九、关于Token刷新
OAuth 2.0里面有提供Token刷新服务,即终端持有的Token快过期的时候,终端可以再调用刷新接口来替换快过期的Token,达到永续状态。简单来说就是请求新的Token,请求时旧Token作废掉,实现并不复杂,参见:Oauth2.0(三):Access Token 与 Refresh Token
十、防重放攻击与签名机制
重放攻击(Replay Attacks)又叫重播攻击,防范这个其实和本文讨论的主题没关系。完整实现的接口都有实现,欲知详情,等我下一篇。
花了好几天来写了这篇文章,同时也是自己对这一技术点的总结归纳,有不对的地方还请指正。
相关链接: