史上最细基于Redis实现的分布式Session解决单点登录问题,入门导师带你一步一步实现

点赞多大胆,就有多大产!开源促使进步,献给每一位技术使用者和爱好者!
干货满满,摆好姿势,点赞发车

前戏

  最近正好在做一个电商项目,跟大家分享一下使用Redis实现分布式session完成单点登录,下一篇与大家分享一下使用Redis实现分布式锁实现定时关单功能,好啦文章干货满满咱们就不多絮叨直接开搞了!

  需要说明一点就是贴出的代码是自己修改过的,有些包名会使用***替代,并且接口都使用Get请求,这样方便测试不需要使用接口测试工具了,没有使用RESTful风格等等,代码应该还算规范,数据表和pojo就不提供了,大家可以随便定义一下表结构之类的,就是个用户表,相信小伙伴们是完全可以的啦,主要还是说功能怎么实现!

在这里插入图片描述

技术架构

  • Spring Cloud:Greenwich.SR2
  • Spring Boot:2.1.14.RELEASE
  • Redis:5.0.5
  • MySQL:5.7
  • Tomcat:8.5
  • nginx:1.18

技术架构中的SpringCloud大家用不用都行,只需要将项目部署两份使用nginx负载均衡就可以了,该案例中项目直接在IDEA中运行在不同的端口,使用nginx直接部署运行了

问题介绍

  随着项目不断运行,用户越来越多,我们项目如果前期使用的是单体架构开发就需要演变成分布式架构,或者前期用户预测,项目开发启动时直接开发的就是分布式项目,如果越来越少基本就快要来凉凉,那就赶紧开始促活,拉新,留存,变现吧

  好,无论是后期的架构演进还是直接设计为分布式项目,我们在享受分布式项目给我们带来的性能提升,易维护,易升级,解耦等优势的同时也不得不去解决分布式给我们带来的新的问题和挑战,可以享受好也可以包容坏!

  就以项目中的用户模块为例我们来说一下单体架构和分布式架构在实现时的区别以及分布式架构中的session共享问题和解决方案,仅个人使用,如果有任何疑问,更好的解决方案欢迎评论区留言,一起进步!

先贴出一些公共类吧

公共响应类

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;
import java.io.Serializable;

/**
 * @author stt
 * 公共响应类
 * 我们会将该类转换成JSON序列化之后返回到前端
 * 如果属性数据为null,我们不将器序列化到结果中
 */
@Data
//序列化结果中只包含非空的字段
@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)
public class ResponseData<T> implements Serializable {
    /**
     *响应状态
     */

    private int status;
    /**
     * 响应信息
     */
    private String msg;

    /**
     * 响应数据
     */
    private T data;

    /**
     * @JsonIgnore
     * @return
     */
    @JsonIgnore
    public boolean isSuccess(){
        //当前对象的状态码和成功的状态码比较
        return this.status == ResponseCode.SUCCESS.getCode();
    }

    /**
     * 定义只有状态构造器
     */
    private ResponseData(int status){
        this.status = status;
    }
    /**
     * 定义有状态,有数据
     */
    private ResponseData(int status,T data){
        this.status = status;
        this.data = data;
    }
    /**
     * 定义有状态,信息,数据
     */
    private ResponseData(int status,String msg,T data){
        this.status = status;
        this.msg = msg;
        this.data = data;
    }
    /**
     * 有状态和信息
     */
    private ResponseData(int status,String msg){
        this.status = status;
        this.msg = msg;
    }

    /**成功的响应*/
    /**
     * 默认的响应成功返回数据格式
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> createBySuccess(){
        return new ResponseData<T>(ResponseCode.SUCCESS.getCode(),ResponseCode.SUCCESS.getDesc());
    }

    /**
     * 自定义成功返回信息
     * @param msg
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> createBySuccess(String msg){
        return new ResponseData<>(ResponseCode.SUCCESS.getCode(),msg);
    }

    /**
     * 查询,就会返回数据
     * @param data
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> createBySuccess(T data){
        return new ResponseData<T>(ResponseCode.SUCCESS.getCode(),data);
    }

    /**
     * 状态码,信息,数据都有
     * @param msg
     * @param data
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> createBySuccess(String msg,T data){
        return new ResponseData<T>(ResponseCode.SUCCESS.getCode(),msg,data);
    }

    /**失败响应*/
    /**
     * 默认失败
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> createByError(){
        return new ResponseData<>(ResponseCode.ERROR.getCode(),ResponseCode.ERROR.getDesc());
    }

    /**
     * 自定义错误信息失败返回
     * @param msg
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> createByError(String msg){
        return new ResponseData<>(ResponseCode.ERROR.getCode(),msg);
    }

    /**
     * 自定义状态码和错误信息
     * @param code
     * @param msg
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> createByError(int code,String msg){
        return new ResponseData<>(code,msg);
    }
}

响应枚举类

/**
 * @author stt
 * 
 */

public enum ResponseCode {

    SUCCESS(0,"SUCCESS"),
    ERROR(1,"ERROR"),
    NEED_LOGIN(10,"NEED_LOGIN")
	//响应码
    private final int code;
    //描述
    private final String desc;

    ResponseCode(int code,String desc){
        this.code = code;
        this.desc = desc;
    }

    public int getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }
}

常量类

import com.google.common.collect.Sets;
import java.util.Set;

/**
 * @author stt
 */
public class Const {

    /**
     * session中的当前用户的key
     */
    public static final String CURRENT_USER = "currentUser";
    /**
    redis中session的key
    */
    public static final long REDIS_SESSION_EXPIRE = 30 * 60;
}

单体架构用户模块

说明

  单体架构用户模块在登陆时将用户存储到HttpSession中,这应该也是大多数单体系统记录用户登陆状态的方式,下边贴出登陆代码,查看个人资料和登出代码,这里贴出Controller,后边的service,mapper等大家简单查询就可以了

用户模块代码

import com.***.***.common.Const;
import com.***.***.common.ResponseCode;
import com..***.***.common.ResponseData;
import com.***.***.pojo.User;
import com.***.***.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpSession;
import javax.xml.ws.Response;

/**
 * <p>
 *  前台用户模块:也就是非管理员用户
 * </p>
 *
 * @author stt
 */
@RestController
@RequestMapping(value = "/user")
public class UserController {

    @Autowired
    private IUserService userService;
    /**
     * 根据用户名和密码登陆
     * @param username
     * @param password
     * @param session
     * @return
     */
    @PostMapping(value = "login")
    public ResponseData<User> login(String username, String password, HttpSession session){
        //调用service的登陆方法
        ResponseData<User> responseData = userService.login(username,password);
        //判断是否查到用户
        if(responseData.isSuccess()){
            //将用户存储到Session中
            session.setAttribute(Const.CURRENT_USER,responseData.getData());
        }
        //返回ResponseData即可
        return responseData;
    }

    /**
     * 退出登陆,
     * 删除session存储的当前登陆的用户即可
     * @param session
     * @return
     */
    @GetMapping(value = "logout")
    public ResponseData<String> logout(HttpSession session){
        //删除session中的当前用户
        session.removeAttribute(Const.CURRENT_USER);
        return ResponseData.createBySuccess("退出登陆成功");
    }

    /**
     *获取个人信息,不能获取别人的,所以这里我们要解决横向越权问题
     * 横向越权就是传递参数,获取到同级别用户的信息
     * 1、从session中获取当前登录用户,根据用户id查询,不能自己传id
     * @param session
     * @return
     */
    @GetMapping(value = "get_user_info")
    public ResponseData<User> getUserInfo(HttpSession session){

        User user = (User)session.getAttribute(Const.CURRENT_USER);
        //判断是否有用户
        if (user == null) {
            return ResponseData.createByError("用户未登录,无法获取用户信息");
        }
        //当前登陆的用户就是你要查询的用户的数据
        return ResponseData.createBySuccess(user);
    }
}

启动项目测试

在这里插入图片描述

问题

  如果系统用户变多,一台后天服务器肯定支撑不了,我们一方面考虑将项目拆分成出用户模块,商品模块,订单模块,支付模块,购物车模块等等独立开发部署,互不影响,用户对不同业务的请求会发送到不同的服务上处理也可以提升性能,可以使用SpringCloud或者dubbo实现,另一方便我们拆分完之后如果用户服务部署一份还是不行,我们就需要考虑横向扩展,将服务部署多份,比如将用户服务两份到不同的tomcat上,使用Nginx负载均衡,那么我们来实现一下

分布式用户模块暴露问题

思路

  • 可以单机部署,但记得的修改端口老铁门,也可以多机部署,安装Linux虚拟机即可
  • 部署两份,项目打包,代码与上边一样,这里为了演示出问题,之后启动tomcat
  • 配置nginx,配置文件后边也贴出,启动nginx,我们访问nginx,做负载均衡到两台tomcat上

这里我们使用的SpringBoot做的开发,有内嵌容器,我直接在IDEA中修改端口启动两份项目,省的打包部署了,注意IDEA默认一个项目单例运行,我们需要修改一下

IDEA设置

我使用的IDEA是2020版本的,如下图将Allow paraller run选中即可,之前用的2018版本是取消Single instance only,大家看自己的版本选择即可

在这里插入图片描述

运行起两个项目

运行起来两个项目,代码都没有改还是上边的,注意的是修改端口号,我这里端口号分别为8001和8002

分别访问

单独使用ip:port访问一下,可以请求到说明服务是没有问题的,接下来就是做nginx配置了

在这里插入图片描述

配置nginx负载均衡

upstream www.yj.com{
	server 127.0.0.1:8001;
	server 127.0.0.1:8002;
}

server{
	listen 80;
	server_name www.yj.com;
	
	location /{
		proxy_pass http://www.yj.com;
		index index.html index.htm index.jsp;
	}
}

这个配置简单说一下

upstream:引入负载均衡模块,后边的www.yj.com可以随便写但是要和location中的prosy_pass一致

upstream中server 后配置的是对应的服务器,ip:port,默认负载是轮询1:1这样

我这里没有用ip用的是域名替代的,大家可以在hosts文件中修改ip映射就可以啦

访问nginx暴露问题

启动nginx

接下来就是启动nginx使用nginx访问项目啦,windows下启动nginx不要双击nginx.exe,进入到命令行,使用nginx.exe命令启动,但是会占用当前窗口不能输入其他命令,如有需要重新加载nginx配置文件或者关闭nginx可以在开启一个新窗口,如下图:

在这里插入图片描述

访问

我们在浏览器上输入www.yj.com/user/login等接口名就可以啦,请求发到nginx上,nginx帮我们转发到tomcat上边,下边我们来看一下
在这里插入图片描述

至此我们的问题就暴露出来了,因为我们登录时访问的是tomcat1,访问获取用户数据时访问的是tomcat2,而用户的session信息在tomcat1上存储,tomcat2并不知道该用户来过,所以在获取个人信息时从session中获取不到数据,所以就显示该用户未登录(未登录字样有点短,在做动图时删的帧数有点多啦,哈哈),下边我们解决一下!

解决session共享问题

问题描述和思路

  这个问题我们统一称为session共享问题,或者说实现单点登录,对于这个问题有很多解决方案,本篇文章我们来说一下使用redis解决,因为redis是每个项目都可以访问的也是可以共享的,大概思路如下:

  • 我们用户模块连接同一个redis实例,比如6379端口的
  • 在登陆时我们根据用户名和密码的登陆逻辑判断用户是否登陆成功
  • 如果登陆成功,将sessionid或者是一个UUID只要唯一就可以了当做key,将用户当做value存到redis中,并将该key设置过期时间为30分钟过期
  • 之后我们的后台再生成一个cookie,将这个sessionID当做cookie的值,cookie的键可以自己取名字,将这个cookie种到浏览器上,
  • 用户再来访问网站时就会携带cookie,我们遍历cookie找到我们需要的那个cookie,将value取出,根据value也就是上一次存储的sessionid,到Redis中查用户,如果能查到说明登陆过,查不到就没有登陆过,或者用户登录超时了
  • 用户退出登录时,将cookie删除,将redis中数据删除
  • 如果用户做了其它任何操作,我们再写一个过滤器,重置key的有效期,否则用户登陆进来就只能玩30分钟,太短了是不!

JsonUtil

我们向redis中存储用户没有使用hash类型,而是将User序列化为json字符串,取出时再反序列化,这个JsonUtil类大家工作时也可以拿走直接使用的

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;


/**
 *@author stt
 */
@Slf4j
public class JsonUtil {

    private static ObjectMapper objectMapper = new ObjectMapper();

    static{
        //对象的所有字段全部列入
        objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
    }

    public static <T> String obj2String(T obj){
        if(obj == null){
            return null;
        }
        try {
            return obj instanceof String ? (String)obj :  objectMapper.writeValueAsString(obj);
        } catch (Exception e) {
            log.warn("Parse Object to String error",e);
            return null;
        }
    }

    /**
     * 格式化json串,看起来比较好看,但是有换行符等符号,会比没有格式化的大
     * @param obj
     * @param <T>
     * @return
     */
    public static <T> String obj2StringPretty(T obj){
        if(obj == null){
            return null;
        }
        try {
            return obj instanceof String ? (String)obj :  objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
        } catch (Exception e) {
            log.warn("Parse Object to String error",e);
            return null;
        }
    }


    public static <T> T string2Obj(String str,Class<T> clazz){
        if(StringUtils.isEmpty(str) || clazz == null){
            return null;
        }

        try {
            return clazz.equals(String.class)? (T)str : objectMapper.readValue(str,clazz);
        } catch (Exception e) {
            log.warn("Parse String to Object error",e);
            return null;
        }
    }


    public static <T> T string2Obj(String str, TypeReference<T> typeReference){
        if(StringUtils.isEmpty(str) || typeReference == null){
            return null;
        }
        try {
            return (T)(typeReference.getType().equals(String.class)? str : objectMapper.readValue(str,typeReference));
        } catch (Exception e) {
            log.warn("Parse String to Object error",e);
            return null;
        }
    }

    /**
     * 转换集合
     * List<User></>
     * @param str
     * @param collectionClass
     * @param elementClasses
     * @param <T>
     * @return
     */
    public static <T> T string2Obj(String str,Class<?> collectionClass,Class<?>... elementClasses){
        JavaType javaType = objectMapper.getTypeFactory().constructParametricType(collectionClass,elementClasses);
        try {
            return objectMapper.readValue(str,javaType);
        } catch (Exception e) {
            log.warn("Parse String to Object error",e);
            return null;
        }
    }
}

CookieUtil

import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
public class CookieUtil {

    private final static String COOKIE_NAME = "yj_login_token";
    private final static String COOKIE_DOMAIN = "yj.com";

    /**
     * 写cookie
     * @param token    就是sessionid,也就是cookie的值,这个值只要唯一就行了,使用UUID也可以
     * @param response 使用响应对象将cookie写到浏览器上
     */
    public static void writeLoginToken(String token, HttpServletResponse response){
        Cookie cookie = new Cookie(COOKIE_NAME, token);
        cookie.setDomain(COOKIE_DOMAIN);
        cookie.setPath("/");
        //设置生存时间.0无效,-1永久有效,时间是秒,生存时间设置为1年
        cookie.setMaxAge(60 * 60 * 24 * 365);
        //设置安全机制
        cookie.setHttpOnly(true);
        log.info("写 cookie name:{},value:{}",cookie.getName(),cookie.getValue());
        //将cookie写到浏览器上
        response.addCookie(cookie);
    }

    /**
     * 读取cookie
     * @param request
     * @return
     */
    public static String readLoginToken(HttpServletRequest request){
        //获取cookie
        Cookie[] cookies = request.getCookies();
        if(cookies != null){
            //遍历cookie,取出我们自己的cookie,根据name获取
            for (Cookie cookie : cookies) {
                log.info("读取cookie cookieName{},cookieValue{}",cookie.getName(),cookie.getValue());
                //获取自己的
                if(StrUtil.equals(cookie.getName(), COOKIE_NAME)){
                    //获取值
                    return cookie.getValue();
                }
            }
        }
        return null;
    }

    /**
     * 删除cookie
     * @param request
     * @param response
     */
    public static void deleteLoginToken(HttpServletRequest request,HttpServletResponse response){
        Cookie[] cookies = request.getCookies();
        if(cookies != null){
            for (Cookie cookie : cookies) {
                if(StrUtil.equals(cookie.getName(), COOKIE_NAME)){
                    //设置cookie的有效期为0
                    cookie.setMaxAge(0);
                    cookie.setPath("/");
                    cookie.setDomain(COOKIE_DOMAIN);
                    log.info("删除cookie cookieName: {},cookieValue: {}",cookie.getName(),cookie.getValue());
                    response.addCookie(cookie);
                    return;
                }
            }
        }
    }
}

RedisUtil

关于SpringBoot整合Redis在《戳这里》有介绍

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public class RedisUtil {

    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public boolean del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                return redisTemplate.delete(key[0]);
            }
            return redisTemplate.delete(CollectionUtils.arrayToList(key)) > 0 ? true : false;
        }
        return false;
    }

    // ============================String=============================
    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }
    /**
     * 普通缓存放入
     * @param key 键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 普通缓存放入并设置时间
     * @param key 键
     * @param value 值
     * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}

Controller

import cn.hutool.core.util.StrUtil;
import com.***.common.Const;
import com.***.ResponseData;
import com.***.pojo.User;
import com.***.service.IUserService;
import com.***.util.CookieUtil;
import com.***.util.JsonUtil;
import com.***.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

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

    @Autowired
    private IUserService userService;

    @Autowired
    private Environment env;

    @Autowired
    private RedisUtil redisUtil;

    /**
     * 登陆:
     * 1、获取sessionid
     * 2、并且将对象转换成json格式,sessionID当做key,json当做value存储到redis中
     * 3、生成Cookie将这个Cookie种到浏览器上
     * @param username
     * @param password
     * @param session
     * @return
     */
    @GetMapping(value = "login")
    public ResponseData<User> login(String username, String password, HttpSession session,HttpServletResponse response){
        //登陆
        ResponseData<User> responseData = userService.login(username, password);
        if(responseData.isSuccess()){
            //生成cookie
            CookieUtil.writeLoginToken(session.getId(),response);
            //存储到redis中
            redisUtil.set(session.getId(), JsonUtil.obj2String(responseData.getData()), Const.REDIS_SESSION_EXPIRE);
        }
        return responseData;
    }

    /**
     * 1、读取cookie
     * 2、将cookie的值当做redis的key将对象取出,再反序列化就可以啦
     * @param request
     * @return
     */
    @GetMapping(value = "get_user_info")
    public ResponseData<User> getUserInfo(HttpServletRequest request){
        //读取sessionID
        String loginToken = CookieUtil.readLoginToken(request);
        if(StrUtil.isBlank(loginToken)){
            return ResponseData.createByError("用户未登录");
        }
        //从Redis中获取用户的json数据
        String userJson = (String)redisUtil.get(loginToken);
        //json转换成Use对象
        User user = JsonUtil.string2Obj(userJson, User.class);
        if(user != null){
            return ResponseData.createBySuccess(user);
        }
        return ResponseData.createByError("用户未登录");
    }

    /**
     * 退出登录
     * @param request
     * @param response
     * @return
     */
    @GetMapping(value = "logout")
    public ResponseData<User> logout(HttpServletRequest request,HttpServletResponse response){
        //读取sessionID
        String loginToken = CookieUtil.readLoginToken(request);
        if(StrUtil.isBlank(loginToken)){
            return ResponseData.createByError("用户未登录,不可注销");
        }
        //删除session
        CookieUtil.deleteLoginToken(request,response);
        //删除缓存
        redisUtil.del(loginToken);
        return ResponseData.createBySuccess("注销成功");
    }
}

过滤器

import cn.hutool.core.util.StrUtil;
import com.***.common.Const;
import com.***.pojo.User;
import com.***.util.CookieUtil;
import com.***.util.JsonUtil;
import com.***.util.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.WebApplicationContextUtils;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * 过滤器,重置redis中session有效期
 */
@Component
@WebFilter(urlPatterns = "/*",filterName = "sessionExporeFilter")
public class SessionExpireFilter implements Filter {
	/**
	这里有一个坑,这里我们使用了RedisUtil大家发现这里并没有用@Autowird注解注入
	是因为注入不进来,和spring的启动顺序有关,我们需要在init方法中引入,如果没有引入就是空指针异常
	*/
    private RedisUtil redisUtil;
    /**
    在这里获取ApplicationContext对象,通过name或者type获取redisUtil赋值给redis变量否则就是空指针
    */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        ApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(filterConfig.getServletContext());
        redisUtil = (RedisUtil)context.getBean("redisUtil");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest)request;
        //读取loginToken
        String loginToken = CookieUtil.readLoginToken(httpServletRequest);

        if (StrUtil.isNotBlank(loginToken)) {
            //从redis中获取
            String jsonStr = (String)redisUtil.get(loginToken);
            //转换
            User user = JsonUtil.string2Obj(jsonStr, User.class);
            if (user != null) {
                //重置时间
                redisUtil.expire(loginToken, Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
            }
        }
        chain.doFilter(request,response);
    }

    @Override
    public void destroy() {

    }
}

User POJO

这里再贴出User类,因为时间使用的是JDK8中的LocalDateTime,在使用jackson序列化和反序列化时出问题,所以在属性上添加注解解决问题,也可以在JsonUtil中做配置,这里也是个坑贴出来比较好,只留一个时间,其余的代码就删掉了

public class User implements Serializable {

    private static final long serialVersionUID = 1L;
            /**
            * 创建时间
            分别是序列化和反序列话使用的序列化器和时间格式以及Json字符串格式
            */
            @JsonDeserialize(using = LocalDateTimeDeserializer.class)
            @JsonSerialize(using = LocalDateTimeSerializer.class)
            @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
            @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
}

测试

通过下图大家可以看出,我们登录时会将cookie种到浏览器上,也会存储到redis中,操作时会携带cookie,解决单点登录问题

在这里插入图片描述

下图就是我们种到浏览器的cookie,后边的请求时就会携带,我们取出value,到redis中查询即可

在这里插入图片描述

总结

  • 单机项目大家还是可以将用户的登录信息存储到HttpSession中的,可是到了分布式项目时如果用户模块就不行了
  • 我们可以是Redis解决分布式session问题买当然还有其他的策略比如使用源IP Hash算法指定的客户机只能访问指定的后端服务,这样容易造成负载不均匀,也可以专门创建一个session的数据库,这样对mysql压力太大不合适,总之有多重方案,redis是使用较多,性价比较高的一种,用就对了
  • 我们上边是使用比较基础的手段实现,其实大家也可以用Spring Session实现,Spring Session是对这种方式的一种封装,原理都一样,好处就是代码侵入比较低,大家可以到Spring Session官网查看文档实现,这个大家能看得懂搞得出来,封装的相信小伙伴们也没有问题

如有任何问题或者更好地方案欢迎大家留言讨论,早上九点写到了下午3点钟,有帮助希望大家动动手指点个赞,这样心里会很爽哦,谢谢阅读啦!

路漫漫其修远兮,吾将上下而求索

最后,有比我更细的吗?我是说文章哦!我是添添!

猜你喜欢

转载自blog.csdn.net/qq_36386908/article/details/106872506
今日推荐