核心解决问题
- 不信任任何微服务rpc的调用,即使是内部网络也是不可信任的
可能存在内部人员伪造rpc请求,可能存在黑客进入内网的情况,因此不可信任
- 用户的核心信息在网络中明文传递,非常不安全
核心思想
- 通过类似token的机制进行票据的颁发和验证,票据是层层传递的,意味着每次rpc请求都需要对票据进行校验,票据类似jwt token,本身携带信息
鉴权系统类型
方案1:单点登录
- 每个服务都访问鉴权服务
- 从性能和可用性角度来说,每个服务都要通过网络请求去鉴权速度是很慢的,而且会产生巨量的网络流量消耗,并且鉴权服务成为了系统的单点,一旦网络抖动或者鉴权服务故障,整个系统的可用性都要受到影响。
- 从架构设计的角度来说,这样不仅会造成对鉴权服务的依赖,也将鉴权和业务耦合在了一起。一旦鉴权方式有变更,系统中所有相关的服务都要改代码重编上线,管理非常困难。
方案2:分布式Session
- 业界常用的办法,但是存在较大缺点,鉴权后将身份信息存入Session,并返回SessionID,每个服务根据SessionID取出身份信息进行验证.这部分的方法就是常常使用的信息中心session存储,分配sessionID的机制
- session的存储是核心问题
- 中心存储:直接将所有Session数据存于专门的存储中,每个需要鉴权的服务都去读取.这种方法最为简单直接,但是这样做其实和单点登录的缺点类似:消耗大量网络资源,Session存储成为单点(中心化的架构都是危险的)
- 存储隔离+一致性哈希:不同身份映射到不同的存储区域,同一身份的请求总是路由到固定的存储区域。 这种方法对存储的压力较小(每个存储区域只存一部分数据),也具有一定的容灾能力(一个区域的存储故障不会影响其他区域)。但是由于不同身份的请求量差异很大(不同商户的交易量千差万别),存储会出现严重倾斜的情况(这部分是我完全没想到的)
- 分布式同步:将Session信息直接存储在业务服务机器上,并且使用分布式算法同步数据.这种方法可以避免中心存储带来的单点故障,存取Session也更快。但是分布式数据同步的实现是非常复杂的,尤其在微信的几万台机器之间同步,需要大量网络带宽消耗,并且最终一致的时延也可能对业务造成影响。
- 可行,但是麻烦而且消耗大量资源
方案3:客户端Token
- 客户端Token(Client Token)是指鉴权后将身份信息进行签名,生成Token返回给客户端,客户端带着Token发起请求,每个服务都可以验证Token的有效性,并通过Token中的信息来验证身份.不仅可以避免存储带来的可用性问题和成本问题,而且验证身份并不需要经过网络:只要把验证有效性的密钥下发到业务服务器上即可本地进行验证。
- 商户票据的实现方式
方案4:客户端Token+API网关
- 客户端Token+API网关(Client Token with API gateway)是指在方案3的基础上加入一层网关,把内部生成的Token转换为对外的Token交给客户端。客户端发起请求时通过网关再转换为内部的Token。这种方案将内外解耦,加强了对Token的控制能力。比如API网关可以随时独立控制某个Token的有效性,而外部毫无感知。并且由于只有API网关可以转换内外Token,无需担心外部服务拿着Token直接非法调用其他的服务。
- 凭证正是基于该方案实现。
- 感觉这种方式其实和session差不多,有中心化的问题,但是还是会出现中性化的问题
票据结构
- 已签名的部分就是加密的部分,内部再放一次票据类型是为了防止篡改,外部放一次为了包形式统一
- 使用RSA非对称加密的方式
- 票据通常放在请求头中
票据获取
- 核心就是即使是CGi也不能直接颁发票据,只能通过某个key区换取用户票据
客户端CGI
- 自带用户票据,唯一一个可以直接信任的设备
微信打开的网页CGI
- 用
export_key
换取pass_ticket
。XCGI可以使用内置拦截器。
小程序CGI
- 使用
session_key
换取用户票据。
API商户票据
- 和飞书的类似,使用定时发送sessionid的形式,后台可以通过session ID拿到对应的票据
颁票方式
- agnet是一个机器ip一台的(方便管理),但是一个机器上可能存在一大堆的server
- 使用公钥验票,私钥颁票
方案一:本地agent颁票
- 部署本地Agent拉取密钥,密钥存放于Agent内存中,业务服务通过本地调用的方式(如Unix Socket)请求Agent颁票或验票
- 这个方式会出现一旦agent爆炸了,本地所有的的颁票和验票服务直接G了,好处是这样本地不储存密钥,安全性高
方案二:本地agent拉取密钥
- 部署 放在内存中,颁票和验票都是让agent进行,方案二则是agent拉取之后直接扔到本地,需要颁票验票自己读取
- 可用性比较好。Agent只负责拉取密钥,即使挂掉也不影响颁票或验票。需要依赖运维手段控制私钥:颁票模块不能与验票模块混布(避免恶意服务直接读取本地颁票)
- 在本地存储时使用本机IP作为密钥的一部分再做一次对称加密,避免私钥被拷贝到其他机器上使用,具体的流程为agent拉取密钥之后将将公私钥使用AES对称加密,密钥为本级ip,本地服务用的时候先根据ip解密才能用
方案三:票据中心服务
- 业务服务直接请求统一的颁票和验票服务
- 无需部署Agent,架构简单,逻辑简单,颁票和验票服务成为整个系统的单点,一旦出问题将导致全局不可用;鉴权和颁票无法一起完成,安全性无法保证。
最终方案
- 方案一,方案二和方案三混合使用,为了安全性,普通的服务不能颁票和验票,只能通过rpc调用特定的服务器才能使用,这种服务就是第二种方案的颁票和验票服务,除此以外,如果设计到票据转换,需要方案三
- 客户端:使用后台网络服务的各种类型终端。
- 客户端使用后台网络服务之前会先接入登录服务器进行身份验证。
- 登录服务器负责处理客户端用户的身份验证,将客户端提交的用户身份及验证信息进行基础的合法性校验后,将最核心的密码验证交由密码验证服务器来处理。
- 密码验证服务器:对用户密码进行验证,验证成功后会派发票据,票据加密时使用的密钥由密钥管理服务器定期更新。
- 密钥管理服务器:定期重新生成加密票据所使用的密钥,以减少外界破解密钥的风险,将密钥加密后推送到生成票据的密码验证服务器和验证票据的核心数据服务器。
- 核心数据服务器:存储用户的核心资料信息,包含用户的个人资料,好友列表,群资料等。逻辑服务器对外提供服务时需要获取用户对应的核心资料信息进行处理,核心数据服务器在提供数据给逻辑服务器时进行票据合法性验证,只有票据验证通过的请求才能拿到对应的用户核心资料信息。
- 逻辑服务器:对外提供网络服务,需要客户端将登录时派发的票据带上来,逻辑服务器将票据透传给核心数据服务器来验证并获取用户的核心资料信息,进行相应的业务逻辑处理,返回处理结果给客户端。
- 本地验票一般作为兜底的方案,出现大规模故障的时候才会使用,但是因为验票服务器比较多,所以一般使用验票服务器操作
密钥下发
Agent定时轮询中心服务
- 实现简单,Agent只管拉密钥,中心系统只管给密钥。中心系统可根据Agent轮询请求获取所有Agent的状况。
- 中心服务需要认证Agent,判断哪些Agent可以拿到私钥,哪些可以拿到公钥。
中心服务主动向Agent推
- 密钥更新时才触发全局推送,有新Agent加入时只需指定推送。
- Agent需要监听端口,中心系统需要定期查询机器变化情况,并主动对失败的Agent定期补发,实现较复杂。
最终方案
- 为了控制不同机器获得密钥的权限(能否拿到公钥或私钥),中心系统记录了一个白名单,里面是当前所有接入商户票据的模块对应的机器IP列表及这些机器具有的权限。Agent前来请求时,密钥存储服务会根据来源IP查找对应机器的权限,应答相应的密钥。
密钥更新
目标
- 最终所有Agent可以拿到新生成的密钥,并使用这个版本来进行颁票和验票。
- 在这个过程中,保证业务无感知,做到平滑过渡升级。
方法
- 对于Agent来说,只需定时上报本地密钥的最新版本号和生效版本号,并把取回的最新版本和生效版本的公私钥写到本地。(需要告诉中心服务自己最新的密钥版本,中心需要告诉agent应该用哪个版本),本地可能储存多个密钥版本
- 中心服务需要统计现在的agent密钥版本,如果都拿到最新版本,那么下次agent请求的时候可以让他们用新的版本颁票,验票的部分因为存在老版本的密钥,就算有延迟导致还是旧的票据也因此不影响验票,只是必须保证最新的密钥需要所有agent都收到才能用最新的颁票,都这无法验票,中心服务如果发现有个agent死活连不上,会强制过期旧的密钥(相当于放弃旧agent)
安全体系
- 使用RSA对称加密用于防止伪造
- 票据和请求唯一ID绑定,该ID在整条请求链中应保持不变(对于Svrkit来说是CallGraphID)
- 每个模块配置模块开关,全局配置全局开关。一旦使用全局开关,所有验票都将直接返回成功,这部分主要是考虑如果中心服务挂了,导致无法下发密钥,啊开这个开关,就会给所有的agent发送开关的请求,此时agent就会通过修改版本密钥的配置的手段使得所有的票据直接通过,这个部分是直接作用于颁票服务和验票服务的,使得验票的服务直接全部通过
凭证
- 凭证用于延期和延续票据,是票据的临时替身:
- 凭证由票据生成,可以落地存储。
- 凭证可以换回票据。
- 凭证可以比票据有更长的有效期。
- 通常用于请求转发给外部门,然后回调本部门
设计
- 加密解密,没有存储,依赖更少,速度更快,可用性更高,但是长度和票据一致,可能很长
- 落地存储,凭证只是一个ID,映射的内容在存储中,调整灵活|需要存储,要考虑存储容灾问题
- 最后使用第二种方法,业务需要落地存储,最好长度比较短且可控;另外方案2还预留了能力可以限制使用次数、续期凭证和撤销凭证。而且票据毕竟是通行证,传递到别的地方又安全风险
消息凭证
方案 | 优点 | 缺点 |
---|---|---|
1. 业务自行进行票据凭证互换 | 现成方案,无需额外开发,也无需改事件中心 | 业务需要感知票据凭证(但并不关心),使用非常繁琐;成为请求关键路径,可用性要求极高;事件中心请求量很大,需要准备大量存储空间。 |
2. 改造事件中心,自动完成票据凭证互换 | 相比方案1,业务感知较小 | 除了方案1的缺点,还要改动事件中心:对事件中心来说依赖了外部系统,破坏了通用性(只有微信支付在用),可用性降低。 |
3. 事件中心透传,事件中心给商户票据加入延期标记,验票时特殊处理 | 业务无感知,不需要存储 | 对于事件中心仍然依赖了外部系统,和方案2一样,通用性和可用性都难以接受。 |
4. 事件中心透传,事件中心自己生成延期标记,放在请求包中,验票时特殊处理 | 业务无感知,不需要存储,而且是通用方案,与业务解耦 | 需要改造事件中心 |
- 核心问题是消息队列会进行重试,但是票据的有效期有限,容易过期,最后选择方案四,和方案一的凭证互换
引申
微信扫码授权登录过程
- app扫码(码的url通常是微信的域名,然后需要实际访问的链接通过query传入,然后app携带访问服务器,服务器授权申请,然后服务器将票据换取凭证,并且重定向到实际访问的链接,凭证通过query携带凭证通常是一个code)
- 实际访问的服务器只拥有凭证(这种凭证有效期一般设置为2分钟),可以通过凭证获取用户的信息(这里通过凭证code获取openid)
- 避免了外部地址拿到凭证,票据加密是一层(避免信息泄漏),签名是一层(避免篡改),换成凭证是一层(避免出现票据泄漏的问题,方便控制时间,本身不携带信息)
总结
- 创建理念是不信任任何一个服务,只认票据不认rpc
- 票据的生成和检验使用的是多种方式结合
- 外部调用使用凭证代替票据,增强安全性
参考