SpringBoot 实现短信验证码登录(单机 Session + Redis 分布式会话)
前言
在小程序类业务系统中,短信验证码登录是很常见的登录方案。单机环境可以直接使用 HttpSession 保存验证码和用户信息;但服务集群部署时,原生 Session 无法跨服务器共享,会出现登录状态丢失。本文就从单机 Session 版本 到Redis 分布式会话版本完整实现,包含流程图、接口、代码、拦截器配置。
一、整体业务流程
1. 发送短信验证码流程
- 前端传入手机号,后端校验手机号格式
- 生成 6 位随机数字验证码
- 将验证码存入 Session,设置过期时间
- 调用短信服务,把验证码发送到用户手机
- 返回成功结果给前端
接口信息 请求方式:POST 请求路径:
/user/code请求参数:phone(手机号)
2. 短信验证码登录流程
- 前端传入手机号 + 用户收到的验证码
- 后端校验手机号格式
- 从 Session 取出缓存的验证码,和前端传入验证码比对
- 校验通过:根据手机号查询用户
- 用户不存在,则自动新建用户;存在则直接复用
- 保存用户信息到 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();
}
- 登录业务逻辑
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();
}
- 登录拦截器(校验登录状态)
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();
}
}
- 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);
}
- 改造拦截器(从 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;
}
五、核心总结
- 单机场景:HttpSession 开发简单,适合单体项目;无法集群。
- 分布式场景:使用 Redis 存储登录态,token 作为身份凭证,多台服务器共享登录信息。
- Redis 存储选型:Hash 适合存储用户对象,支持局部更新。
- 拦截器:统一校验登录状态,ThreadLocal 保存用户信息,避免参数层层传递。
- 过期策略:登录信息设置过期时间,每次访问刷新有效期,实现会话续期。
六、拓展优化方向
- 接口限流:防止短信接口被恶意刷手机号
- 验证码过期:验证码也存入 Redis,设置 5 分钟过期
- token 安全:可以增加 token 前缀,防止 key 冲突
- 登录互斥:同一账号在新设备登录,踢掉旧登录 token