轻松筹是全国1.6亿人使用的全民众筹平台,几乎所有核心业务都依赖于账号系统,账号系统的用户体验,安全性,稳定性直接影响着轻松筹所有业务的运行;
轻松筹的发展非常迅速,已经展开了多条产品线,单点登录的需求愈加强烈;另外由于历史包袱的原因,也遗留了一些问题亟待解决。
本次交流主要与大家分享一下轻松筹账号系统(侧重登录授权服务)的架构设计和改造方案。
历史背景:
由于历史包袱的遗留问题,轻松筹的账号系统(登录授权服务)之前主要存在以下几个方面的不足:
- 由于历史包袱的遗留问题,三端登录方式不统一
- 产品线增多,单点登录的需求越来越强烈
- 安全性不够
- 容灾能力不够
设计目标:
需要达到以下几个方面的目标
A用户体验:
- 保证只要用户较长时间段(比如30天)内登录过,就不需要重新登录
- 单点登录,在一个站点登录后,另外一个站点就不需要重复登录(用户几乎无感知)
- 采用第三方登录的时候,能使用每一个站点对应的公众号
B安全性:
- token难以伪造,具有一定不可逆性
- token应该有较快的过期机制,避免被人获取token后伪造用户操作
- 被恶意窃取后,具有发现机制
C稳定性:
- token具有自解释性,即自带某些信息,在某些极端恶劣情况下(比如存储服务挂了),依然能提供服务
实现方案
概述
账号系统最核心的功能就是登录授权,总体思路也很简单:
用户登录成功后,服务端会生成token信息,并将其和用户信息关联起来,返回给前端token信息,
前端携带token来访问所有接口,后端再根据前端发过来的token信息标识这是哪个用户的行为
1 token信息的设计说明
这里所说的token信息包括以下几个字段(服务端生成后返回给前端的)
字段
|
意义
|
说明
|
备注
|
---|---|---|---|
access_token | 用户的唯一标识 | 需要验证登录的每一个请求都需要携带,放在http请求头Qsc-Token中 | 默认过期时间2小时 |
token_type | token的类型 | 暂时保留 | 无用 |
| access_token的过期时间 | 前端发起请求的时候可以先用这个时间预先判断一下,减少不必要的请求 | |
refresh_token | 刷新token | 当access_token过期时,用refresh_token去换取新的token信息 | 默认过期时间30天 |
服务端token存储的信息(服务端记录的信息)
字段
|
意义
|
说明
|
备注
|
---|---|---|---|
platform | android,ios,wx-h5,pc-h5等 | 用户区别当前用户是通过什么形式登录的 | 同一个平台只维护一个token |
session_id | 登录后的唯一标识,不重新登录就不会变 | 不会随着access_token和refresh_token的刷新而改变 | 可以用于存储一些和登录关联的数据 |
auth_type | 登录的方式 | 微信h5,微信公众号,微信app,qq,微博,等等 |
服务端存储的数据(key-val存储):
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
- 前端h5("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的前端互斥很好做,加锁就可以了;h5怎么实现前端互斥,后面会介绍)
- 后端刷新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 的互斥,会导致反复失效的情况
2 web-app平台单点登录方案
对于所有产品线的web平台都实现单点登录SSO(Single Sign On)的功能,这样只要在一个产品线上(比如站点A)登录了,在其它产品线上(比如站点B)就不需要再重新登陆了
实现目标A2,A3

- 如果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重新获取,这也是用户体验上的 考虑
3 native-app平台登录方案
对于所有产品线的ios,android等平台暂时不考虑单点登录的功能,但是以后会考虑(比如一个app唤起另外一个app获取登录token信息)
整体方案与web-app几乎一致,但是有一些特殊性:
- 对于 ios,android,wxapp小程序,需要独立开发sdk,然后各个站点统一使用同一套sdk
- 这个sdk应该包括ui的展现和后端的交互逻辑,统一开发,方便维护
- 各平台保证ui风格统一
这里不作过多描述
接下来 介绍一下 具体的登录流程:
目前登录授权支持两种方式
1. 通过手机验证码登录
2.通过第三方平台登录(新用户需要绑定手机号,即再走一遍第一步
4 手机验证码登录
这里仅存在于passport前端和后端交互
流程图:
1. 为了防止刷短信的问题,增加了图片验证码的校验,但是考虑用户体验,仅仅只是在怀疑对方是恶意操作的时候
2. 为了防止短信验证码的暴力破解,做了一些错误次数的校验
5 第三方登录(微信,微博,qq等)

总结
轻松筹是全国1.6亿人使用的全民众筹平台,我们的账号系统为平台所有用户提供着服务。
以上 就是今天所有的分享,感谢大家抽出宝贵的时间一起交流
欢迎大家提出宝贵的意见。