相关知识
大多数网站都会使用 cookie
记录用户的身份。cookie
是由少量数据组成的字符串(通常还要经过加密)。网站会要求浏览器存储这些数据,并在向服务端发起请求时将这些数据传回给服务端。
通常,用于处理登录(识别用户身份)的 cookie
分为两种:
- 签名式
cookie
- 存储包含用户
ID
等可直接识别用户的信息 - 附加一个签名,核对
cookie
信息是否被恶意篡改
- 存储包含用户
- 令牌式
cookie
- 存储一个随机字符串(令牌)
- 通过在服务端的数据库中查找随机字符串和用户的对应关系识别用户身份
这两种 cookie
各有优缺,我们可以通过一个表格对比两者的优缺:
类型 | 优点 | 缺点 |
---|---|---|
签名式 cookie | 直接存储用户信息,方便验证用户身份;可以包含额外信息;对 cookie 进行签名较简单 | 遗漏签名会导致安全漏洞,加密方法不当会泄露用户敏感信息 |
令牌式 cookie | cookie 体积小,可加快通信速度 | 需要使用数据库存储令牌,会造成额外开销 |
为了避免安全漏洞,本关卡中,我们使用令牌式 cookie
。
这篇文章将教会你掌握:1
.如何核对令牌,2
.如何更新令牌,3
.如何定期清理无用信息。
如何核对令牌
前面提到了令牌式 cookie
的最大缺点就是会造成额外开销。大多数关系型数据库在每台数据库服务器上只能插入、更新或删除 200 - 2000
个数据行/秒。当网站的负载变高时,数据库就成为了瓶颈。所以,我们需要使用 Redis
取代关系型数据库,存放令牌。
首先,我们约定令牌的存储方式。我们使用一个哈希存储登录令牌与用户的映射关系,其中:
- 哈希键名为
login
- 登录令牌作为域
- 用户
ID
作为值
所以核对令牌就变得十分简单,方法如下:
import redis
conn = redis.Redis()
def check_token(token):
return conn.hget('login', token)
我们使用 hget()
方法从 Redis
中取出并返回令牌对应的用户 ID
,而**当令牌不存在时,该方法则会返回 None
**,从而达到核对检查的效果。
如何更新令牌
更新令牌需要做两个工作:
- 记录用户令牌
- 记录令牌生成时间
由于令牌存在被人窃取的可能,所以我们不允许令牌永不过期。通过记录令牌生成的时间戳,我们可以通过定期清理的方式,清理掉一定时间前生成(过老)的令牌,从而实现令牌的时限性,在一定程度上也减少了 Redis
的存储量,避免内存过高消耗。
使用有序集合记录令牌生成时间能让我们更便捷的根据时间戳对令牌进行排序,然后再对一定时间前生成(过老)的令牌进行删除。将时间戳作为分值,令牌作为成员,记录到有序集合 recent:token
中:
import time
timestamp = time.time() #返回当前时间的时间戳(1970年后经过的浮点秒数)
conn.zadd('recent:token', token, timestamp)
而记录用户令牌可以使用 hset()
方法,将 token
和 user_id
的域-值对关系记录到哈希中:
conn.hset('login', token, user_id)
上述两个操作是相关的,记录了用户令牌则应该也有对应的令牌生成时间,所以我们应该使用事务将两条命令包起来,最后一起提交给 Redis
处理:
def update_token(token, user_id):
timestamp = time.time()
pipe = conn.pipeline()
pipe.hset('login', token, user_id)
pipe.zadd('recent:token', token, timestamp)
pipe.execute()
如何定期清理无用信息
随着登录用户的增多,令牌存储所需的内存也会不断增加,这时,我们需要定期清理过期的令牌数据。
决定令牌的有效时间需要权衡数据安全与用户体验两方面:
- 有效时间过短,则用户需要频繁的输入账户密码登入系统
- 有效时间过长,则令牌泄露的可能性增大,伪造用户身份的可能性也越大
综合上述两个方面的考虑,我们将令牌的有效时间设置为一个星期(86400
秒),在每次清理令牌数据时,我们找到令牌生成时间在一个星期前的数据,并将这些令牌和令牌生成时间数据全部删除。
寻找一个星期前生成的令牌是关键,我们可以使用当前 Unix
时间减去 86400
得到一个星期前的 Unix
时间戳,然后使用有序集合命令 ZRANGEBYSCORE
获取有序集合 recent:token
中所有分值(生成时间)大于等于 0
,小于等于一个星期前的 Unix
时间戳的成员:
one_week_ago_timestamp = time.time() - 86400
exipred_tokens = conn.zrangebyscore('recent:token', 0, one_week_ago_timestamp)
接下来,我们要从有序集合 recent:token
中删除掉 expired_tokens
的所有成员,以及从哈希 login
中移除所有过期的 token
域。
为了减少客户端与 Redis
之间的通信次数,我们可以直接使用 ZREMRANGEBYSCORE
命令移除有序集合 recent:token
中所有分值(生成时间)大于等于 0
,小于等于一个星期前的 Unix
时间戳的成员:
conn.zremrangebyscore('recent:token', 0, one_week_ago_timestamp)
conn.hdel('login', *expired_tokens)
我们使用了 hdel()
方法一次性从哈希 login
中移除了所有过期的 token
域,由于 expired_tokens
是一个数组,而客户端会默认将输入的值转换为字符串,所以我们在这里要使用 *expired_tokens
,以指针的形式调用 expired_tokens
变量,传入多个参数(域)。
让清理方法自动执行
你可以使用守护进程的方式来保证这个方法始终在运行,也可以通过定时任务(
cron job
)每隔一段时间执行一次
因为这些知识超出了本实训的讲解范围,在这里就不再详述。
编程要求
根据提示,在右侧Begin-End
区域补充代码,完成令牌管理的后端处理逻辑:
- 在
check_token(token)
方法中:- 使用
hget()
方法从哈希login
中取出参数token
域的值 - 返回(
return
)上述值
- 使用
- 在
update_token(token, user_id)
方法中:- 参数说明:
token
为令牌
- 参数说明:
user_id
为该令牌对应的用户ID
- 获得当前时间并赋值给
timestamp
- 使用事务提交下列命令:
- 将域
token
与值user_id
对存入哈希键login
中
- 将域
- 获得当前时间并赋值给
- 将成员
token
存入有序集合recent:token
中,分值为timestamp
- 在
clean_tokens()
方法中:- 使用当前时间减去
86400
得到一周前时间戳,并赋值给one_week_ago_timestamp
- 使用
zrangebyscore
方法获取有序集合recent:token
中- 分值大于等于
0
- 分值大于等于
- 使用当前时间减去
- 小于等于
one_week_ago_timestamp
的所有成员 - 并赋值给变量
expired_tokens
- 使用
zremrangebyscore
方法移除有序集合recent:token
中- 分值大于等于
0
- 分值大于等于
- 使用
- 小于等于
one_week_ago_timestamp
的所有成员- 移除哈希
login
中所有与变量expired_tokens
中相同的域- 使用指针形式传入参数
*expired_tokens
- 使用指针形式传入参数
- 移除哈希