记一个社交APP的开发过程——用户身份认证与在线标记
网站能做到自动登录,通常是因为有一个能记录token的cookie,在web app拦截请求的过滤器里面读取token的值然后进行验证,另外这个cookie会有一个过期时间,一星期一个月或者用户自己手动选择,不过现在越来越多的网站貌似已经不在去设置这个cookie过期时间了,一直会有效的样子,从产品的角度确实是方便用户,但是从安全的角度有一定的隐患,绝大多数时候增加了安全性就要失去一些产品上的方便,反之亦然。
对于移动应用,没有cookie,一般都是登录的时候服务端给客户端返回一个token,之后的每次请求,客户端都要在请求的URL上将token做为参数传递给服务端,以此来识别身份,比如GET /api/v1/timeline?token=xxxxxxxxxx, 另外对于移动应用,这个token往往没有过期这一说,除非用户手动的退出应用。
服务端怎么去把这个token跟具体哪个用户对应起来?通常有两种方式,一是将所有用户信息比如用户ID、token创建时间等通过某种可以反解的加密算法和一个密钥加密起来,把加密字符串传递给客户端,当客户端把token字符串传递过来的时候,再通过密钥去解开然后活动相关的用户信息;第二种方式是根据用户的唯一特征在加一些随机的干扰,生成一个不可逆的hash,然后服务端会保存着用户ID到这个hash的映射关系,比如像一个这样的表结构:
CREATE TABLE `user_token` ( `token` varchar(32) NOT NULL, `user_id` int(11) NOT NULL, `created_on` datetime NOT NULL, PRIMARY KEY (`token`), UNIQUE (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
一是为了安全(全局的密钥太危险了),二是为了能对发布出去的token做到很好的控制,我这里选择的是第二种实现方式,但第二种方式最大的问题就是每次验证用户身份的时候都要去读库,因此在前面加一个Redis,Redis的缓存过期时间为一天,先去Redis里面查找,如果Redis里面没有那么再去库里面查找。这些逻辑都是在web.py的一个processor中进行的,代码如下:
def checktoken(handler, *args):
if web.ctx.path in _no_token_urls:
return handler(*args)
token = form(True, 'token')
user_id = check_token(token) if token else None
if user_id:
mark_online(user_id)
web.ctx.user_id, web.ctx.token = user_id, token
return handler(*args)
else:
return output_json({"status":"ERROR",
"error":{"code":"invalid token"}, "result":{}})
_no_token_urls是一个集合,里面包含那些不需要token就可以使用的功能,比如注册、登录,另外web.py提供一个非常给力的工具就是web.ctx,它是一个记录请求线程上下文的参数,线程之间不会干扰,从token获取的用户ID就可以放到web.ctx里面,供后续逻辑使用。
用户在线标记
对于基于socket的应用,标记哪些用户在线哪些用户不在线可能根本就不是问题,但是对于这种基于Http无状态的应用,要标记是否在线就要费些周折了,好在这年头神器和资源众多,参考了一些文章,比如http://flask.pocoo.org/snippets/71/ 然后没写多少代码就把这个问题搞定了。
大体的思路都是这样的,规定一个用户活跃时间段,比如5min, 如果用户在5min之内没有任何活动(请求),那么就算他已经不在线了,把每一分钟的活跃用户保存到一个集合,比如Redis的Set,当获取在线用户的时候,把最近5分钟的set union一下得到的用户id就是在线用户了。
为了节约内存的使用,我这里没有使用redis的set,而是使用了redis的bitmap去做标记,每个bit代表一个用户,bit的索引是用户的id,bit值为0的时候说明用户不在线,bit值为1的时候说明用户在线。
def mark_online(user_id):
now = int(time.time())
expires = now + (_MAX_ACTIVE_TIME * 60) + 10
user_online_key = rds.keys.user_online.key(now // 60)
p = rds.default.pipeline()
p.setbit(user_online_key, user_id, 1)
p.expireat(user_online_key, expires)
p.execute()
redis API对bitmap的操作已经做的非常友好,可以使用setbit操作来直接设置某个位的值,0还是1,_MAX_ACTIVE_TIME是我们规定的活跃时间段,5min,这个函数也是每次在web.py的processor中调用。
那么怎么获取数据呢?比如我要得到我的在线好友列表?
def query_online(*user_id_list):
current = int(time.time()) // 60
minutes = xrange(_MAX_ACTIVE_TIME)
keys = [rds.keys.user_online.key(current - x)
for x in minutes]
online_data_lst = rds.default.mget(*keys)
online_users = set()
for online_data in online_data_lst:
if not online_data:
continue
a = bitarray()
a.frombytes(online_data)
a_len = len(a)
online_users.update({user_id
for user_id in user_id_list if user_id < a_len and a[user_id]})
return online_users
首先通过一个列表表达式生成最近5分钟的redis key列表,然后mget从redis批量得到它们的值,这里要同二进制位打交道,需要把redis的bitmap转换为Python中的数据结构,而且要做到同样的高效和紧凑,找到了一个不错的Python库叫bitarray, https://pypi.python.org/pypi/bitarray/,把redis的bitmap转换为python的bitarray,剩下的事情就好办多了,直接拿user_id取bitarray对应索引的值,就可以知道哪些用户最近5min有活跃过(在线)