背景
SSO,英文全称Single Sign On,单点登录,一般应用于多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统的保护资源。如登录访问 blog.baidu.com 后,对于 edu.baidu.com 也是登录访问。
虽然在我们日常工作中产品的账号体系一般都由各司的账号中台部门负责,业务方只需要按照文档接入即可开箱使用,如果事先了解一些实现原理,可以减少不必要的沟通成本。
概念
实现方案
记录用户身份信息的 Token 一般以 Cookie 的方式存储到浏览器,因此以下出现两种方案。
主域名
如果公司内的所有业务都共享到 baidu.com 这个主域名下,则可以将 Token 的 Cookie 种到 .baidu.com 这个根域名下,这样在任一系统登录后,在访问其他业务系统就不需要重新登录了,因为其他业务系统也是 xx.baidu.com,浏览器会把根域名的 Cookie 带到每个请求中,即可实现单点登录功能。
这个方案的优点是实现简单,前端的接入成本更低,但是有以下几个缺点:
- 限制了业务系统使用域名的自由,在业内是几乎是不可行的
- Token 种到根域名下,能访问所有业务的资源,无法做到 Token 的隔离
多域名
如果公司的业务系统使用不止 baidu.com 这个域名,业内常用 CAS 来解决这个问题,需要新增一个认证中心的服务,网上的文章很多,感兴趣的同学可以看CAS实现单点登录SSO执行原理探究这篇文章。
单点登录
还有几个问题需要解释:
- 如何检测局部会话?
局部会话是通过将 Token (名称通常为xxxx_st)以 Cookie 的形式保存在浏览器端,每个系统都会使用 CAS 的 SDK,通过请求拦截器的方式,会校验请求 Cookie 里的 Token 是否合法,即局部会话是否合法,同时每个系统的 Token 都是由不同的秘钥加密生成,因此不同系统的局部会话是隔离的,保证了各系统的安全性。
- 如何检测全局会话?
全局会话是通过将 Token (名称固定为passToken)以 Cookie 的形式保存在浏览器端,由于帐号中台的域名是 id.baidu.com,用户登录成功后创建的全局会话就以 passToken 的 Cookie 形式存到了 id.baidu.com 域名下,所有系统在跳转到帐号中台进行全局会话的验证时,浏览器都能把 passToken 带到请求里,从而实现了所有系统共享全局会话的功能。
- 令牌 authToken 是什么?
令牌 authToken 是 CAS 为了解决跨域种 Cookie 的一个凭证,因为帐号中台的域名是 id.baidu.com,而系统B的局部会话 Token 需要种到 www.B.com 下才能生效,而浏览器是不允许跨域名种 Cookie,即 CAS 无法设置 www.B.com 下的 Cookie,而只能颁发一个令牌 authToken,通过系统B内的局部会话验证接口后,在同域的情况下设置 Cookie, 从而完成系统B的局部会话的建立。
对于 Token 具体该如何校验会在文章后续部门给出。
单点登出
- CAS 收到系统A的登出后,如何通知其他系统?
这个既不是通过 Zookeeper 或 MQ,也不是系统接口调用,而是通过同一个缓存层来控制,这个缓存是验证局部会话时会使用到,即每个系统在验证局部会话时,都会额外的访问这个缓存验证当前会话是不是已被其他系统注销失效了,验证的依据是会话id,每一次用户登录都会生成一个唯一的会话id,通过这个id可以精准的销毁所有系统内的其他局部会话,使用缓存进行统一管理的好处是,完全解耦帐号中台与其他接入系统的关系,唯一的不足是缓存的可用性可能会造成注销短暂性的失效。
Token 鉴权
前面几节讲述了整个 CAS 的登录登出流程,但是留有一个问题:Token 该如何验证?
可选方案
1)随机字符串+盐hash
最简单的办法就是通过 UUID.randomUUID()
生成一个随机的字符串,然后和一个随机的盐进行 md5 hash,最终得到一个字符串,并以 KV 进行存储。使用这个方式非常简单,冲突的概率并不大,但是缺点也是非常明显:
- 严重依赖 Cache,如果 Cache 一旦可用性下降,直接影响了用户的体验;
- 无法支持多机房的场景,如系统分为南北 2 个机房,如果采用了这个方案,一旦用户在北方机房进行了登录,Token 当时只在北方的 Cache 写入,然后同步到南方的 Cache 中,这个同步依赖南北机房的专线的稳定性,无法保证时延,用户一旦被调度到南方机房,则可能直接 Token 校验失败;
2)JWT
JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案,主要应用于2个系统之间的授信场景,当然用于身份 Token 也可以,具体细节可看 JSON Web Token 入门教程。
3)自研算法
目前绝大多数厂商都会采用自研加密算法将用户的身份信息加密后生成一个字符串作为用户的身份 Token,并在业务服务器中完成对 Token 的合法性校验。
优缺点对比
方案 | 实现难易 | 安全性 | 保密性 | 稳定性 | 可拓展性 |
---|---|---|---|---|---|
随机字符串+盐hash | 简单 | 一般 | 好 | 不好 | 一般 |
JWT | 简单 | 好 | 不好 | 好 | 好 |
自研算法 | 复杂 | 好 | 好 | 好 | 好 |
- 随机字符串+盐hash的方式实现简单,校验时的稳定性不能保证(依赖于缓存的稳定性),如果需要在 token 中新增属性时,扩展性也很难满足;同时这种 token 一旦被劫持,缺乏有效的吊销机制,安全性并不好,适用于项目初期搭建,快速上线流程
- JWT 由于引入了签名认证功能保证了一定的安全性,且是一个行业标准,实现起来也挺简单,但是它最大的问题是保密性不好,token 里的信息等同于明文展示,如果token里需要包含一些敏感信息是难以接受这个方案
- 自研加密的方式除了实现复杂度更高外,其他点都有优势,比较适合于规模更大的使用场景
验证流程
验证 Token 的步骤大致分为下面几个阶段:
解密Token --> 验证UID是否匹配 --> 验证Token是否在有效期 --> 返回结果
- 解密Token是在业务的服务器上进行的本地解密,解决了依赖缓存的稳定性问题,无论量有多大,只要机器够多就没问题;
- 验证token是否在有效期是为了提供了在 token 已经下发后随时吊销的能力,但于此同时它还是引入了缓存的依赖,只不过缓存依赖是可降级的,在大型活动中可降级处理,避免缓存拖慢接口响应速度的问题。
Token 安全
1)Token 分类
为了解决 Token 被劫持后无法有效吊销的问题,可以再次对可鉴权的 token 分为以下2种:
- ServiceToken:客户端访问业务服务端时使用,一般会设置比较短的有效期,及时被劫持,只能用比较短的世界
- PassToken:当 ServiceToken 过期后,用于刷新 ServiceToken,需要存储在客户端内非常安全的地方,只在https环境下传输,有效期时间比较长
其中 ServiceToken 是在访问业务服务端时使用的,由于业务场景的复杂,有可能被恶意的黑产获取,但是因为只有短时间的有效期,很快就会过期,缩小了影响范围,而 Passtoken 仅仅在 https 环境下传输,且使用场景单一,不易泄露,专门用于刷新 ServiceToken。
2)Token 隔离
又由于公司可能有很多的产品线,每个产品线都会下发鉴权 token,而鉴权 token 在各个产品之间是需要隔离的。其实现原理是给每个产品线使用不同的加密秘钥,这样能确保如果一个产品的秘钥泄露,不会对其他产品的鉴权安全性造成风险。
服务间授信
什么是授信的场景:跨部门合作共同完成一个功能很常见,整个流程中需要部门A的服务处理完成后,再交给部门B的服务继续处理,那如何让部门B得知当前流程已经得到了部门A的完整处理呢?