示例:自动过期的登录会话

在前面的《散列》一章,我们了解到了如何使用散列去构建一个会话程序。正如 12-1 所示,当时的会话程序会使用两个散列分别储存会话的令牌以及过期时间戳。这种做法虽然可行,但是储存过期时间戳需要消耗额外的内存,并且判断会话是否过期也需要用到额外的代码。


图 12-1 会话程序创建的散列数据结构_images/IMAGE_SESSION_HASHS.png


在学习了 Redis 的自动过期特性之后,我们可以对会话程序进行修改,通过给会话令牌设置过期时间来让它在指定的时间之后自动被移除。这样一来,程序只需要检查会话令牌是否存在,就能够知道是否应该让用户重新登录了。

代码清单 12-4 展示了修改之后的会话程序。因为 Redis 的自动过期特性只能对整个键使用,所以这个程序使用了字符串而不是散列来储存会话令牌,但总的来说,这个程序的逻辑跟之前的会话程序的逻辑基本相同。不过由于新程序无需手动检查会话是否过期,所以它的逻辑简洁了不少。


代码清单 12-4 带有自动过期特性的会话程序:/expire/login_session.py

  1. import random
  2. from hashlib import sha256
  3.  
  4. # 会话的默认过期时间
  5. DEFAULT_TIMEOUT = 3600*24*30 # 一个月
  6.  
  7. # 会话状态
  8. SESSION_NOT_LOGIN_OR_EXPIRED = "SESSION_NOT_LOGIN_OR_EXPIRED"
  9. SESSION_TOKEN_CORRECT = "SESSION_TOKEN_CORRECT"
  10. SESSION_TOKEN_INCORRECT = "SESSION_TOKEN_INCORRECT"
  11.  
  12. def generate_token():
  13. """
  14. 生成一个随机的会话令牌。
  15. """
  16. random_string = str(random.getrandbits(256)).encode('utf-8')
  17. return sha256(random_string).hexdigest()
  18.  
  19.  
  20. class LoginSession:
  21.  
  22. def __init__(self, client, user_id):
  23. self.client = client
  24. self.user_id = user_id
  25. self.key = "user::{0}::token".format(user_id)
  26.  
  27. def create(self, timeout=DEFAULT_TIMEOUT):
  28. """
  29. 创建新的登录会话并返回会话令牌,
  30. 可选的 timeout 参数用于指定会话的过期时间(以秒为单位)。
  31. """
  32. # 生成会话令牌
  33. token = generate_token()
  34. # 储存令牌,并为其设置过期时间
  35. self.client.set(self.key, token, ex=timeout)
  36. # 返回令牌
  37. return token
  38.  
  39. def validate(self, input_token):
  40. """
  41. 根据给定的令牌验证用户身份。
  42. 这个方法有三个可能的返回值,分别对应三种不同情况:
  43. 1. SESSION_NOT_LOGIN_OR_EXPIRED —— 用户尚未登录或者令牌已过期
  44. 2. SESSION_TOKEN_CORRECT —— 用户已登录,并且给定令牌与用户令牌相匹配
  45. 3. SESSION_TOKEN_INCORRECT —— 用户已登录,但给定令牌与用户令牌不匹配
  46. """
  47. # 获取用户令牌
  48. user_token = self.client.get(self.key)
  49. # 令牌不存在
  50. if user_token is None:
  51. return SESSION_NOT_LOGIN_OR_EXPIRED
  52. # 令牌存在并且未过期,那么检查它与给定令牌是否一致
  53. if input_token == user_token:
  54. return SESSION_TOKEN_CORRECT
  55. else:
  56. return SESSION_TOKEN_INCORRECT
  57.  
  58. def destroy(self):
  59. """
  60. 销毁会话。
  61. """
  62. self.client.delete(self.key)

以下代码展示了这个会话程序的基本使用方法:

  1. >>> from redis import Redis
  2. >>> from login_session import LoginSession
  3. >>> client = Redis(decode_responses=True)
  4. >>> uid = "peter"
  5. >>> session = LoginSession(client, uid) # 创建会话
  6. >>> token = session.create() # 创建令牌
  7. >>> token
  8. '89e77eb856a3383bb8718286802d32f6d40e135c08dedcccd143a5e8ba335d44'
  9. >>> session.validate("wrong token") # 验证令牌
  10. 'SESSION_TOKEN_INCORRECT'
  11. >>> session.validate(token)
  12. 'SESSION_TOKEN_CORRECT'
  13. >>> session.destroy() # 销毁令牌
  14. >>> session.validate(token) # 令牌已不存在
  15. 'SESSION_NOT_LOGIN_OR_EXPIRED'

为了演示这个会话程序的自动过期特性,我们可以创建一个有效期非常短的令牌,并在指定的时间后再次尝试验证该令牌:

  1. >>> token = session.create(timeout=3) # 创建有效期为三秒钟的令牌
  2. >>> session.validate(token) # 三秒内访问
  3. 'SESSION_TOKEN_CORRECT'
  4. >>> session.validate(token) # 超过三秒之后,令牌已被自动销毁
  5. 'SESSION_NOT_LOGIN_OR_EXPIRED'