在前后端分离架构中不建议使用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;
// 成功值,默认为1
private static final int SUCCESS_CODE = 1;
// 失败值,默认为0
private 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.100
port: 6379
database: 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
@Bean
public 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 {
@Autowired
private RedisTemplate redisTemplate;
@Test
public 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;
//机器ID
private final long workerId;
//机房ID
private 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) {
// 当前毫秒内,则+1
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒内计数满了,则等待下一秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
// ID偏移组合生成最终的ID,并返回ID
long 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容器
*/
@Bean
public IdWorker idWorker(){
//参数一:机器id
//参数二:机房id
return 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 {
@Autowired
IdWorker idWorker;//sessionId生成器
@Autowired
RedisTemplate redisTemplate;//redis
@Override
public 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"
}
}
成功