Skip to content

2026-08-30 · rbac-system 登录链路里 Redis 的三种用法:防爆破计数、会话登记、权限缓存。

Redis 实战(以 rbac-system 登录链路为例)

1. 解决什么问题

登录链路里 Redis 干三件事,对应我们在 debug 时看到的三处代码:

位置Redis 干的活数据结构key
AuthService.isLoginLocked()防爆破:记连续输错次数String 计数login:fail:{用户名}
OnlineSessionServiceImpl.createSession()会话登记(在线用户/能登出)Hashlogin:token:{jti}
PermCacheServiceImpl.cachePerms()权限缓存(减少查库)Setauth:perms:{userId}

2. 环境:启动本机 Redis

Windows 用 scoop 装的 Redis,带密码配置文件启动:

bash
cd C:/Users/zgqin/scoop/apps/redis/current && redis-server redis.conf

(桌面有「启动Redis.vbs」可无窗启动。本机密码 rbac@redis789,记在 redis.confrequirepass

验证:

bash
redis-cli -a rbac@redis789 ping   # 返回 PONG 即正常(没有 -a 密码会报 NOAUTH)

3. 依赖与配置

pom.xml:

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

application.yaml(注意是 spring.data.redis.*spring.redis.* 是旧前缀不生效):

yaml
spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: rbac@redis789
      timeout: 3s

timeout 3s 很重要:Redis 挂了时接口快速失败返回提示,不挂死。

4. 用法一:防爆破计数(登录第一步)

AuthService 里的三段代码,用 StringRedisTemplate 做原子计数:

java
// 登录前判断是否锁定
private boolean isLoginLocked(String username) {
    String count = redisTemplate.opsForValue().get(LOGIN_FAIL_KEY + username);
    return count != null && Integer.parseInt(count) >= LOGIN_MAX_FAILS;
}

// 失败一次:INCR 自增,首次设置 15 分钟 TTL
private void recordLoginFail(String username) {
    Long fails = redisTemplate.opsForValue().increment(LOGIN_FAIL_KEY + username);
    if (fails != null && fails == 1L) {
        redisTemplate.expire(LOGIN_FAIL_KEY + username, Duration.ofMinutes(15));
    }
}

// 登录成功:删 key,连续失败链条终结
private void clearLoginFail(String username) {
    redisTemplate.delete(LOGIN_FAIL_KEY + username);
}

要点:

  • INCR 是原子的,并发安全;达到 5 次即锁定,TTL 到期自动解锁
  • Redis 异常时降级try-catch (DataAccessException) 包住,Redis 挂了不阻断登录,只丢防爆破

5. 用法二:会话登记(登录成功后)

OnlineSessionServiceImpl,登录成功把会话写进 Redis hash,TTL 24h:

java
@RequiredArgsConstructor
@Service
public class OnlineSessionServiceImpl implements OnlineSessionService {

    private final StringRedisTemplate redisTemplate;

    @Override
    public void createSession(String jti, Long userId, String username, ...) {
        String key = KEY_PREFIX + jti;   // login:token:{jti}
        Map<String, String> m = new HashMap<>();
        m.put("userId", String.valueOf(userId));
        m.put("username", username);
        // ... 昵称/组织/IP/登录时间等
        redisTemplate.opsForHash().putAll(key, m);
        redisTemplate.expire(key, Duration.ofSeconds(24 * 3600));
    }
}

这个 key 是三个功能的地基:

  • 在线用户列表:SCAN 前缀收集
  • 登出/强退redisTemplate.delete(key) 删掉 → 该会话下次请求 401
  • 权限变更即时生效:删该用户会话 → 强制重登拿新权限

6. 用法三:权限缓存(登录后预热)

PermCacheServiceImpl,用 Set 存权限点编码,读时只做 contains 判断:

java
public void cachePerms(Long userId, Collection<String> pointCodes) {
    String key = KEY_PREFIX + userId;   // auth:perms:{userId}
    redisTemplate.delete(key);
    if (!CollectionUtils.isEmpty(pointCodes)) {
        redisTemplate.opsForSet().add(key, pointCodes.toArray(new String[0]));
    }
    redisTemplate.expire(key, Duration.ofHours(12));   // 12h 兜底
}

public Set<String> getPerms(Long userId) {
    if (!Boolean.TRUE.equals(redisTemplate.hasKey(key))) return null;
    return redisTemplate.opsForSet().members(key);
}

要点:

  • 用 Set 不用 String(JSON):判断"有没有某权限"直接 SISMEMBER,不用反序列化
  • TTL 只是兜底,正常走"变更即删":改权限后 evict(userId) 删 key,下次请求查库回填,即"权限变更即时生效"
  • Redis 异常返回 null → 拦截器走 DB 查询兜底,鉴权不中断

7. 复刻时记住的三条规则

  1. key 设计业务:资源:{id} 前缀式,如 login:token:{jti} —— 同类 key 好 SCAN、好排查
  2. Redis 必须能挂:所有读写包 try-catch (DataAccessException),Redis 挂了降级(放行/查库),不能拖垮主流程
  3. SCAN 不用 KEYSkeys * 会阻塞单线程 Redis,用 SCAN 游标分批扫

8. 常见坑

现象解法
启动报 Redis 连接失败redis-cli -a 密码 ping 确认 Redis 活着;检查 yaml 前缀是不是 spring.data.redis.*
配置了密码但连不上redis-server 必须带 redis.conf 启动(不带=无密码默认配置)
接口挂起 60s 才返回Lettuce 默认超时太长,yaml 加 timeout: 3s
登出了 token 还能用会话 key 没删(redisTemplate.delete(login:token:{jti})),或没做 Redis 会话校验