基于redis实现同时允许最多N台设备登录

1,310 阅读1分钟

背景:

一个uid最多允许N台设备同时在线,token有2小时的有效期。

如果有新设备登录,则删除最近将要过期的key,来替换它。

实现:

默认token的key为: fmt.Sprintf(jwt_%s_%s, uid, deviceId)。这里deviceId我们是使用 fingerprintjs 获取的。

当我们调用登录接口后,通过了用户名密码等验证,首先从数据源获取这个uid最多允许几台设备同时登录。

// 从MySQL检查最多允许几台设备
maxCount, err := db.GetMaxDeviceCount(ctx, uid)
if err != nil {
	return err
}

接着我们从redis获取这个用户目前有几个有效可用的token。注意使用 SCAN 命令(KEYS 命令我们在生产环境禁用,原因可见 redis.io/commands/ke… 中的 Warning 段落)。

这里为了简单还是用KEYS为例:

keys, err := rdb.Keys(ctx, fmt.Sprintf("jwt_%s_*", uid).Result()
if err != nil {
	return err
}

我们对比现在redis中的token数量和最大允许的数量,会发现有以下两种情况:

  • 如果 len(keys) < maxCount , 那我们直接set新的token,带上ttl就行。
  • 否则我们要从redis中删除 len(keys) - maxCount + 1 条,然后再保存新的token。

第一种情况:

len(keys) < maxCount:

return rdb.Set(ctx, fmt.Sprintf("jwt_%s_%s", uid, deviceId), token, 2*time.Hour).Err()

第二种情况更加复杂,我们可能会遇到现有的token中包含当前deviceId的,所以可以不用删除直接更新它。

更加极端的情况下,我们可能之前允许这个用户最多登录5台,redis中有5个这个uid的token,现在我们只允许最多登录3台了。

len(keys) == maxCount:

if len(keys) == maxCount {
	for idx := range keys {
		if keys[idx] == fmt.Sprintf("jwt_%s_%s", uid, deviceId) {
					return rdb.Set(ctx, fmt.Sprintfjwt_%s_%s", uid, deviceId), token, 2*time.Hour).Err()
		}
	}
}

len(keys) > maxCount:

// 需要删除ttl最小的
toBeDeletedCount := len(keys) - maxCount + 1

keysTTLmap := make(map[string]float64)

for idx := range keys {
	ttl, err := rdb.TTL(ctx, keys[idx]).Result()
	if err != nil {
		return err
	}
	keysTTLmap[keys[idx]] = ttl.Seconds()
}

ranked := rankKeyByTTLAsc(keysTTLmap)

for idx := range ranked {
	err = rdb.Del(ctx, ranked[idx]).Err()
	if err != nil {
		return err
	}

	toBeDeletedCount--

	if toBeDeletedCount == 0 {
		break
	}
}

return rdb.Set(ctx, fmt.Sprintf("jwt_%s_%s", uid, deviceId), token, 2*time.Hour).Err()

辅助函数: 将key按照过期时间排序,最近要过期的在最前面。

func rankKeyByTTLAsc(values map[string]float64) []string {
	type kv struct {
		Key   string
		Value float64
	}
	var ss []kv
	for k, v := range values {
		ss = append(ss, kv{k, v})
	}
	sort.Slice(ss, func(i, j int) bool {
		return ss[i].Value < ss[j].Value
	})
	ranked := make([]string, len(values))
	for i, kv := range ss {
		ranked[i] = kv.Key
	}
	return ranked
}