redis与session
基本引入
本质就是map 以key value的形式进行一个缓存 所以简单使用的时候也当作map用就行了
加入依赖
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
修改配置文件
spring:
redis:
host: localhost # Redis 主机
port: 6379 # Redis 端口
database: 2 # Redis 使用的数据库索引
使用只需要简单设置key
然后注入
@Resource
private RedisTemplate<String, Object> redisTemplate;
使用其get set方法即可
以下是一个简单的示例 实现密码重试过多锁定功能
private static final String PASSWORD_RETRY_KEY = "YunPicture:user:password_retry:%s";
@Value("${user.max-password-retry}")
private int maxPwdRetryLimit;
@Override
public LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request) {
User user = getUserByAccount(userAccount);
//密码重试过多锁定 (redis) done
String retryKey = String.format(PASSWORD_RETRY_KEY, userAccount);
Integer retryCount = (Integer) redisTemplate.opsForValue().get(retryKey);
if (retryCount != null && retryCount >= maxPwdRetryLimit) {
throw new BusinessException(ErrorCode.FORBIDDEN_ERROR, "密码输入次数过多,请10分钟后重试");
}
boolean isPasswordValid = passwordUtils.verifyPassword(userPassword, user.getUserPassword(), user.getSalt());
if (!isPasswordValid) {
// 密码错误时记录次数
redisTemplate.opsForValue().set(retryKey, retryCount != null ? retryCount + 1 : 1, 600, TimeUnit.SECONDS);
throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");
} else {
// 登录成功重置计数器
redisTemplate.delete(retryKey);
}
LoginUserVO LoginuserVO = convertToLoginUserVO(user);
//todo 引入redis 控制登入设备数量
request.getSession().setAttribute(USER_LOGIN_STATE, LoginuserVO);
return LoginuserVO;
}
测试下来功能是没问题的 但是打开quickredis一看全是乱码
这是因为默认情况下,Spring Boot的RedisTemplate使用JdkSerializationRedisSerializer作为键/值的序列化器,这会导致:
键名会被序列化为二进制格式
QuickRedis等客户端无法直接识别二进制键名
即使数据存储成功,但通过客户端查看时显示为乱码或无法识别
(虽然没影响 但分别调试 最好还是修改以下)
配置序列化
其实就是简单去规定一下键值对的存储格式
自定义一下redistemplate
package com.zwnsyw.yunpicturebackend.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.zwnsyw.yunpicturebackend.exception.BusinessException;
import com.zwnsyw.yunpicturebackend.exception.ErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
@Configuration
@Slf4j
public class RedisTemplateConfig {
@Value("${redis.host:localhost}")
private String redisHost;
@Value("${redis.port:6379}")
private int redisPort;
/**
* 配置 RedisTemplate 使用 Jackson 序列化
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 键的序列化:纯文本
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 值的序列化:JSON 格式
GenericJackson2JsonRedisSerializer valueSerializer = genericJackson2JsonRedisSerializer();
template.setValueSerializer(valueSerializer);
template.setHashValueSerializer(valueSerializer);
template.setDefaultSerializer(valueSerializer);
template.setEnableTransactionSupport(true); // 开启事务支持
template.afterPropertiesSet();
try {
template.afterPropertiesSet();
} catch (RedisConnectionFailureException e) {
log.error("Redis 连接失败:主机:{},端口:{}", redisHost, redisPort);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "连接 Redis 失败");
} catch (Exception e) {
log.error("Redis 配置失败:主机:{},端口:{}", redisHost, redisPort);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "Redis 配置出错");
}
log.info("Redis 配置成功,主机:{},端口:{}", redisHost, redisPort);
return template;
}
/**
* 配置通用 JSON 序列化器
*/
private GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
// 允许的业务包路径
.allowIfSubType("com.zwnsyw")
.allowIfSubType("com.zwnsyw.yunpicturebackend.model.*")
.allowIfSubType("com.zwnsyw.yunpicturebackend.model.entity.*")
.allowIfSubType("com.zwnsyw.yunpicturebackend.model.vo.*")
// 允许的集合类型
.allowIfSubType("java.util.ArrayList")
.allowIfSubType("java.util.HashMap")
.allowIfSubType("java.util.LinkedList")
.allowIfSubType("java.util.TreeMap")
.allowIfSubType("java.util.LinkedHashMap")
// 允许基础类型和 Date
.allowIfSubType("java.lang.String")
.allowIfSubType("java.lang.Long")
.allowIfSubType("java.lang.Integer")
.allowIfSubType("java.util.Date")
// 其他类型(如 MyBatis 分页类)
.allowIfSubType("com.baomidou.mybatisplus.extension.plugins.pagination.Page")
.allowIfSubType("java.sql.Timestamp")
.build(),
ObjectMapper.DefaultTyping.NON_FINAL
);
return new GenericJackson2JsonRedisSerializer(objectMapper);
}
}
配置分布式session
需引入
<!-- Spring Session + Redis 支持 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.6.3</version>
</dependency>
配置
session:
store-type: redis
redis:
namespace: session # 设置 Redis 中存储 session 数据的命名空间
flush-mode: on_save # 表示当 session 更新时,才会将数据保存到 Redis
timeout: 86400 # session 过期时间:24小时
SessionConfig
package com.zwnsyw.yunpicturebackend.config;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import java.time.Duration;
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30)
public class SessionConfig {
// 配置Lettuce客户端参数
@Bean
public LettuceClientConfiguration redisSessionConfig() {
return LettuceClientConfiguration.builder()
.commandTimeout(Duration.ofMinutes(30))
.shutdownTimeout(Duration.ofSeconds(10)) // 添加关闭超时配置
.build();
}
@Bean
@Qualifier("springSessionDefaultRedisSerializer")
public RedisSerializer springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
实现登陆设备数量限制:
@Override
public LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request) {
User user = getUserByAccount(userAccount);
//to do 密码重试过多锁定 (redis) done
String retryKey = String.format(PASSWORD_RETRY_KEY, userAccount);
Integer retryCount = (Integer) redisTemplate.opsForValue().get(retryKey);
if (retryCount != null && retryCount >= maxPwdRetryLimit) {
throw new BusinessException(ErrorCode.FORBIDDEN_ERROR, "密码输入次数过多,请10分钟后重试");
}
boolean isPasswordValid = passwordUtils.verifyPassword(userPassword, user.getUserPassword(), user.getSalt());
if (!isPasswordValid) {
// 密码错误时记录次数
redisTemplate.opsForValue().set(retryKey, retryCount != null ? retryCount + 1 : 1, 600, TimeUnit.SECONDS);
throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");
} else {
// 登录成功重置计数器
redisTemplate.delete(retryKey);
}
LoginUserVO LoginuserVO = convertToLoginUserVO(user);
// 获取用户 IP 和会话 SessionId
String userIp = request.getRemoteAddr();
String sessionId = request.getSession().getId();
// 验证设备数量,并记录登录设备
if (!recordUserDevice(user.getId(), sessionId, userIp)) {
throw new BusinessException(ErrorCode.FORBIDDEN_ERROR, "设备登录数量超限制");
}
//to do 引入redis 控制登入设备数量 done
request.getSession().setAttribute(USER_LOGIN_STATE, LoginuserVO);
log.info("用户登录成功: userId={}, account={}, sessionId={}, ip={}", user.getId(), userAccount, sessionId, userIp);
return LoginuserVO;
}
/**
* 记录用户设备信息,同时清理旧设备的登录状态
*/
private boolean recordUserDevice(long userId, String sessionId, String userIp) {
String redisKey = "login_devices:" + userId;
String deviceKey = sessionId + "_" + userIp;
// 使用 ZSet 存储设备信息,并以时间戳为权重
long currentTime = System.currentTimeMillis();
redisTemplate.opsForZSet().add(redisKey, deviceKey, currentTime);
// 检查设备数量是否超出限制
Long deviceCountObj = redisTemplate.opsForZSet().size(redisKey);
long deviceCount = deviceCountObj != null ? deviceCountObj : 0;
if (deviceCount > maxDeviceLimit) {
log.warn("超过设备数量限制: userId={}, deviceCount={}, maxDeviceLimit={}", userId, deviceCount, maxDeviceLimit);
// 获取超出部分的旧设备
Set devicesToRemove = redisTemplate.opsForZSet()
.range(redisKey, 0, deviceCount - maxDeviceLimit - 1);
if (devicesToRemove != null) {
for (Object device : devicesToRemove) {
String[] parts = device.toString().split("_");
if (parts.length > 0) {
String oldestSessionId = parts[0];
// 销毁旧设备登录态
invalidateSession(oldestSessionId);
Message message = new Message();
message.setUserId(userId);
message.setTitle("下线通知");
message.setContent("您的账户已达到最大设备登录数量限制,已自动下线最早登入设备"); //todo session(vo)中记录ip 登录地点 优化通知
message.setType(MessageTypeEnum.SYSTEM_MESSAGE.getValue());
messageService.addMessage(message);
}
// 从 Redis 中移除旧设备
redisTemplate.opsForZSet().remove(redisKey, device);
}
}
}
// 设置 Redis 键的过期时间
redisTemplate.expire(redisKey, 7, TimeUnit.DAYS);
return true;
}
/**
* 销毁给定会话的 HttpSession
*/
private void invalidateSession(String sessionId) {
// 使用分布式 Session 管理销毁会话
Session session = sessionRepository.findById(sessionId);
if (session != null) {
sessionRepository.deleteById(sessionId); // 强制删除 Redis 中的 Session
log.info("Session 已销毁: sessionId={}", sessionId);
} else {
log.warn("未能找到 Session: sessionId={}", sessionId);
}
}
@Override
public boolean userLogout(HttpServletRequest request) {
// 获取当前用户ID
LoginUserVO loginUser = (LoginUserVO) request.getSession().getAttribute(USER_LOGIN_STATE);
if (loginUser != null) {
String redisKey = "login_devices:" + loginUser.getId();
String sessionId = request.getSession().getId();
String userIp = request.getRemoteAddr();
// 移除设备记录
String deviceKey = sessionId + "_" + userIp;
redisTemplate.opsForZSet().remove(redisKey, deviceKey);
log.info("用户注销成功: userId={}, sessionId={}, userIp={}", loginUser.getId(), sessionId, userIp);
}
// 销毁 Session
request.getSession().invalidate();
return true;
}
项目分区导航:⬅️ 04-@Cacheable和@CacheEvict 缓存失效策略 | 05-redis与session | ➡️ 06-图片优化
💬 评论