SpringBoot + Redis 实现登录校验:分布式会话管理,安全又高效

一、问题背景:为什么需要 Redis 实现登录校验?

在传统的单体应用中,我们通常使用 Session 来管理用户登录状态。但在微服务架构和分布式系统中,Session 面临着诸多挑战:

传统 Session 的痛点

  1. 单点故障:Session 存储在单个服务器上,服务器宕机会导致所有用户登录状态丢失
  2. 无法水平扩展:多台服务器之间无法共享 Session,用户请求可能被路由到不同服务器导致登录失效
  3. 内存压力:大量用户 Session 占用服务器内存,影响性能
  4. 跨域问题:前后端分离架构下,Session 跨域处理复杂

Redis 方案的优势

特性传统 SessionRedis + Token
分布式支持❌ 不支持✅ 天然支持
水平扩展❌ 困难✅ 容易
性能⚠️ 内存受限✅ 独立缓存服务
跨域❌ 复杂✅ 天然支持
安全性⚠️ 一般✅ 可设置过期时间
持久化❌ 易丢失✅ 可持久化

二、核心概念:Token + Redis 认证机制

1. 认证流程

┌─────────────┐     1. 登录请求      ┌─────────────┐
│   客户端     │ ─────────────────> │   服务端     │
│  (浏览器/App)│                    │  (SpringBoot)│
└─────────────┘                    └──────┬──────┘
                                          │
                                          │ 2. 验证用户名密码
                                          │
                                          ▼
                                   ┌─────────────┐
                                   │   数据库     │
                                   └─────────────┘
                                          │
                                          │ 3. 验证成功
                                          │
                                          ▼
                                   ┌─────────────┐
                                   │  生成 Token  │
                                   │  存储到 Redis │
                                   └──────┬──────┘
                                          │
                     4. 返回 Token        │
                                          │
┌─────────────┐ <─────────────────────────┘
│   客户端     │
│  (存储Token) │
└─────────────┘

后续请求:
┌─────────────┐     5. 携带 Token      ┌─────────────┐
│   客户端     │ ───────────────────> │   服务端     │
│  (浏览器/App)│                     │  (SpringBoot)│
└─────────────┘                     └──────┬──────┘
                                           │
                                           │ 6. 从 Redis 验证 Token
                                           │
                                           ▼
                                    ┌─────────────┐
                                    │    Redis    │
                                    └─────────────┘
                                           │
                                           │ 7. Token 有效
                                           │
                                           ▼
                                    ┌─────────────┐
                                    │  返回数据    │
                                    └─────────────┘

2. Token 设计

Token 通常包含以下信息:

  • 用户ID:标识用户身份
  • 签发时间:用于计算过期时间
  • 过期时间:Token 有效期
  • 随机字符串:防止 Token 被猜测

3. Redis 存储结构

# Token 存储
Key: token:{userId}:{token}
Value: {userId, username, loginTime, expireTime}
TTL: 7200 (2小时)

# 用户登录状态
Key: login:{userId}
Value: {token, loginTime, deviceInfo}
TTL: 7200

# 登录设备管理(支持多设备登录)
Key: devices:{userId}
Value: Set<deviceId>

三、实现方案:SpringBoot + Redis

1. 技术栈

  • Spring Boot 2.7.5
  • Spring Data Redis
  • JWT(可选)
  • FastJSON
  • Java 11+

2. 项目结构

springboot-redis-login-demo/
├── src/
│   └── main/
│       ├── java/
│       │   └── com/example/demo/
│       │       ├── config/           # 配置类
│       │       │   ├── RedisConfig.java
│       │       │   └── WebConfig.java
│       │       ├── controller/       # 控制器
│       │       │   └── AuthController.java
│       │       ├── service/          # 服务层
│       │       │   ├── AuthService.java
│       │       │   └── TokenService.java
│       │       ├── interceptor/      # 拦截器
│       │       │   └── AuthInterceptor.java
│       │       ├── annotation/       # 自定义注解
│       │       │   └── RequireAuth.java
│       │       ├── dto/             # 数据传输对象
│       │       │   ├── LoginRequest.java
│       │       │   ├── LoginResponse.java
│       │       │   └── Result.java
│       │       ├── entity/          # 实体类
│       │       │   └── User.java
│       │       ├── repository/      # 数据访问层
│       │       │   └── UserRepository.java
│       │       └── RedisLoginDemoApplication.java
│       └── resources/
│           └── application.yml
├── pom.xml
└── README.md

3. 添加依赖

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- Spring Boot Validation -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <!-- FastJSON -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>2.0.40</version>
    </dependency>

    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- Spring Boot Test -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

4. 配置文件

server:
  port: 8080

spring:
  application:
    name: redis-login-demo

  # Redis 配置
  redis:
    host: localhost
    port: 6379
    password:
    database: 0
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5
        max-wait: 3000ms

# Token 配置
token:
  expire-time: 7200  # Token 过期时间(秒)
  refresh-time: 3600  # Token 刷新时间(秒)
  secret: your-secret-key  # Token 密钥(可选,用于 JWT)

logging:
  level:
    com.example.demo: debug

四、代码实现:完整示例

1. Token 服务

@Service
@Slf4j
public class TokenService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Value("${token.expire-time:7200}")
    private Long tokenExpireTime;

    @Value("${token.refresh-time:3600}")
    private Long tokenRefreshTime;

    /**
     * 生成 Token
     */
    public String generateToken(Long userId, String username) {
        // 生成随机 Token
        String token = UUID.randomUUID().toString().replace("-", "");
        
        // 构建 Token 信息
        TokenInfo tokenInfo = TokenInfo.builder()
                .userId(userId)
                .username(username)
                .token(token)
                .loginTime(System.currentTimeMillis())
                .expireTime(System.currentTimeMillis() + tokenExpireTime * 1000)
                .build();

        // 存储到 Redis
        String tokenKey = getTokenKey(userId, token);
        redisTemplate.opsForValue().set(tokenKey, JSON.toJSONString(tokenInfo), tokenExpireTime, TimeUnit.SECONDS);

        // 存储用户登录状态
        String loginKey = getLoginKey(userId);
        redisTemplate.opsForValue().set(loginKey, token, tokenExpireTime, TimeUnit.SECONDS);

        log.info("Token generated for user: {}, token: {}", username, token);
        return token;
    }

    /**
     * 验证 Token
     */
    public TokenInfo validateToken(String token) {
        if (StringUtils.isBlank(token)) {
            return null;
        }

        // 从 Redis 获取 Token 信息
        String tokenKey = getTokenKeyByToken(token);
        String tokenJson = redisTemplate.opsForValue().get(tokenKey);

        if (StringUtils.isBlank(tokenJson)) {
            log.warn("Token not found or expired: {}", token);
            return null;
        }

        TokenInfo tokenInfo = JSON.parseObject(tokenJson, TokenInfo.class);

        // 检查是否过期
        if (tokenInfo.getExpireTime() < System.currentTimeMillis()) {
            log.warn("Token expired: {}", token);
            // 删除过期的 Token
            redisTemplate.delete(tokenKey);
            return null;
        }

        // 自动续期(如果接近过期时间)
        if (tokenInfo.getExpireTime() - System.currentTimeMillis() < tokenRefreshTime * 1000) {
            refreshToken(tokenInfo);
        }

        return tokenInfo;
    }

    /**
     * 刷新 Token
     */
    public void refreshToken(TokenInfo tokenInfo) {
        tokenInfo.setExpireTime(System.currentTimeMillis() + tokenExpireTime * 1000);
        
        String tokenKey = getTokenKey(tokenInfo.getUserId(), tokenInfo.getToken());
        redisTemplate.opsForValue().set(tokenKey, JSON.toJSONString(tokenInfo), tokenExpireTime, TimeUnit.SECONDS);

        String loginKey = getLoginKey(tokenInfo.getUserId());
        redisTemplate.expire(loginKey, tokenExpireTime, TimeUnit.SECONDS);

        log.info("Token refreshed for user: {}", tokenInfo.getUsername());
    }

    /**
     * 删除 Token(登出)
     */
    public void removeToken(Long userId, String token) {
        String tokenKey = getTokenKey(userId, token);
        String loginKey = getLoginKey(userId);
        
        redisTemplate.delete(tokenKey);
        redisTemplate.delete(loginKey);

        log.info("Token removed for user: {}", userId);
    }

    /**
     * 获取用户所有 Token(支持多设备登录)
     */
    public List<TokenInfo> getUserTokens(Long userId) {
        String pattern = "token:" + userId + ":*";
        Set<String> keys = redisTemplate.keys(pattern);
        
        if (CollectionUtils.isEmpty(keys)) {
            return Collections.emptyList();
        }

        List<String> tokenJsonList = redisTemplate.opsForValue().multiGet(keys);
        return tokenJsonList.stream()
                .filter(StringUtils::isNotBlank)
                .map(json -> JSON.parseObject(json, TokenInfo.class))
                .collect(Collectors.toList());
    }

    private String getTokenKey(Long userId, String token) {
        return "token:" + userId + ":" + token;
    }

    private String getTokenKeyByToken(String token) {
        // 这里可以通过 Scan 或者维护 Token -> UserId 的映射来优化
        Set<String> keys = redisTemplate.keys("token:*:" + token);
        return CollectionUtils.isEmpty(keys) ? null : keys.iterator().next();
    }

    private String getLoginKey(Long userId) {
        return "login:" + userId;
    }

    @Data
    @Builder
    public static class TokenInfo {
        private Long userId;
        private String username;
        private String token;
        private Long loginTime;
        private Long expireTime;
    }

}

2. 认证服务

@Service
@Slf4j
public class AuthService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TokenService tokenService;

    /**
     * 用户登录
     */
    public LoginResponse login(LoginRequest request) {
        // 1. 验证用户名密码
        User user = userRepository.findByUsername(request.getUsername());
        if (user == null) {
            throw new AuthException("用户不存在");
        }

        if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
            throw new AuthException("密码错误");
        }

        // 2. 生成 Token
        String token = tokenService.generateToken(user.getId(), user.getUsername());

        // 3. 返回登录信息
        return LoginResponse.builder()
                .userId(user.getId())
                .username(user.getUsername())
                .token(token)
                .expireTime(tokenService.getTokenExpireTime())
                .build();
    }

    /**
     * 用户登出
     */
    public void logout(Long userId, String token) {
        tokenService.removeToken(userId, token);
        log.info("User logged out: {}", userId);
    }

    /**
     * 获取当前登录用户
     */
    public User getCurrentUser(String token) {
        TokenService.TokenInfo tokenInfo = tokenService.validateToken(token);
        if (tokenInfo == null) {
            throw new AuthException("Token 无效或已过期");
        }
        return userRepository.findById(tokenInfo.getUserId()).orElse(null);
    }

    /**
     * 踢出用户(强制下线)
     */
    public void kickoutUser(Long userId) {
        List<TokenService.TokenInfo> tokens = tokenService.getUserTokens(userId);
        for (TokenService.TokenInfo tokenInfo : tokens) {
            tokenService.removeToken(userId, tokenInfo.getToken());
        }
        log.info("User kicked out: {}", userId);
    }

}

3. 认证拦截器

@Component
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenService tokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 检查是否需要认证
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        RequireAuth requireAuth = handlerMethod.getMethodAnnotation(RequireAuth.class);
        
        // 如果没有 @RequireAuth 注解,则不需要认证
        if (requireAuth == null) {
            requireAuth = handlerMethod.getBeanType().getAnnotation(RequireAuth.class);
        }

        if (requireAuth == null) {
            return true;
        }

        // 2. 获取 Token
        String token = extractToken(request);
        if (StringUtils.isBlank(token)) {
            writeErrorResponse(response, HttpStatus.UNAUTHORIZED, "请先登录");
            return false;
        }

        // 3. 验证 Token
        TokenService.TokenInfo tokenInfo = tokenService.validateToken(token);
        if (tokenInfo == null) {
            writeErrorResponse(response, HttpStatus.UNAUTHORIZED, "Token 无效或已过期");
            return false;
        }

        // 4. 将用户信息存入请求属性
        request.setAttribute("currentUserId", tokenInfo.getUserId());
        request.setAttribute("currentUsername", tokenInfo.getUsername());
        request.setAttribute("token", token);

        log.debug("User authenticated: {}", tokenInfo.getUsername());
        return true;
    }

    private String extractToken(HttpServletRequest request) {
        // 从 Header 获取
        String token = request.getHeader("Authorization");
        if (StringUtils.isNotBlank(token) && token.startsWith("Bearer ")) {
            return token.substring(7);
        }

        // 从 Cookie 获取
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("token".equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }

        // 从 URL 参数获取
        return request.getParameter("token");
    }

    private void writeErrorResponse(HttpServletResponse response, HttpStatus status, String message) throws IOException {
        response.setStatus(status.value());
        response.setContentType("application/json;charset=UTF-8");
        
        Result result = Result.error(message);
        response.getWriter().write(JSON.toJSONString(result));
    }

}

4. 自定义注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireAuth {
    
    /**
     * 是否需要管理员权限
     */
    boolean requireAdmin() default false;

}

5. 认证控制器

@RestController
@RequestMapping("/api/auth")
@Slf4j
public class AuthController {

    @Autowired
    private AuthService authService;

    /**
     * 用户登录
     */
    @PostMapping("/login")
    public Result<LoginResponse> login(@RequestBody @Valid LoginRequest request) {
        log.info("User login: {}", request.getUsername());
        LoginResponse response = authService.login(request);
        return Result.success(response);
    }

    /**
     * 用户登出
     */
    @PostMapping("/logout")
    @RequireAuth
    public Result<String> logout(HttpServletRequest request) {
        Long userId = (Long) request.getAttribute("currentUserId");
        String token = (String) request.getAttribute("token");
        
        authService.logout(userId, token);
        return Result.success("登出成功");
    }

    /**
     * 获取当前用户信息
     */
    @GetMapping("/current")
    @RequireAuth
    public Result<User> getCurrentUser(HttpServletRequest request) {
        String token = (String) request.getAttribute("token");
        User user = authService.getCurrentUser(token);
        return Result.success(user);
    }

}

6. 业务控制器示例

@RestController
@RequestMapping("/api/user")
@Slf4j
public class UserController {

    /**
     * 获取用户信息(需要登录)
     */
    @GetMapping("/{userId}")
    @RequireAuth
    public Result<User> getUser(@PathVariable Long userId) {
        // 业务逻辑
        return Result.success(user);
    }

    /**
     * 更新用户信息(需要登录)
     */
    @PutMapping("/{userId}")
    @RequireAuth
    public Result<String> updateUser(@PathVariable Long userId, @RequestBody User user) {
        // 业务逻辑
        return Result.success("更新成功");
    }

    /**
     * 公开接口(无需登录)
     */
    @GetMapping("/public/info")
    public Result<String> getPublicInfo() {
        return Result.success("公开信息");
    }

}

五、安全增强:更多保护措施

1. 密码加密

@Configuration
public class SecurityConfig {

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

2. 登录失败限制

@Service
public class LoginAttemptService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final int MAX_ATTEMPTS = 5;
    private static final long LOCK_TIME = 30; // 30分钟

    /**
     * 记录登录失败
     */
    public void loginFailed(String username) {
        String key = "login:attempts:" + username;
        Long attempts = redisTemplate.opsForValue().increment(key);
        
        if (attempts == 1) {
            redisTemplate.expire(key, LOCK_TIME, TimeUnit.MINUTES);
        }

        if (attempts >= MAX_ATTEMPTS) {
            // 锁定账户
            String lockKey = "login:lock:" + username;
            redisTemplate.opsForValue().set(lockKey, "locked", LOCK_TIME, TimeUnit.MINUTES);
        }
    }

    /**
     * 检查是否被锁定
     */
    public boolean isBlocked(String username) {
        String lockKey = "login:lock:" + username;
        return Boolean.TRUE.equals(redisTemplate.hasKey(lockKey));
    }

    /**
     * 登录成功,清除失败记录
     */
    public void loginSucceeded(String username) {
        String key = "login:attempts:" + username;
        redisTemplate.delete(key);
    }

}

3. 单点登录(SSO)

@Service
public class SingleSignOnService {

    @Autowired
    private TokenService tokenService;

    /**
     * 限制同时登录设备数
     */
    public void limitDevices(Long userId, int maxDevices) {
        List<TokenService.TokenInfo> tokens = tokenService.getUserTokens(userId);
        
        if (tokens.size() >= maxDevices) {
            // 踢出最早登录的设备
            tokens.stream()
                    .sorted(Comparator.comparing(TokenService.TokenInfo::getLoginTime))
                    .limit(tokens.size() - maxDevices + 1)
                    .forEach(tokenInfo -> tokenService.removeToken(userId, tokenInfo.getToken()));
        }
    }

}

六、性能优化:提升认证效率

1. Token 缓存优化

@Service
public class TokenCacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 使用 Pipeline 批量操作
     */
    public void batchValidateTokens(List<String> tokens) {
        redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            for (String token : tokens) {
                connection.get(("token:*:" + token).getBytes());
            }
            return null;
        });
    }

}

2. 本地缓存(Caffeine)

@Configuration
public class CacheConfig {

    @Bean
    public Cache<String, TokenService.TokenInfo> tokenLocalCache() {
        return Caffeine.newBuilder()
                .maximumSize(10000)
                .expireAfterWrite(5, TimeUnit.MINUTES)
                .build();
    }

}

七、监控与告警:确保系统安全

1. 登录监控

@Component
public class LoginMetrics {

    @Autowired
    private MeterRegistry meterRegistry;

    public void recordLoginSuccess(String username) {
        meterRegistry.counter("login.success", "user", username).increment();
    }

    public void recordLoginFailure(String username, String reason) {
        meterRegistry.counter("login.failure", "user", username, "reason", reason).increment();
    }

    public void recordTokenRefresh(String username) {
        meterRegistry.counter("token.refresh", "user", username).increment();
    }

}

2. 异常登录告警

  • 异地登录:检测到非常用 IP 登录时告警
  • 频繁登录失败:短时间内多次登录失败时告警
  • 异常时间登录:在非工作时间登录时告警

八、最佳实践:登录系统设计建议

1. Token 设计

  • 长度:至少 32 位,增加破解难度
  • 有效期:建议 2 小时,支持自动续期
  • 存储:使用 Redis,设置合理的过期时间
  • 传输:使用 HTTPS,防止中间人攻击

2. 密码安全

  • 加密算法:使用 BCrypt、PBKDF2 等慢哈希算法
  • 密码强度:要求包含大小写字母、数字、特殊字符
  • 定期更换:建议 90 天更换一次密码
  • 历史密码:禁止重复使用最近 5 次密码

3. 会话管理

  • 并发控制:限制同时登录设备数
  • 超时处理:设置合理的会话超时时间
  • 主动退出:支持用户主动退出所有设备
  • 强制下线:支持管理员强制用户下线

4. 安全防护

  • 验证码:登录时添加验证码,防止暴力破解
  • IP 限制:限制登录 IP 范围
  • 设备绑定:重要操作需要设备验证
  • 操作日志:记录所有登录和操作日志

九、常见问题与解决方案

1. Token 被盗

问题:Token 被恶意获取
解决方案

  • 使用 HTTPS 传输
  • 设置合理的过期时间
  • 支持 Token 黑名单
  • 检测到异常立即强制下线

2. Redis 故障

问题:Redis 服务不可用
解决方案

  • 使用 Redis 集群,保证高可用
  • 配置 Redis 持久化
  • 降级到本地缓存(短期)
  • 快速恢复 Redis 服务

3. 高并发场景

问题:高并发下 Redis 压力过大
解决方案

  • 使用 Redis 集群分担压力
  • 本地缓存 + Redis 二级缓存
  • 优化 Token 验证逻辑
  • 使用 Pipeline 批量操作

十、总结与展望

1. 核心价值

  • 分布式支持:天然支持分布式系统
  • 高性能:Redis 提供高性能的 Token 存储和验证
  • 安全性:支持 Token 过期、自动续期、强制下线
  • 可扩展:易于扩展和集成其他安全机制

2. 应用场景

  • Web 应用:前后端分离的 Web 应用
  • 移动应用:App 的用户认证
  • 微服务架构:服务间的身份验证
  • 单点登录:多系统统一认证

3. 未来发展

  • JWT 结合:结合 JWT 实现无状态认证
  • OAuth2 集成:支持第三方登录
  • 生物识别:集成指纹、人脸等生物识别
  • 零信任架构:持续验证,永不信任

十一、互动话题

  1. 你在项目中是如何实现登录认证的?
  2. 你认为 Session 和 Token 哪种方式更好?
  3. 你对 Token 安全有什么好的实践经验?
  4. 你如何看待 JWT 在登录认证中的应用?

欢迎在评论区分享你的经验和观点!


关于作者:服务端技术精选,专注于分享后端技术、分布式系统、微服务架构等内容。

个人博客www.jiangyi.space

公众号:服务端技术精选


标题:SpringBoot + Redis 实现登录校验:分布式会话管理,安全又高效
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/12/1772961685425.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消