本篇文章将从无到完整的登录框架或 API 详细讲述登录令牌原理, 攻略等安全点.
有些协议或框架也喜欢把令牌叫票据(Ticket), 不论是 App 还是 web 浏览器, 很多框架或协议都用到到了本文所说的这套类似的认证机制(客户端各种加密用户名密码当我没说), 这里的以 ASP.NET core 下 Web 登录和验证为例子进行讲述, 但原理攻略和语言, 框架都无关.
目录:
一, 过程与原理
二, Demo 数据库结构
三, Demo 源码介绍
四, 构建与验证 Token
五, Token 失效与登录唯一性
六, CAS/SSO 单点登录
七, URL 授权验证与扫码登录
八, Session 实现
九, 关于 Token 刷新
本片文章 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) 是可有可无的, 可以根据实际业务来处理, 有必要的话再增加其他相关联的数据和表
三, 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 和 inputPassword 是起到了盐作用, 让整个构建更加复杂
不论是终端 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 可以有效控制其授权, 达到访问控制的目的.
六, CAS/SSO 单点登录
CAS 即中央认证服务, SSO 即 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) 又叫重播攻击, 防范这个其实和本文讨论的主题没关系. 完整实现的接口都有实现, 欲知详情, 等我下一篇.
花了好几天来写了这篇文章, 同时也是自己对这一技术点的总结归纳, 有不对的地方还请指正.
相关链接:
ASP.NET Web API 与 Owin OAuth: 调用与用户相关的 Web API
微信公众平台技术文档 - 获取 access_token
来源: https://www.cnblogs.com/huangsheng/p/10736796.html