SpringBoot + Redis 实现登录校验:分布式会话管理,安全又高效
一、问题背景:为什么需要 Redis 实现登录校验?
在传统的单体应用中,我们通常使用 Session 来管理用户登录状态。但在微服务架构和分布式系统中,Session 面临着诸多挑战:
传统 Session 的痛点
- 单点故障:Session 存储在单个服务器上,服务器宕机会导致所有用户登录状态丢失
- 无法水平扩展:多台服务器之间无法共享 Session,用户请求可能被路由到不同服务器导致登录失效
- 内存压力:大量用户 Session 占用服务器内存,影响性能
- 跨域问题:前后端分离架构下,Session 跨域处理复杂
Redis 方案的优势
| 特性 | 传统 Session | Redis + 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 集成:支持第三方登录
- 生物识别:集成指纹、人脸等生物识别
- 零信任架构:持续验证,永不信任
十一、互动话题
- 你在项目中是如何实现登录认证的?
- 你认为 Session 和 Token 哪种方式更好?
- 你对 Token 安全有什么好的实践经验?
- 你如何看待 JWT 在登录认证中的应用?
欢迎在评论区分享你的经验和观点!
关于作者:服务端技术精选,专注于分享后端技术、分布式系统、微服务架构等内容。
个人博客:www.jiangyi.space
公众号:服务端技术精选
标题:SpringBoot + Redis 实现登录校验:分布式会话管理,安全又高效
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/12/1772961685425.html
公众号:服务端技术精选
- 一、问题背景:为什么需要 Redis 实现登录校验?
- 传统 Session 的痛点
- Redis 方案的优势
- 二、核心概念:Token + Redis 认证机制
- 1. 认证流程
- 2. Token 设计
- 3. Redis 存储结构
- 三、实现方案:SpringBoot + Redis
- 1. 技术栈
- 2. 项目结构
- 3. 添加依赖
- 4. 配置文件
- 四、代码实现:完整示例
- 1. Token 服务
- 2. 认证服务
- 3. 认证拦截器
- 4. 自定义注解
- 5. 认证控制器
- 6. 业务控制器示例
- 五、安全增强:更多保护措施
- 1. 密码加密
- 2. 登录失败限制
- 3. 单点登录(SSO)
- 六、性能优化:提升认证效率
- 1. Token 缓存优化
- 2. 本地缓存(Caffeine)
- 七、监控与告警:确保系统安全
- 1. 登录监控
- 2. 异常登录告警
- 八、最佳实践:登录系统设计建议
- 1. Token 设计
- 2. 密码安全
- 3. 会话管理
- 4. 安全防护
- 九、常见问题与解决方案
- 1. Token 被盗
- 2. Redis 故障
- 3. 高并发场景
- 十、总结与展望
- 1. 核心价值
- 2. 应用场景
- 3. 未来发展
- 十一、互动话题
评论
0 评论