分布式登录实战:Session 会话共享改造,Redis 存储用户登录状态

SpringBoot 实现短信验证码登录(单机 Session + Redis 分布式会话)

前言

在小程序类业务系统中,短信验证码登录是很常见的登录方案。单机环境可以直接使用 HttpSession 保存验证码和用户信息;但服务集群部署时,原生 Session 无法跨服务器共享,会出现登录状态丢失。本文就从单机 Session 版本 到Redis 分布式会话版本完整实现,包含流程图、接口、代码、拦截器配置。

一、整体业务流程

1. 发送短信验证码流程

  1. 前端传入手机号,后端校验手机号格式
  2. 生成 6 位随机数字验证码
  3. 将验证码存入 Session,设置过期时间
  4. 调用短信服务,把验证码发送到用户手机
  5. 返回成功结果给前端

接口信息 请求方式:POST 请求路径:/user/code 请求参数:phone(手机号)

2. 短信验证码登录流程

  1. 前端传入手机号 + 用户收到的验证码
  2. 后端校验手机号格式
  3. 从 Session 取出缓存的验证码,和前端传入验证码比对
  4. 校验通过:根据手机号查询用户
  5. 用户不存在,则自动新建用户;存在则直接复用
  6. 保存用户信息到 Session,完成登录

接口信息 请求方式:POST 请求路径:/user/login 请求参数:phone、code

二、单机 Session 版本代码实现

1. 发送验证码 Controller & Service

java 复制代码
@override
public Result sendcode(string phone, httpsession session) {
    //1.校验手机号格式
    if (!regexutils.isphoneinvalid(phone)) {
        return result.fail("手机号格式错误!");
    }
    //2.生成6位随机验证码
    string code = randomutil.randomnumbers(6);
    //3.验证码存入session
    session.setattribute("code", code);
    log.debug("发送短信验证码成功,验证码:{}", code);
    return result.ok();
}
  1. 登录业务逻辑
java 复制代码
public Result login(loginformdto loginform, httpsession session) {
    //1.校验手机号
    string phone = loginform.getphone();
    if (regexutils.isphoneinvalid(phone)) {
        return result.fail("手机号格式错误!");
    }
    //2.校验验证码
    object cachecode = session.getattribute("code");
    string code = loginform.getcode();
    if (cachecode == null || !cachecode.tostring().equals(code)) {
        return result.fail("验证码错误");
    }
    //3.根据手机号查询用户
    user user = query().eq("phone", phone).one();
    //4.用户不存在,自动创建用户
    if (user == null) {
        user = createuserwithphone(phone);
    }
    //5.用户信息存入session
    session.setattribute("user", user);
    return result.ok();
}
  1. 登录拦截器(校验登录状态)
java 复制代码
public class logininteceptor implements handlerinterceptor {
    @override
    public boolean prehandle(httpservletrequest request, httpservletresponse response, object handler) throws exception {
        //获取session
        httpsession session = request.getsession();
        //获取session中的用户
        object user = session.getattribute("user");
        //判断用户是否存在
        if (user == null) {
            //不存在,返回401未登录
            response.setstatus(401);
            return false;
        }
        //存在,存入threadlocal,供后续业务使用
        userholder.saveuser((user) user);
        return true;
    }

    @override
    public void aftercompletion(httpservletrequest request, httpservletresponse response, object handler, exception ex) throws exception {
        //请求结束,清除threadlocal
        userholder.removeuser();
    }
}
  1. Web 配置类,注册拦截器
java 复制代码
@configuration
public class webconfig implements webmvcconfigurer {
    @override
    public void addinterceptors(interceptorregistry registry) {
        registry.addinterceptor(new logininteceptor())
                .addpathpatterns("/**")
                .excludepathpatterns(
                        "/user/code",
                        "/user/login",
                        "/shop/**",
                        "/swagger/**"
                );
    }
}

三、单机 Session 存在的问题

HttpSession 默认保存在 Tomcat 服务器的 JVM 内存中。 当项目做集群部署,多台服务器负载均衡时:

  • 用户第一次请求落在服务器 A,Session 保存在 A 内存
  • 第二次请求被负载均衡分发到服务器 B
  • B 服务器没有该 Session,登录状态丢失

👉 解决方案:使用 Redis 实现分布式会话,多服务共享登录状态

四、Redis 分布式会话改造方案

核心思路:不再使用 Tomcat 原生 Session,登录成功后生成唯一 token,用户信息存入 Redis。

Redis 推荐使用 Hash 结构存储用户信息: key:login:user:{token} hash field:用户属性(id,name,phone 等) 优势:可以单独修改某个字段,节省内存,适合存储对象。

1. 改造登录逻辑

java 复制代码
public Result login(loginformdto loginform) {
    //1.手机号、验证码校验逻辑不变
    string phone = loginform.getphone();
    string code = loginform.getcode();
    //校验验证码...
    //查询用户,不存在则新建用户...

    //2.生成唯一token
    string token = uuid.randomuuid().tostring();
    //3.用户对象转map,存入redis hash
    map<string, object> usermap = beanutil.beantomap(user, true, true);
    //4.存入redis,设置过期时间
    string key = "login:user:" + token;
    stringredistemplate.opsforhash().putall(key, usermap);
    stringredistemplate.expire(key, 30, timeunit.minutes);
    //返回token给前端,前端后续请求携带token在请求头
    return result.ok(token);
}
  1. 改造拦截器(从 Redis 读取登录信息)
java 复制代码
public boolean prehandle(httpservletrequest request, httpservletresponse response, object handler) throws exception {
    //1.从请求头获取token
    string token = request.getheader("authorization");
    if (stringutils.isempty(token)) {
        response.setstatus(401);
        return false;
    }
    //2.拼接redis key
    string key = "login:user:" + token;
    //3.查询redis
    map<string, object> usermap = stringredistemplate.opsforhash().entries(key);
    if (usermap.isempty()) {
        response.setstatus(401);
        return false;
    }
    //4.map转回用户对象,存入threadlocal
    userdto userdto = beanutil.maptobean(usermap, userdto.class);
    userholder.saveuser(userdto);
    //5.刷新过期时间
    stringredistemplate.expire(key, 30, timeunit.minutes);
    return true;
}

五、核心总结

  1. 单机场景:HttpSession 开发简单,适合单体项目;无法集群。
  2. 分布式场景:使用 Redis 存储登录态,token 作为身份凭证,多台服务器共享登录信息。
  3. Redis 存储选型:Hash 适合存储用户对象,支持局部更新。
  4. 拦截器:统一校验登录状态,ThreadLocal 保存用户信息,避免参数层层传递。
  5. 过期策略:登录信息设置过期时间,每次访问刷新有效期,实现会话续期。

六、拓展优化方向

  1. 接口限流:防止短信接口被恶意刷手机号
  2. 验证码过期:验证码也存入 Redis,设置 5 分钟过期
  3. token 安全:可以增加 token 前缀,防止 key 冲突
  4. 登录互斥:同一账号在新设备登录,踢掉旧登录 token
相关推荐
HAHAXX82 小时前
电商RPA批量上架通用方案:一套流程如何同时跑通拼多多、抖店、淘宝和跨境平台
java·运维·rpa
\光辉岁月/2 小时前
3.java运算符
java·开发语言
kaiyou20262 小时前
用户研究岗秋招,统计分析能力怎么学习和证明?
数据库·python·学习
liangshanbo12152 小时前
面试题:线上 JavaScript 报错如何快速定位到源码?
开发语言·javascript·ecmascript
mjhcsp2 小时前
DeepSeek V4.1 Flash 批量处理效能实测
java·前端·数据库
ShineWinsu2 小时前
对于Redis:Steam、Geospatial、Hyperloglog、Bitmap、Bitfield类型的解析
数据库·c++·redis·分布式·缓存·面试·zset
lie..3 小时前
30天从零开始学AI应用开发(Day 17):ChromaDB 上手:给本地文档建一个“外挂大脑”
数据库·人工智能·oracle
海绵宝宝转agent3 小时前
操作系统八股开源笔记分享
java·面试