这篇文章主要是在新开发一个app的过程,对于注册、登录、校验这一块的总结(遇到的一些问题和个人的一些思考),期望能将此经历转为有效的经验。
一、认知
每个app基本上都会有注册、登录,它属于一个基础功能;
注册登录,当前大部份的app里面,一般有三种:
一是用帐号密码登录;
二是手机号加短信验证码登录;
三是第三方超级app授权登录。
在开发一个新的app的过程中,较多被采用的,是二、三种方式相结合的方式。
原因无非有三个:
第一种方式,注册新用户时,需要用户填入太多的信息,必定会造成流失
用手机号加验证码登录,方便又快捷,且收集到了用户手机,可以为用户的帐号安全或是app的业务推广做准备(当然前提是确保用户的隐私不在我们这里泄漏出去)
基于第三方超级app的方式,源于这些超级app用户量具大,一键授权登录方便又安全,可以提高自己app的注册用户量
二、背景
新app中不需复杂的权限控制,不涉及到【用户】、【角色】、【操作】 三者之间的复杂关系,只侧重于接口层的安全校验;
三、需要思考的问题
帐号绑定的问题
帐号换绑的问题
帐号合并的问题
分布式环境下登录态一致的问题
续签和客户端并发场景下token的处理机制
token的安全性问题
token的重放攻击应对策略
四、解决问题的思路
帐号绑定的问题:
不管是采用哪种方式登录的,都将他们归为第三方登录,只是类型不同而已,例如:手机号的方式定义类型为1,支付宝的方式定义为2....。数据模型上,本地的帐号(例如:即时号)与第三方授权后得到的第三方帐号之间是一种一对多的关系。
登录后,不管再去绑定什么第三方帐号的登录方式,app内自建的帐号是不会改变的,需要改变的只是增加一种关系而已。即然本地帐号不会改变,后面基于新加的第三方帐号去登录,也就可以查对应的同一个本地帐号所有数据。(有一前提条件是:这个新绑定的第三方帐号,还没有绑定过任何一个本地帐号)
帐号换绑的问题:
其实也是帐号绑定的问题,只不过,需要做一个替换的操作,什么时候去做这个替换操作,那就是必须要与用户完成一次交互,才认可。比如:新号还没有绑定过,往新的手机号上发短信验证码,校验短信验证码
帐号合并的问题:
这种需求,我只能说能尽量避免就避免,因为帐号合并,会涉及到两个帐号产生的历史数据合并的问题,而且一旦又涉及分库、分表了就变得更加的复杂了。实在避免不了的话,那就需要考虑再增加中间关系表来处理了(太过复杂)
分布式环境下登录态一致的问题:
在pc web站点里面,采用比较多的方案是基于session 、cookie的方式,简单的理解是,服务端生成用户的session对象,并将sessionId,通过set-cookie设置到响应中,返回给浏览器,由浏览器存储在对应域名下的本地cookie文件中,后面再发起请求时,浏览器自动将这些cookie设置到请求中,发送给服务器,服务器从cookie中获取到sessionId,再得到对应的session对象信息和关联的用户信息。 这种方式又会引出一个新的问题,那就是sessiond信息的同步问题,这个问题,特别是在分布式的系统下比较明显,如果不处理好同步,可能会出现的一种情况是:用户在服务器A机器登录了,但请求到服务器B机器时,又提示没有登录,需要重登。解决方案有很多,一般是通过session的集中存储来实现。 在我们的app项目中,我们不采用session、cookie的方案,而使用一种token认证的方式,原理上有相同之处,但更好的适应我们这类app的产品。使用这种方式又会面临什么样的问题,做一些怎样的取舍,在下面一节会详细说明。
续签和客户端app并发场景下token的处理机制
在一个app中,内部必定在存多个线程与服务端进行交互,如果其中一个线程在与服务器交互的过程中token是合法,但下一个时刻就过期了,此时如果端的另一个请求到达的时候,难道还提示他token不合法,这种情况应该是需要避免的,也就是正在使用app的用户,理论不应该出现他上一秒内还能使用,而下一秒钟却提示他非法的情况。为避免这种情况,必须端与服务器在设计方案上进行配合,简单描述解决方案是这样的:在端请求服务器接口,触发到需要校验token时,服务器检测token是否快过期,如果快过期则生成一个新的token并返回给端(新token的有效期比较长,相当于续签),但与此同时旧的token仍然保留一小段时间的有效期(例如5分钟),端在收到新的token后,将其广播通知到端中各个模块token有更新,下一次需要用新的来与服务器交互。如果端正在用旧的token发出了请求,到达服务器时,服务器会首先用新的token去验证合法性,不通过时,会再去用旧的token校验,能通过,这样的话用户是无感知的。而且在这一小段时间内,端上各个模块肯定都能接收完有新的token这一事件。
token的安全性问题 还是一句老话:没有绝对的安全,当付出的成本远大于收益时,我们可以认为是安全的。但是基本的安全还是有必要的,如:https传输、token中不包含用户敏感信息、密钥安全等
token的重放攻击应对策略 服务端可以结合客户端设备id和ip的在一段时间内的请求数,做相应的处理; 或者通过加随机数,加时间戳的方式来处理。
五、token详解
参考JWT,采用自定义的token来实现。JWT是 Json Web Token 的缩写。它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧 和 自包含 的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。 JWT,详见(https://jwt.io/)。
取舍问题
是不是使用JWT就可以解决所有问题了,其实又会引入新的问题,面临这些问题,又该如何取舍,面临的问题如下:
jwt是提倡token每次请求都改变的
改变的话 : 客户端多线程并发请求, 会导致第一个到达的请求可以, 后边的请求就token失败,又会要求用户重新登录,试想一下,进在玩着app,突然提示要登录?用户心里肯定是一万只“草泥马”奔腾而过。
不变的话 :1)程序无法控制token的过期时间 只能等token过期后 用户在重新登录(这个过程中即使用户频繁请求接口也无法避免)2). token不变化就会导致重放攻击 不变的token 跟sessionID 基本没什么区别
jwt提倡将信息放在token第2段,第3段通过签名实现客户端无法恶意改token信息,但是token第2段是可以直接 base64_decode 拿到明文信息, 这样token泄漏就等于用户信息泄漏
用户信息放在token里面 当用户信息改变的话 就要 重新生成token ,修改用户信息是通过客户端请求触发的 接口可以生成新的token发给客户端,但 如果 这个修改是从后台操作的 这个新生成的token如何给到客户端。jwt中的一些实现是每次token解出来后去查寻数据库来更新用户信息,但是每次接口都去查用户信息的开销太大 还不如sessionID + redis 的结合
用户退出登录的时候 token并未过期 如果服务端不处理将token(例如加入黑名单token列表中), 即使退出登录他的token还是可以使用的,这样的不方便还不如sessionID
上面这些问题的根源,其实是把jwt想得太过复杂了,在这里我们使用jwt的目的,是用来做安全校验,而不是做系统权限的管理。对于安全性要求非常高的,可考虑使用另外的方案。例如:结合spring security等框架
针对问题1: 登录后的每次请求,token不经常变,设计上我们可以有两个token,一个长久的(例如一个月有效),一个临时的(例如5分钟有效),首次生成时生成一个长久的,在快过期时,我们将会生成一个新的token,但将这个旧的转换为临时的token,只返回新的token给前端,由前端自己决定,什么时候将本地的token全部替换为新的,并广播出去,通知客户端各模块有的token来了(这个在上面我已经提到过); 至于程序无法控制token的过期,这点我们通过将token持久化后,就可以解决,例如,通过运营后台,找到某个uid对应的token,将其至为无效即可;token在有效期内不变就会导致双重攻击?的确,但是你变了也没有办法抵御啊 ,别人可以拿到你新生成的token再继续请求啊,所以要靠其他方式实现,比如每次请求的时候带上客户端的标识(如:设备id),然后拿到用户的ip,在后端做判断,如果这个标识的用户在这个ip一分钟请求的次数到了上限,就判定为恶意请求,可以禁用ip. 禁用设备,其实这个和token没多大关系,要自己来做防御的。
针对问题2:token不适合放重要敏感信息,适合放用户唯一标识,如uid,服务端拿到唯一标识后去数据库或者缓存拿用户数据;
针对问题3:由于token中本身就不存用户的详细信息,只有uid,修改了用户信息,就更新用户信息对应的表数据和缓存数据,和token并没有关系。token有缓存,token也执久化到db,用户有缓存,用户也持久化到db.将token的缓存时间设置为1天或2天,没有时再去db查,并更新回缓存。可以预见的对token的查询大部份都会由缓存处理掉,例如,是否存在该token,是否已过期,是否与对应的设备id相匹配。
针对问题4:用户在客户端退出,其实就是把本地存储的token给删除了而已,服务端是不用管的,这个不影响什么,对于普通用户来说,的确是实现了登录一样的效果。对于心怀恶意的人来说,由于退出时删除了本地的token,所以他是没有办法知道别人token的,所以不会对服务端造成任何损失.
总之
就是利用了jwt的数据结构特性、但结合持久化、考虑与端的交互场景合理的进行设计。实现一个简单、有效、方便可扩展的基于api接口安全校验的注册与登录。