之前设计一个采用 OAuth 鉴权的 RESTFul API Web Service,用户的 Access Token 和 Refresh Token 均缓存在 Redis 中,因为 API 资源受访问较频繁,且 OAuth 令牌不需要串行化保存,即使宕机重启用户也只需重新认证获取新令牌。

令牌保存

发放令牌时,所需字段除了 token 本身和 token_type 外,还需要包括 OAuth 的 user_idclient_id,以确定 permission。以及一个 expires 过期时间,虽然 Redis 可以通过 EXPIRE 设置 TTL,但保留该字段可以方便服务器验证一些其他信息。
保存可以用 Hash 实现,示例:

#!/usr/local/bin/env python
import redis

expire_time = 3600    # access_token 缺省过期时间
rdb = redis.Redis()

rdb.hmset(\"token:aTpFBmc8uq5PqtxFWSYiaYw61rhvjQbhb1cEZk34XWM\", \\
{\"user_id\": 1927, \"client_id\": \"web_admin\", \\
\"token_type\": \"Access\", \\
\"expires\": (rdb.time()[0] + expire_time)})
rdb.expire(\"token:aTpFBmc8uq5PqtxFWSYiaYw61rhvjQbhb1cEZk34XWM\", expire_time)
rdb.hmset(\"token:P3wk6F4jdAafuUgVSoaRUZ6HK3Z4yl4qVMrRf5l1PZU\", \\
{\"user_id\": 3154, \"client_id\": \"ios_app_v1\", \\
\"token_type\": \"Refresh\", \\
\"expires\": (rdb.time()[0] + expire_time * 24)})
rdb.expire(\"token:P3wk6F4jdAafuUgVSoaRUZ6HK3Z4yl4qVMrRf5l1PZU\", expire_time * 24)

鉴权

客户端发起一个请求时,在 Request Headers 中附带 Authorization 字段,附上 Bearer 承载令牌(仅用于身份认证):

Request Headers
……
Authorization: Bearer aTpFBmc8uq5PqtxFWSYiaYw61rhvjQbhb1cEZk34XWM
……

服务器收到请求后在 Redis 中查找该令牌,后端根据返回的查找结果做后续处理。

回收策略

考虑到用户可能在不同设备上请求令牌,不应严格限制单一用户单一时间内生成的令牌数,但仍需限制内存防止内存耗尽,在 redis.conf 中配置:

# 限制最大内存使用量为 1024MB,设为 0 则是无限制,但在 32 位系统中限制为 3GB
maxmemory 1024mb

若在服务器内存有限的情况下,需要有 Redis 的 LRU(Least Recently Used) 算法来进行内存回收。Redis 官方提供六种可用回收策略,其中对设置了 EXPIRE 的键适用的有三种:

  • volatile-lru: 从设置了 EXPIRE 的键中尽可能选择最近最少使用的键进行清除,供新数据缓存
  • volatile-random: 从设置了 EXPIRE 的键中随机选择键进行清除,供新数据缓存
  • volatile-ttl: 从设置了 EXPIRE 的键中尽可能选择那些 TTL 最短的键清除,供新数据缓存

从适用且不影响用户体验来看,鉴权系统使用 volatile-lru 或 volatile-ttl 较好,前者可以让较少访问的用户需要及时更新令牌,后者是优先清除最接近过期的用户令牌。在 redis.conf 中配置:

maxmemory-policy volatile-ttl

值得注意的是,Redis 提供的 LRU 算法并不严格,而是一个近似的 LRU 算法,Redis 不使用严格的 LRU 算法是因为这样太消耗内存。Redis 的回收策略并非选择最优的候选键清除,而是抽取少量键作为一组样本,从中选择最优的候选键清除。该算法在 Redis 3.0 后得到了改善,使得结果更接近于严格的 LRU 算法。算法的精度取决于配置的每组样本数,可以在 redis.conf 中配置:

maxmemory-samples 5

以下是 Redis 官方的近似 LRU 算法和真正的 LRU 算法的对比图:

LRU_Comparison


白色区域是被清除的数据,灰色区域是未被清除的数据,绿色区域是添加的数据。