图上面有一个 seq,可能是我们这边比较独特的设计,你要实现可踢除,就像前图更多是对比两个 ticket 是不是对等,ticket 比较大是一个串,放在一个存储里面,空间挺大,并且不停地变,我们想把它改造成 seq,一个四字节的 int,通过 seq 达到 ticket 踢除的目的。
在用户登录的时候发 ticket,ticket 有 seq,跟手机号加密在里面,每登录一次我会 seq 1,我们有状态的是 seq,由很长的存储变成 int。我们验证 ticket 是否有效?只需要解密,把 seq 拿出来,跟数据库 seq 对比是不是一样?一样,就过了。
这个项目我说了几点。第一,我用 token 的概念,实际上是没用,把它干掉,通过有状态的 seq 做到。另外,ticket 里面是自己包含内容是有语义的,这为我们降级各方面做了很多的探索,我们在降级的情况下会牺牲一点点的安全性。刚才说 Seq,由 ticket 变成 seq,存储下降非常多。
这里实例说的还是柔性降级,假设 seq 存在 cache 里面,cache 这一级挂了,我们还是能够做到验证 seq,能解密,seq 判断符合要求,在降级的情况下也是可以过的。当然这也牺牲了一点安全性。
4. 柔性及可降级之短信验证码
我们最早大部分登录使用验证码,另外我们系统有很多的入口,我们在腾讯的微信、支付宝里面都有访问入口。在 Web 环境下系统很容易被攻击,后面我会讲攻击的事情。
登录时候,输手机,发验证码,输入验证码,然后到我这边服务端做校验。通常做法也是用户点获取验证码,验证码有效期几分钟,系统存储一个手机号跟 code(验证码)的关系,登录的时候把手机号跟 code 传进来。

验证时候根据手机号找到存储里面 code,两个一比,相同就通过了。但是也有问题,假如 cache 挂了,登录不了就会很被动,如何实现高可用?
Cache 复制永远高可用是另外一个话题,我们尝试了另外一种柔性可用的方法。我们的需求是验证码能够在几分钟内有效,我们也可以计算,手机号加上当前时间戳,实际是 unix seconds 变成 unix minutes,算当前是多少秒分钟。通过手机号加时间,在它发的时候算 code,输入就是手机号加 unit,输出给它一个 code。

第二步,用户输入手机号传过来手机号加 code,假设配置是 5 分钟有效,计算其中的时间,拿手机号加上当前的时间,假设是 5 分钟,递减 5 分钟,当前的分钟数和手机号算一个值与 code 对比,不对的话最多循环五次(当然这个也有优化之处)。
当极端情况 cache 不可用的时候,我们可以手机号以及时间,通过内部的算法算出验证码是否基本可符,降级之后安全级别会一定降低,在可用性和安全性方面取得一个折衷。(编者:假如在系统正常情况下,cache 的验证码可以通过算法再加一个随机因子,严格符合才能通过,这样正常时候安全级别是可以有保证。)
攻击者有两种攻击,一种拿着手机号,换不同的 code,这是一种攻击。另外一种攻击,拿固定的 code 换不同的手机号,我们现在结合的方式,我们现在还是采用第一种,当我们后端服务不可用,还有兜底方,当然牺牲了一点点安全性。当我挂掉那段时间,我还是照样可以登录。
5. 高可用与异地多活
讲一下异地多活,保证系统永远可登录。在滴滴,由于业务发展太快,下图是当前业务分布的情况,它带给我们的一些麻烦。

上面是当前 Passport 简单的图解,我们现在是有 3 IDC,每一个 IDC 里面部署不同的业务,我们滴滴还没有做到业务异地多活。可能 IDC1 有专车快车,IDC2 有顺风车,IDC3 里面有代驾。
我们现在是租用的机房,一个 IDC 如果机器不够用,就从别的机房匀出一些,导致我们的业务非常分散,这也给 Passport 和账号团队的服务提出挑战,我们要提前业务做异地多活。但是现在业务本身并没有做到异地多活。
我们把登录实现了多活,注册还没做,但是目前已经足够满足我们要求,如果一个机房挂掉只是影响新注册的用户,在一定程度是可以接受的,所有其它的服务可保证正常使用。
刚才提到不同的 IDC 存在不同的业务。一个人登录进来,先用快车在 IDC1,点开顺风车在 IDC2。这里面就有一些很细节的东西,也就是刚才说 ticket 问题。用户用户在 IDC1 登录,IDC1 给他一个 ticket,这个时候 IDC2 里的 ticket 并没有更新,因为我们所有的请求都是在同机房完成。这时候切到另外一个 IDC 校验,如果当前的 seq 比它传过来,而且发现比他当前小的情况,可以考虑放行。这是由于有可能同步的延迟,seq 还没同步过来。通过这个柔性可用策略,一定程度解决了多机房数据同步不一致的问题。
6. 独立的访问控制层——Argus
我们部门所有的服务都是平台级的业务,比如账号支付,所有业务线都要访问,一般都是通过内网来访问。
为什么要做过载保护?当公司业务部门增多后,会碰到不同的业务拿到线上做压力测试的情况,我们现在所有的公共业务部署,不是按业务线多地部署,我们是大池子大集群,每个业务线都来混合访问。账号访问由于容量比较大,一般压测并没有引起问题。但是在支付的时候,做压力测试在线上支付,就可能会直接把支付拖挂。
我们想必须有一种机制,不能相信任何的业务方,它随时能犯错误,需要通过技术手段去解决。因此需要有过载保护,包括权限控制等一系列机制等。
如上图,防控就是 Argus 系统,承载了过载保护,白名单、安全策略等等职责。它是独立的服务,所有的业务流量打过来,都需要通过它做过滤。
上面提到现在业务并没有多机房的部署,因此如果需要对调用方进行 QPS 的限制?只需要通过在 cache 里设置一个配额,每调一次检验一次。
但这样有个问题,调用量太大。比如说快车有千万级别调用,调用量比较大,我给快车的某一个核心业务一个配额,如果都放在单个实例是支撑不住。因此可以增加一个简单的散列的方式,比如每个调用方调用的时通过 hash 到不同的 Argus 节点上。比如配额是十万 QPS,则可以部署 10 个节点,Argus 每一个就是一万,这样访问就比较可控了。
7. 接口拆分
刚才说的核心登录功能,不经常变,我们希望最稳定的接口独立出来,目的是让稳定不变更的部分故障率降低,所以需要考虑进行拆分。
核心的接口包括登录这一块,其实不经常变,但是像一些小逻辑,策略会经常跟着去上线,但大部分事故都是上线引起的。
分享一个 Pass-TT 的案例。当时跟快的合并时候,快的所有业务在阿里,滴滴所有的服务在深圳腾讯机房,ticket 服务在内网,两个机房跨公网,改造太大了,并且延时不可以接受。
所以我们设计了一个方式,简单说,登录从我们这边登录,访问快的服务的时候再给它 ticket,token。这个 token 专门为代驾用的,但是设计时候犯了一个错误,就是 RSA 方式进行加密。因为有一个远程校验,为了不想 key 泄漏,所以用 RSA 的方式,他们那边部署了一个公钥,我们这边是私钥,token 用我们的私钥加密,然后到它那边进行解密就 OK 了。
