史上最细基于redis实现的分布式session解决单点登录问题,入门导师带你一步一步实现...(代码片段)

androidstarjack androidstarjack     2022-12-06     521

关键词:

点击上方蓝色“终端研发部”,选择“设为星标”

学最好的别人,做最好的我们 

前戏

最近正好在做一个电商项目,跟大家分享一下使用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
来源:csdn

BAT等大厂Java面试经验总结 想获取 Java大厂面试题学习资料扫下方二维码回复「BAT」就好了回复 【加群】获取github掘金交流群回复 【电子书】获取2020电子书教程回复 【C】获取全套C语言学习知识手册回复 【Java】获取java相关的视频教程和资料回复 【爬虫】获取SpringCloud相关多的学习资料回复 【Python】即可获得Python基础到进阶的学习教程回复 【idea破解】即可获得intellij idea相关的破解教程关注我gitHub掘金,每天发掘一篇好项目,学习技术不迷路!



回复 【idea激活】即可获得idea的激活方式
回复 【Java】获取java相关的视频教程和资料
回复 【SpringCloud】获取SpringCloud相关多的学习资料
回复 【python】获取全套0基础Python知识手册
回复 【2020】获取2020java相关面试题教程
回复 【加群】即可加入终端研发部相关的技术交流群
阅读更多
为什么HTTPS是安全的
因为BitMap,白白搭进去8台服务器...
《某厂内部SQL大全 》.PDF
字节跳动一面:i++ 是线程安全的吗?
大家好,欢迎加我微信,很高兴认识你!
在华为鸿蒙 OS 上尝鲜,我的第一个“hello world”,起飞!

相信自己,没有做不到的,只有想不到的在这里获得的不仅仅是技术!



就给个“在看”

kafka史上最详细原理总结

Kafka是最初由Linkedin公司开发,是一个分布式、支持分区的(partition)、多副本的(replica),基于zookeeper协调的分布式消息系统,它的最大的特性就是可以实时的处理大量数据以满足各种需求场景:比如基于hadoop的批处理系统、... 查看详情

关于分布式session的几种实现方式

分布式Session的几种实现方式1.基于数据库的Session共享2.基于NFS共享文件系统3.基于memcached的session,如何保证memcached本身的高可用性?4.基于resin/tomcatweb容器本身的session复制机制5.基于TT/Redis或jbosscache进行session共享。6.基于cookie进... 查看详情

分布式session的几种实现方式

一。分布式Session的几种实现方式1.基于数据库的Session共享2.基于NFS共享文件系统3.基于memcached的session,如何保证memcached本身的高可用性?4.基于resin/tomcatweb容器本身的session复制机制5.基于TT/Redis或jbosscache进行session共享。6.基于cook... 查看详情

分布式session的几种实现方式

1.基于数据库的session共享2.基于NFS共享文件系统3.基于memcached的session,怎么保证session的高可用4.基于resin/tomcatweb容器本身的session复制机制5.基于TT/Redis或jbosscache进行session共享。6.基于cookie进行session共享<1.>SessionReplication方式... 查看详情

session的实现方式

分布式Session的几种实现方式1.基于数据库的Session共享2.基于NFS共享文件系统3.基于memcached的session,如何保证memcached本身的高可用性?4.基于resin/tomcatweb容器本身的session复制机制5.基于TT/Redis或jbosscache进行session共享。6.基于cookie进... 查看详情

mysqlworkbench操作详解(史上最细)(代码片段)

MysqlWorkSpace右键新建的数据库BMI,设置为此次连接的默认数据库,接下来的所有操作都将在这个数据库下进行将bmi下拉单展开,点击Table,右键创建Table:给Table命名,添加Column,设置Column的Datatype,P... 查看详情

springsession解决分布式session问题的实现原理

使用SpringSession和Redis解决分布式Session跨域共享问题上一篇介绍了如何使用spring Session和Redis解决分布式Session跨域共享问题,介绍了一个简单的案例,下边就学习一下SpringSession的实现原理。注:以下步骤是基于XML的方式实现Spr... 查看详情

[redis]基于redis的分布式锁

前言分布式锁一般有三种实现方式:1.数据库乐观锁;2.基于Redis的分布式锁;3.基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。可靠性首先,为了确保分布式锁可用,我们至少要确保锁的实现同时... 查看详情

史上最简单的springcloud教程|第六篇:分布式配置中心(springcloudconfig)

...务来保存各个服务的配置文件。它就是SpringCloudConfig。在分布式系统中 查看详情

黑马点评项目总结(代码片段)

...1.优惠券秒杀下单2.超卖问题3.一人一单功能4.基于redis的分布式锁a.setnx命令b.普通setnx分布式锁出现的问题四、消息队列优化五、达人探店1.发布探店笔记2.实现点赞功能3.点赞排行榜六、好友关注1.关注和取关2.共同关注3.关注推送... 查看详情

史上最简单的springcloud教程|第六篇:分布式配置中心(springcloudconfig)(finchley版本)

...务来保存各个服务的配置文件。它就是SpringCloudConfig。在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。在SpringCloud中,有分布式配置中心组件springcloudconfig,它支... 查看详情

史上最简单的springcloud教程|第七篇:高可用的分布式配置中心(springcloudconfig)

最新Finchley版本请访问:https://www.fangzhipeng.com/springcloud/2018/08/30/sc-f7-config/或者http://blog.csdn.net/forezp/article/details/81041045上一篇文章讲述了一个服务如何从配置中心读取文件,配置中心如何从远程git读取配置文件,当服务实例很多... 查看详情

spark运行原理史上最详细

...ngxing/article/details/81746988Spark应用程序以进程集合为单位在分布式集群上运行,通过driver程序的main方法创建的SparkContext对象与集群交互。1、Spark通过SparkContext向Clustermanager(资源管理器)申请所需执行的资源(cpu、内存等)2、Cluster 查看详情

zookeeper事件监听-史上最详解读(代码片段)

...最后疯狂创客圈亿级流量高并发IM实战系列疯狂创客圈Java分布式聊天室【亿级流量】实战系列之-25【博客园总入口】写在前面?大家好,我是作者尼恩。目前和几个小伙伴一起,组织了一个高并发的实战社群【疯狂创客圈】。正... 查看详情

redis分布式缓存,是如何实现多台服务器session实时共享的(代码片段)

现在提到多服务器的共享session,几乎都是回答用redis。对于redis实现几台服务器共享session,不是很理解。假如一个网站分别部署在ABC3台服务器上,他们的代码都是相同的。用户在访问的过程中是随机切换到其他服务器,使用redis... 查看详情

分布式应用session会话管理-基于redis(代码片段)

...服务器下不会出现共享问题,现在应用部署方式都是分布式,或者集群部署,这样必然会面临一个问题,session共享。session共享的解决方案也有很多,一、web服务器的粘性请求,比如采用nginx请求分发,... 查看详情

史上最简单的springcloud教程|第八篇:消息总线(springcloudbus)

...或者http://blog.csdn.net/forezp/article/details/81041062SpringCloudBus将分布式的节点用轻量的消息代理连接起来。它可以用于广播配置文件的更改或者服务之间的通讯,也可以用于监控。本文要 查看详情

redis分布式session

...stTomcatCoyoteAdapterhttp://localhost:8081/login?username=lisi&password=321分布式session分布式环境session失效问题session粘粘session复制session集中式存储优点:实现简单缺点:可扩展性差、分配不均衡缺点:每台机器存储全量数据,比较占机器内存s... 查看详情