尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Redis实现短信验证码登录与分布式会话管理
1. 项目背景与需求分析在当今互联网应用中短信验证码登录已成为主流的用户认证方式之一。相比传统账号密码登录短信验证码登录具有以下优势无需记忆复杂密码降低用户使用门槛通过手机号直接验证用户身份安全性更高注册登录流程合二为一提升用户体验然而传统的基于Session的实现方式在分布式环境下会面临Session共享问题。当应用部署在多台服务器时用户的登录状态无法在不同服务器间共享导致需要重复登录。这正是Redis大显身手的地方——通过将Session数据存储在Redis中我们可以轻松实现跨服务器的登录状态共享。2. 技术选型与架构设计2.1 Redis的优势分析选择Redis作为会话存储主要基于以下几点考虑内存存储Redis基于内存操作读写性能极高适合高频访问的会话数据丰富的数据结构支持String、Hash等多种数据结构可以灵活存储用户信息过期机制内置TTL(Time-To-Live)支持可自动清理过期会话高可用通过Redis集群可实现数据的高可用和负载均衡2.2 系统架构设计整体架构采用前后端分离模式客户端 → Nginx → 应用集群(Spring Boot) → Redis集群 → MySQLNginx负责负载均衡和静态资源服务应用服务器无状态设计所有会话数据存储在RedisMySQL作为持久化存储保存用户基本信息3. 核心功能实现3.1 短信验证码发送验证码生成与存储的关键代码如下Override public Result sendCode(String phone) { // 1. 校验手机号格式 if (!RegexUtils.isPhoneValid(phone)) { return Result.fail(手机号格式错误); } // 2. 生成6位随机验证码 String code RandomUtil.randomNumbers(6); // 3. 保存验证码到Redis有效期5分钟 stringRedisTemplate.opsForValue().set( LOGIN_CODE_KEY phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES ); // 4. 模拟发送短信实际项目应接入短信服务商API log.debug(发送短信验证码成功手机号{}验证码{}, phone, code); return Result.ok(); }注意事项实际生产环境中应对短信发送频率进行限制防止恶意刷短信。常见的做法有同一手机号60秒内只能发送一次每日发送次数限制IP频率限制3.2 验证码登录实现登录流程的核心逻辑Override public Result login(LoginFormDTO loginForm) { // 1. 校验手机号 String phone loginForm.getPhone(); if (!RegexUtils.isPhoneValid(phone)) { return Result.fail(手机号格式错误); } // 2. 从Redis获取验证码并校验 String cacheCode stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY phone); String code loginForm.getCode(); if (cacheCode null || !cacheCode.equals(code)) { return Result.fail(验证码错误); } // 3. 查询用户是否存在 User user query().eq(phone, phone).one(); if (user null) { // 4. 新用户自动注册 user createUserWithPhone(phone); } // 5. 保存用户信息到Redis String token UUID.randomUUID().toString(); UserDTO userDTO BeanUtil.copyProperties(user, UserDTO.class); // 5.1 将UserDTO转为Map MapString, Object userMap BeanUtil.beanToMap(userDTO, new HashMap(), CopyOptions.create() .setIgnoreNullValue(true) .setFieldValueEditor((fieldName, fieldValue) - fieldValue ! null ? fieldValue.toString() : null)); // 5.2 存储Hash结构 String tokenKey LOGIN_USER_KEY token; stringRedisTemplate.opsForHash().putAll(tokenKey, userMap); // 5.3 设置token有效期 stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES); // 6. 删除已使用的验证码 stringRedisTemplate.delete(LOGIN_CODE_KEY phone); // 7. 返回token return Result.ok(token); }关键点说明使用Hash结构存储用户信息相比String更节省内存空间设置合理的TTL通常30分钟-2小时登录成功后立即删除验证码防止重复使用使用UUID生成唯一token作为Redis key4. 登录状态校验设计4.1 双拦截器架构为解决token续期问题我们采用双拦截器设计RefreshTokenInterceptor负责所有请求的token刷新LoginInterceptor负责需要登录的接口校验// 刷新token拦截器 public class RefreshTokenInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 1. 获取token从header或cookie String token getToken(request); if (StrUtil.isBlank(token)) { return true; } // 2. 查询Redis用户信息 String tokenKey LOGIN_USER_KEY token; MapObject, Object userMap stringRedisTemplate.opsForHash().entries(tokenKey); if (userMap.isEmpty()) { return true; } // 3. 转换为UserDTO并保存到ThreadLocal UserDTO userDTO BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); UserHolder.saveUser(userDTO); // 4. 刷新token有效期 stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES); return true; } } // 登录校验拦截器 public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (UserHolder.getUser() null) { response.setStatus(401); return false; } return true; } }4.2 拦截器配置Configuration public class MvcConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { // 刷新token拦截器优先级高 registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)) .addPathPatterns(/**).order(0); // 登录校验拦截器优先级低 registry.addInterceptor(new LoginInterceptor()) .excludePathPatterns( /user/code, /user/login, /blog/hot, /shop/**, /shop-type/**, /upload/** ).order(1); } }设计要点RefreshTokenInterceptor拦截所有请求order0LoginInterceptor只拦截需要登录的接口order1通过ThreadLocal保存用户信息避免多次查询Redis5. 性能优化与安全加固5.1 Redis存储优化使用Hash代替String存储用户信息时使用Hash结构比String更节省内存特别是当用户信息字段较多时合理设置TTL根据业务场景设置合适的过期时间通常验证码5分钟登录token30分钟-2小时批量删除用户退出时应删除所有相关keypublic Result logout(String token) { // 删除Redis中的token String tokenKey LOGIN_USER_KEY token; stringRedisTemplate.delete(tokenKey); // 清除ThreadLocal UserHolder.removeUser(); return Result.ok(); }5.2 安全防护措施Token安全使用足够随机的UUID生成token通过HTTPS传输token设置HttpOnly和Secure标志如果使用Cookie防暴力破解验证码错误次数限制IP访问频率限制敏感操作二次验证修改密码等敏感操作需再次验证短信验证码6. 常见问题排查6.1 验证码发送失败可能原因手机号格式校验不通过Redis连接异常短信服务商接口限制排查步骤检查手机号正则表达式查看Redis连接配置检查短信服务商返回的错误码6.2 登录状态失效可能原因Redis key过期未续期拦截器顺序配置错误ThreadLocal未正确清理解决方案确保RefreshTokenInterceptor正确配置且order0检查Redis的TTL设置在拦截器afterCompletion中清理ThreadLocalOverride public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserHolder.removeUser(); }7. 扩展思考7.1 多端登录支持当前方案中同一账号在不同设备登录会生成不同的token。如果需要实现类似微信的一处登录处处在线效果可以使用用户ID作为Redis key的一部分登录时检查是否已存在该用户的token强制下线已登录的设备通过消息队列通知7.2 分布式锁优化在高并发场景下新用户注册可能存在并发问题。可以使用Redis分布式锁确保原子性public User createUserWithPhone(String phone) { String lockKey lock:user: phone; try { // 尝试获取锁 boolean locked stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, 1, 10, TimeUnit.SECONDS); if (!locked) { // 获取锁失败重试或抛异常 throw new RuntimeException(操作太频繁); } // 再次检查用户是否已存在 User user query().eq(phone, phone).one(); if (user ! null) { return user; } // 创建新用户 user new User(); user.setPhone(phone); user.setNickName(user_ RandomUtil.randomString(8)); save(user); return user; } finally { // 释放锁 stringRedisTemplate.delete(lockKey); } }7.3 异地登录检测通过记录登录IP可以实现简单的异地登录检测// 登录成功后记录IP String ip request.getRemoteAddr(); stringRedisTemplate.opsForValue().set( user:login:ip: userDTO.getId(), ip, 30, TimeUnit.MINUTES ); // 校验时比对IP String lastIp stringRedisTemplate.opsForValue().get(user:login:ip: userId); if (lastIp ! null !lastIp.equals(currentIp)) { // 发送异地登录提醒 sendLoginAlert(userId, lastIp, currentIp); }
RELATED

相关推荐

WIZnet IO模块选型指南:工业自动化与物联网通信

WIZnet IO模块选型指南:工业自动化与物联网通信

1. WIZnet IO模块选型核心考量维度在工业自动化和物联网设备开发中,选择合适的WIZnet IO模块往往决定着整个项目的通信可靠性和长期维护成本。根据我过去五年在智能工厂项目中集成各类IO模块的经验,选型时需要重点评估以下六个硬指标:1.1 通信…

📅 2026/11/2 4:48:05
XGP会员状态与购买故障分诊:授权票据、支付回调与资格校验

XGP会员状态与购买故障分诊:授权票据、支付回调与资格校验

XGP 获取不了会员状态、点了购买转半天没反应、钱扣了权益没到账——这三句话我在过去两年里至少听过几十遍,而且绝大多数人把它们当成同一个问题,一上来就重装应用、换账号、反复点购买按钮,越折腾越乱。实际上这是三条完全不同的链路&#…

📅 2026/11/2 4:48:05
Jev不该被问什么?技术协作中的提问边界与高效沟通指南

Jev不该被问什么?技术协作中的提问边界与高效沟通指南

1. 从“Jev 不该被问什么”说起:一个被误读的提问边界第一次看到“Jev 不该被问什么”这个标题,我脑子里蹦出来的不是某个具体的人,而是一类场景。你在技术群、项目复盘会、开源社区或者线下技术沙龙里,总会遇到某个人——可能是团…

📅 2026/11/2 4:48:05
MORE NEWS

更多资讯

📰

AI预测美联储利率决策:机器学习在量化投资中的应用

1. 项目背景与核心价值最近金融圈最热的话题莫过于美联储的降息时间表了。作为一名在量化投资领域摸爬滚打多年的从业者,我发现传统预测方法在解读美联储政策转向时总是慢半拍。这促使我尝试用AI预测模型结合多维数据源,构建一个更精准的利率决策预测框架…

📰

从零构建AI工程能力:知识体系、实战项目与踩坑全记录

很多人学AI工程最大的误区,就是以为从“调库”开始就够了。我见过太多人,PyTorch用得飞起,但问一句“你的模型为什么在这个数据集上表现不好”,就答不上来。这个“ai-engineering-from-scratch”项目,说白了就是把自己…

📰

AI工程从零起步:系统构建、模型部署与实战指南

这两年"ai-engineering"这个词被提得越来越密,尤其是大模型冒出来之后,几乎每周都有人跑来问我:"我零基础,想走AI工程这条路,到底该怎么开始?"说实话,这个问题我也被问了无…

📰

C语言内存池状态机:模拟与追踪dead cell生命周期

1. 项目概述:这道题不是考C语言语法,而是考你能不能把“细胞死亡”翻译成内存操作看到标题【C语言】每日一题afeng111 and dead cells,别急着翻《C Primer Plus》——这根本不是一道传统意义上的编程题。它背后藏着一个在嵌入式开发、游戏引擎…

📰

Twitter运营自动化转型:工具链构建与效率提升实战

1. 从手动到自动化的Twitter运营转型之路三年前我刚接手公司Twitter账号时,每天要花4小时手动发推、回复评论、追踪热点。直到有次凌晨两点还在修改活动海报时,我意识到必须改变这种低效模式。经过18个月的反复试错,我的运营效率提升了600%&a…

📰

ysyx:pa批处理系统架构设计与优化实践

1. 什么是ysyx:pa.批处理系统ysyx:pa.批处理系统是一个专注于高效处理批量任务的系统框架。"ysyx"可能是某个组织或项目的代号缩写,"pa"在这里可能代表"Parallel Automation"(并行自动化)或"Process Auto…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬