轻松筹是全国 1.6 亿人使用的全民众筹平台,几乎所有核心业务都依赖于账号系统,账号系统的用户体验,安全性,稳定性直接影响着轻松筹所有业务的运行。轻松筹发展迅速,目前已经展开了多条产品线,单点登录的需求愈加强烈。另外由于历史包袱的原因,也遗留了一些问题亟待解决。本文主要与大家分享一下轻松筹账号系统(侧重登录授权服务)的架构设计和改造方案。
由于历史包袱的遗留问题,轻松筹的账号系统(登录授权服务)之前主要存在以下几个方面的不足:
由于历史包袱的遗留问题,三端登录方式不统一;
产品线增多,单点登录的需求越来越强烈;
安全性不够;
容灾能力不够;
需要达到以下几个方面的目标:
保证只要用户较长时间段(比如 30 天)内登录过,就不需要重新登录;
单点登录,在一个站点登录后,另外一个站点就不需要重复登录(用户几乎无感知);
采用第三方登录的时候,能使用每一个站点对应的公众号;
Token 难以伪造,具有一定不可逆性;
Token 应该有较快的过期机制,避免被人获取 token 后伪造用户操作;
被恶意窃取后,具有发现机制;
Token 具有自解释性,即自带某些信息,在某些极端恶劣情况下(比如存储服务挂了),依然能提供服务。
账号系统最核心的功能就是登录授权,总体思路也很简单:
用户登录成功后,服务端会生成 Token 信息,并将其和用户信息关联起来,返回给前端 Token 信息,前端携带 Token 来访问所有接口,后端再根据前端发过来的 Token 信息标识这是哪个用户的行为。
这里所说的 Token 信息包括以下几个字段(服务端生成后返回给前端的):
服务端 Token 存储的信息(服务端记录的信息)如下:
服务端存储的数据(Key-Value 存储):
key(access_token) =(user_id,session_id,platform,auth_type)key(refresh_token) =(user_id,session_id,platform,auth_type)key(user_id,platform) =(access_token,refresh_token,expires_in,token_type)
说明:
access_token=hex(hash(uid+time)+aes(uid+time)) 得到,达到目标 B1;
access_token 过期时间较短,refresh_token 较长,两者结合,达到目标 A1,B2;
Token 中自带 uid 信息,即使服务存储挂了,依然不影响其它业务,达到目标 C1;
前端 HTML 5("wxh5" "pch5" "waph5")每一个平台仅维护一个 token,多次登录会剔除旧的,实现目标 B3;
passport 前端拿到这个 token 信息之后,应该记录一下获取时间 cur_time;通过 cur_time+expires_in 与当前时间 提前进行比较来判断 token 是否已经过期, 减少不必要的后端请求;
需要登录的接口,前端需要每次都在请求头中写入 Qsc-Token:access_token 字段;
当 access_token 过期时,用 refresh_token 去换取新的 token 信息,刷新 token 接口流程为:
用户使用 refresh_token 调用刷新 token 接口,后端判断,若 key(refresh_token) 不存在,直接报错;
若是存在,再用 key(user_id,platform) 存储的信息校验两者当前的 refresh_token 是否一致,若是不一致,说明有人利用这个 token 在你之前调用了刷新 Token 接口;
若是一致,则重新生成 Token 信息,替换 key(user_id,platform) 存储的信息,并且处理旧的信息:旧的 key(access_token) 直接删掉,旧的 key(refresh_token) 保留一段时间;
注意:
前端刷新 token 的时候,应该保证操作是互斥 (串行) 的,否则影响第上述的“踢人”功能(App 的前端互斥很好做,加锁就可以了;HTML 5 怎么实现前端互斥,后面会介绍);
后端刷新 Token 的时候,应该保证操作是互斥 (串行) 的 (分布式锁),保证始终只有一个 Token 有效;
关于踢人逻辑的说明:
需要在以下两种情况都能达到踢人的效果:
用户的账号被窃取(第三方账号,或者手机验证码);
用户前端的 refresh_token 被窃取;
解决方法:
以上两种情况用户操作后,都会重新生成 token 信息,并且替换 key(user_id,platform) 存储的信息,然后将旧的 key(access_token) 直接删掉,旧的 key(refresh_token) 保留一段时间,这个时候这个恶意用户是可以伪装的;
但是当原来的用户调用刷新 token 接口的时候(使用的是旧的 refresh_token),这个时候还是能获得 key(refresh_token) 里面的信息,再取出 key(user_id,platform) 存储的信息来校验,会发现两处的 refresh_token 是不一致的,即 可以发现是被人踢下去的;
特别说明:
为什么不用一个 Token 随着请求反复刷新来达到 access_token 和 refresh_token 的效果呢?
因为这种方式前端页面会有并发请求的情况,Token 的刷新是需要加锁的,会带来很大的开销;
没法保证前端刷新 Token 的互斥,会导致反复失效的情况;
对于所有产品线的 Web 平台都实现单点登录 SSO(Single Sign On)的功能,这样只要在一个产品线上(比如站点 A)登录了,在其它产品线上(比如站点 B)就不需要再重新登陆了。
注意:
如果 passport 本地有缓存,优先使用缓存;
在 token 失效的时候,跳转到 passport 的同时,应该把旧的 Token 带着,如果 passport 本地的 Token 跟这个相同则刷新 Token,如果不相同,则直接返回这个 Token 即可;
每一个站点跳转的时候需要携带该站点的标识,passport 根据这个标识决定使用哪一个公众号登录(仅限第三方登录);
各个站点不应该自己 refresh token,在 access_token 失效的时候跳转的 passport 统一处理,这里可以保证刷新 token 是互斥的passport 前端拿到这个 token 信息之后,应该记录一下获取时间 cur_time;然后把这个 cur_time+expires_in+access_token 传给各个站点;
各个站点 通过 cur_time+expires_in 与当前时间进行比较来判断是否已经过期。如果过期,则不需要请求后端(当然这个时候请求后端也会返回 Token 失效)。各个站点可以根据自己的需要,提前过期,比如说提前一个小时就认为 Token 过期了,跳转到 passport 重新获取,这也是用户体验上的考虑。
对于所有产品线的 iOS、Android 等平台暂时不考虑单点登录的功能,但是以后会考虑(比如一个 app 唤起另外一个 app 获取登录 token 信息)整体方案与 web-app 几乎一致,但是有一些特殊性:
对于 iOS、Android、wxapp 小程序,需要独立开发 SDK,然后各个站点统一使用同一套 SDK;
这个 SDK 应该包括 UI 的展现和后端的交互逻辑,统一开发,方便维护;
各平台保证 UI 风格统一;
这里不作过多描述,接下来介绍一下具体的登录流程。目前登录授权支持两种方式:
通过手机验证码登录;
通过第三方平台登录(新用户需要绑定手机号,即再走一遍第一步);
这里仅存在于 passport 前端和后端交互,下面是流程图。
为了防止刷短信的问题,增加了图片验证码的校验,但是考虑用户体验,仅仅只是在怀疑对方是恶意操作的时候;
为了防止短信验证码的暴力破解,做了一些错误次数的校验;
图中黄色区域指的是通过第三方平台获取第三方提供的 code,中间需要周转几次,这里省略。红色区域指的是通过手机验证码登录,就是 手机验证码登录 - 流程图。
轻松筹是全国 1.6 亿人使用的全民众筹平台,我们的账号系统为平台所有用户提供着服务。账号系统其实还包含很多方面,目前还有很多不足的地方,轻松筹还在一直在用户体验,安全性,稳定性等方面持续改进。
张阳阳,轻松筹 Golang 资深工程师,高可用、高并发和分布式领域多年架构经验,曾在 360、滴滴出行工作。
点击下方图片即可阅读
聊聊魅族的个性化推荐体系和大数据平台架构
在海量数据不断产生的时代,基于数据分析和精准推荐的计算平台可以帮助企业实现更高的数据利用价值,同时,算法水平的提升,机器学习模型的使用,都在给大数据利用添砖加瓦。在 7 月 7 日晚上举办的魅族技术晚场上,我们将邀请来自腾讯、百度、eBay 和魅族的技术专家,围绕大数据平台架构、推荐系统、图像识别、机器学习和搜索基础架构话题,带领现场的技术学习爱好者展开深入讨论,帮助大家提供技术解决方案新思路。点击“阅读原文”,免费报名。