在前后端分离架构中不建议使用cookie-session机制实现端状态识别
原因:
1.前后端分离存在跨域问题,cookie无法共享
2.后台服务器一旦建立集群,可能导致session数据丢失,即·后台有多台服务器,每个服务器存的session不一样,导致访问到不同的服务器时导致找不到对应的session
3.前端浏览器如果禁用session,则该机制无法生效
4.后台服务器需要维护session对象,又内存开销
所以我们可以使用redis来模拟session-cookie机制
我们可以再后端通过雪花算法生成一个独一的sessionid,然后存入redis中,并把这个sessionId值发送给前端,前端再通过发请求数据->里面包含sessionId,后端再通过sessionId去redis取出值进行比较。我们还可以使用redis的数据过期模式来模拟session的过期机制
下面我们通过实现验证码的功能来举例
第一步:了解前端要我们返回的数据变量名字,变量类型
{"code": 1,"data": {"imageData": "iVBORw0KGgoAAAANSUh...省略...AAAPoAAAAoCAYAAADX=", //base64格式图片"sessionId": "1479063316897845248" //保存在redis中验证码对应的key,模拟sessioinId}
}
我们来封装一个这个json数据->由外到内
1.封装code,data成一个result类,专门用于返回数据
<!--jackson相关注解,实现日期格式转换和类型格式转换并序列化等--><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-annotations</artifactId></dependency>
/*** 返回数据类* @JsonInclude 保证序列化json的时候,如果是null的对象,key也会消失* @param <T>*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class R<T> implements Serializable {private static final long serialVersionUID = 7735505903525411467L;// 成功值,默认为1private static final int SUCCESS_CODE = 1;// 失败值,默认为0private static final int ERROR_CODE = 0;//状态码private int code;//消息private String msg;//返回数据private T data;private R(int code){this.code = code;}private R(int code, T data){this.code = code;this.data = data;}private R(int code, String msg){this.code = code;this.msg = msg;}private R(int code, String msg, T data){this.code = code;this.msg = msg;this.data = data;}public static <T> R<T> ok(){return new R<T>(SUCCESS_CODE,"success");}public static <T> R<T> ok(String msg){return new R<T>(SUCCESS_CODE,msg);}public static <T> R<T> ok(T data){return new R<T>(SUCCESS_CODE,data);}public static <T> R<T> ok(String msg, T data){return new R<T>(SUCCESS_CODE,msg,data);}public static <T> R<T> error(){return new R<T>(ERROR_CODE,"error");}public static <T> R<T> error(String msg){return new R<T>(ERROR_CODE,msg);}public static <T> R<T> error(int code, String msg){return new R<T>(code,msg);}public static <T> R<T> error(ResponseCode res){return new R<T>(res.getCode(),res.getMessage());}public int getCode(){return code;}public String getMsg(){return msg;}public T getData(){return data;}
}
2.封装data里面的数据
data里面的数据我们其实可以不用特意封装成一个类,因为只有两个key,且没有什么实际意义,
最重要的是验证码不用与数据库进行交互,只需要访问redis即可,所以我们可以直接使用Map
第二步:导入redis依赖
<!--redis场景依赖-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis创建连接池,默认不会创建连接池 -->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency>
在yml文件中写redis的配置,如果我们把所有配置信息都写在application.yml文件中,就会导致这个文件十分复杂,所以我们额外创建一个application-cache.yml文件,在此文件中写redis配置,再通过application.yml来激活application-cache.yml
spring:profiles:active: cache #激活其他配置文件
spring:# 配置缓存redis:host: 192.168.230.100port: 6379database: 0 #Redis数据库索引(默认为0)lettuce:pool:max-active: 8 # 连接池最大连接数(使用负值表示没有限制)max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)max-idle: 8 # 连接池中的最大空闲连接min-idle: 1 # 连接池中的最小空闲连接timeout: PT10S # 连接超时时间
application-cahe.yml没有叶子的解决方法
找到对应的模块添加spring
再点击这个小叶子进行添加
自定义redis序列化
@Configuration
public class RedisCacheConfig {/*** 配置redisTemplate bean,自定义数据的序列化的方式,避免使用默认的jdk序列化方式* jdk序列化缺点:* 1.阅读体验差* 2.序列化后内容体积比较大,占用过多内存* @param redisConnectionFactory 连接redis的工厂,底层有场景依赖启动时,自动加载* @return*///TODO:方法名必须是redisTemplate,这是bean id 如果自己装配了这个类的bean,SpringBoot就不会自动装配了//TODO:而底层又是使用了redisTemplate这个bean id 的,所以方法名必须为redisTemplate@Beanpublic RedisTemplate redisTemplate(@Autowired RedisConnectionFactory redisConnectionFactory){//不加@Autowired也行//1.构建RedisTemplate模板对象RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(redisConnectionFactory);//2.为不同的数据结构设置不同的序列化方案//设置key序列化方式template.setKeySerializer(new StringRedisSerializer());//设置value序列化方式template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));//设置hash中field字段序列化方式template.setHashKeySerializer(new StringRedisSerializer());//设置hash中value的序列化方式template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));//5.初始化参数设置template.afterPropertiesSet();return template;}
}
测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestRedisCache {@Autowiredprivate RedisTemplate redisTemplate;@Testpublic void test(){ValueOperations valueOperations = redisTemplate.opsForValue();//取出操作String类型的操作类valueOperations.set("name","ajx");System.out.println(valueOperations.get("name"));}
}
第三步:使用雪花算法生成唯一的sessionId,即一个数字
64位ID (42(时间戳)+5(机房ID)+5(机器ID)+12(序列号-同毫秒内重复累加))
1.导入一个工具类
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;/*** 分布式自增长ID实现,底层基于Twitter的Snowflake* 64位ID (42(时间戳)+5(机房ID)+5(机器ID)+12(序列号-同毫秒内重复累加))* @author itheima*/
public class IdWorker {// 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)private final static long twepoch = 1288834974657L;// 机器标识位数private final static long workerIdBits = 5L;// 数据中心标识位数private final static long datacenterIdBits = 5L;// 机器ID最大值private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);// 数据中心ID最大值private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);// 毫秒内自增位private final static long sequenceBits = 12L;// 机器ID偏左移12位private final static long workerIdShift = sequenceBits;// 数据中心ID左移17位private final static long datacenterIdShift = sequenceBits + workerIdBits;// 时间毫秒左移22位private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;private final static long sequenceMask = -1L ^ (-1L << sequenceBits);/* 上次生产id时间戳 */private static long lastTimestamp = -1L;//同毫秒并发控制private long sequence = 0L;//机器IDprivate final long workerId;//机房IDprivate final long datacenterId;public IdWorker(){this.datacenterId = getDatacenterId(maxDatacenterId);this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);}/*** @param workerId* 工作机器ID* @param datacenterId* 序列号*/public IdWorker(long workerId, long datacenterId) {if (workerId > maxWorkerId || workerId < 0) {throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));}if (datacenterId > maxDatacenterId || datacenterId < 0) {throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));}this.workerId = workerId;this.datacenterId = datacenterId;}/*** 获取下一个ID** @return*/public synchronized long nextId() {long timestamp = timeGen();if (timestamp < lastTimestamp) {throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));}if (lastTimestamp == timestamp) {// 当前毫秒内,则+1sequence = (sequence + 1) & sequenceMask;if (sequence == 0) {// 当前毫秒内计数满了,则等待下一秒timestamp = tilNextMillis(lastTimestamp);}} else {sequence = 0L;}lastTimestamp = timestamp;// ID偏移组合生成最终的ID,并返回IDlong nextId = ((timestamp - twepoch) << timestampLeftShift)| (datacenterId << datacenterIdShift)| (workerId << workerIdShift) | sequence;return nextId;}private long tilNextMillis(final long lastTimestamp) {long timestamp = this.timeGen();while (timestamp <= lastTimestamp) {timestamp = this.timeGen();}return timestamp;}private long timeGen() {return System.currentTimeMillis();}/*** <p>* 获取 maxWorkerId* </p>*/protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {StringBuffer mpid = new StringBuffer();mpid.append(datacenterId);String name = ManagementFactory.getRuntimeMXBean().getName();if (!name.isEmpty()) {/** GET jvmPid*/mpid.append(name.split("@")[0]);}/** MAC + PID 的 hashcode 获取16个低位*/return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);}/*** <p>* 数据标识id部分* </p>*/protected static long getDatacenterId(long maxDatacenterId) {long id = 0L;try {InetAddress ip = InetAddress.getLocalHost();NetworkInterface network = NetworkInterface.getByInetAddress(ip);if (network == null) {id = 1L;} else {byte[] mac = network.getHardwareAddress();id = ((0x000000FF & (long) mac[mac.length - 1])| (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;id = id % (maxDatacenterId + 1);}} catch (Exception e) {System.out.println(" getDatacenterId: " + e.getMessage());}return id;}
}
2.配置机房id和机器id,并存入IoC容器
/*** 根据雪花算法保证sessionId的唯一性* @return 返回第三方bean,加入到IoC容器*/@Beanpublic IdWorker idWorker(){//参数一:机器id//参数二:机房idreturn new IdWorker(1l,2l);}
第四步:导入验证码图片生成包
<!--hutool万能工具包-->
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId>
</dependency>
第五步:定义一个常量类,用于封装我们使用的数据
/*** * @Description 常量类信息封装*/
public class StockConstant {/*** 定义校验码的前缀*/public static final String CHECK_PREFIX="CK:";}
第六步:业务逻辑
@Service
@Slf4j
public class UserServiceImpl implements UserService {@AutowiredIdWorker idWorker;//sessionId生成器@AutowiredRedisTemplate redisTemplate;//redis@Overridepublic R<Map<String, String>> getCaptchaCode() {//1.生成图片验证码,使用huTool工具包/*参数一:生成验证码的宽度参数二:生成验证码的长度参数三:图片中包含验证码的长度参数四:干扰线数量*/LineCaptcha captcha = CaptchaUtil.createLineCaptcha(250, 40, 4, 5);//设置背景颜色captcha.setBackground(Color.LIGHT_GRAY);//获取校验码String code = captcha.getCode();//获取经过base64编码处理的图片数据String imageData = captcha.getImageBase64();//2.生成sessionId,并转换成String类型,防止发送给前端时数据丢失String sessionId = String.valueOf(idWorker.nextId());//把Long类型的数据变成String类型再发送给前端log.info("当前生成的验证码为:{},sessionId为:{}",code,sessionId);//3.将sessionId作为key,验证码作为value保存到redis,TODO:设置验证码的有效时间,即key(sessionID)的存活时间//TODO:这里给sessionID加一个前缀,区别这是用于验证码的,到时候可以在redis使用 keys CK*来查看现在redis到底有多少验证码key//redisTemplate.opsForValue().set("CK:"+sessionId,code,5, TimeUnit.MINUTES);redisTemplate.opsForValue().set(StockConstant.CHECK_PREFIX +sessionId,code,5, TimeUnit.MINUTES);//4.组装数据Map<String,String>map=new HashMap<>();map.put("sessionId",sessionId);//sessionId key值不能乱写,要和前端的变量名字一样map.put("imageData",imageData);//5.返回数据return R.ok(map);}
}
第七步:测试接口
{"code": 1,"data": {"imageData": "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","sessionId": "1826774073703927808"}
}
成功