资讯动态

基于Redis的短信登录模块设计:从验证码到登录态的全链路解析

发布时间:2026/9/9 21:09:14 来源:尧图企业网站定制
1. 项目里的短信登录到底在解决什么问题做了几年后端开发你会发现很多教学项目里塞了一堆功能但真正面试时值得反复咀嚼的往往就那么几个模块。黑马点评这个项目里短信登录就是那个最值得扣细节的部分——它看起来只是一个“手机号验证码登录”但背后牵扯到会话管理、Redis缓存策略、拦截器设计、并发安全等一系列老生常谈但又必须说清楚的点。先说清楚这个模块的业务背景。黑马点评是一个仿大众点评的商户探店类项目用户需要浏览店铺、点赞、关注、发布笔记。这些操作都要求识别“当前操作的人是谁”所以登录是所有业务的前置条件。而短信登录的流程很典型用户在登录页输入手机号点击获取验证码后端生成6位随机数调用短信服务商发送到用户手机用户输入验证码后完成登录随后在一段时间内保持登录状态。我见过的很多初学者做登录的第一反应就是往session里塞用户信息完事。这样做在单体应用、单机部署、用户量不大的场景下确实能跑但黑马点评这个项目之所以用Redis来接管登录态是为了模拟真实企业级项目的做法——把会话数据从进程内存里挪到独立缓存中间件中这样即使服务重启、多实例部署用户登录态也不会丢失这也是后续做集群扩展的前提。更重要的是这个模块几乎把Java后端面试里会话管理相关的考点一网打尽session与token的区别、Redis key设计、过期时间刷新策略、拦截器执行时机、ThreadLocal线程隔离等。所以不管你是在校生、培训班出来的、还是工作几年想跳槽这个模块都值得耐着性子拆开揉碎看懂。2. 为什么不用session而是选择Redis保存登录状态2.1 session方案的两个致命问题如果你用session保存登录用户最直接的痛点是session数据默认存在Tomcat进程的内存里。单机部署一切正常但一旦你要部署多台服务器做负载均衡用户的请求被分发到不同机器时session就丢了。解决办法要么是引入session共享组件要么是搞粘性会话不管是哪种都是在给架构添乱。第二个问题是session的有效期管理不够灵活。服务器端的session超时时间通常是全局配置的你很难针对“记住我”这种需求单独定制。而用Redis保存token过期时间可以精确到每一个key用户勾选了“7天免登录”就设置7天过期没勾选就30分钟过期互不干扰干干净净。2.2 Redis方案的完整链路在黑马点评里实现逻辑是这样的用户提交手机号后端校验格式生成6位随机验证码存入Rediskey设计为login:code:手机号value为验证码过期时间5分钟。调用短信服务商接口发送验证码项目里通常是模拟发送直接打印到控制台或日志里。用户输入验证码后端取出Redis里的验证码比对匹配则说明手机号归属无误执行登录逻辑。查询用户表若手机号对应的用户不存在则自动注册一个新用户这是很多项目的常见做法降低用户门槛。登录成功后生成一个随机的UUID作为token将用户信息转为Hash结构存入Rediskey设计为login:token:UUID设置过期时间30分钟。前端拿到token后存储在本地后续每次请求在请求头中携带这个token后端通过拦截器统一校验。这套链路的核心思想是服务器不保存session只保存“凭据与用户数据的映射关系”。客户端持有token服务器持有token对应的用户数据双方各管一段天然适合前后端分离架构。2.3 用Redis存用户信息的细节操作我用Redis存用户信息时没有用简单的String序列化整个User对象而是用了Redis的Hash结构。原因有几个第一Hash结构支持对单个字段做操作。比如我只想更新用户的昵称直接hset一个字段就行不用整个value取出来反序列化再塞回去。第二可视化和排障更方便。在redis-cli里输入hgetall login:token:xxxx直接能看到这个用户的id、手机号、昵称不用借助反序列化工具去解析二进制String。第三省内存。Redis的Hash在字段数量少时内部采用ziplist编码比JSON字符串的存储效率高不少。具体代码层面// 保存用户信息到Redis使用Hash结构 stringRedisTemplate.opsForHash().putAll( RedisConstants.LOGIN_USER_KEY token, BeanUtil.beanToMap(user, new HashMap(), CopyOptions.create() .setIgnoreNullValue(true) .setFieldValueEditor((fieldName, fieldValue) - fieldValue.toString())) ); // 设置过期时间 stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY token, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);注意这里有个很关键的细节BeanUtil.beanToMap转出来的Mapvalue必须是String类型否则HMSET操作会报错。所以要用setFieldValueEditor把所有字段值统一转成字符串包括Long类型的id和userId。这个坑我一开始没注意运行时直接整了个类型转换异常排查了半天。3. 核心流程逐段拆解从发送验证码到登录成功3.1 发送验证码的代码逻辑与细节发送验证码的接口是整个登录流程的起点代码本身不多但每一行都有讲究。先看核心实现Override public Result sendCode(String phone, HttpSession session) { // 1. 校验手机号格式 if (RegexUtils.isPhoneInvalid(phone)) { return Result.fail(手机号格式错误); } // 2. 生成6位随机验证码 String code RandomUtil.randomNumbers(6); // 3. 保存验证码到Redis5分钟有效 stringRedisTemplate.opsForValue().set( RedisConstants.LOGIN_CODE_KEY phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES ); // 4. 发送验证码模拟 log.debug(发送短信验证码成功{}, code); return Result.ok(); }这里有几个值得展开的点。手机号校验这块项目里的RegexUtils.isPhoneInvalid通常是写了个正则表达式去匹配运营商号段。国内手机号目前是11位以1开头第二位通常是3、5、7、8、9。面试时如果问到你你就说这里是为了防非法请求打到短信服务商上。因为短信服务商是计费的如果接口被恶意刷每一发一条验证码都是钱所以校验格式是第一道防线后面还可以加频率限制比如同一手机号60秒内只能发一次。验证码用RandomUtil.randomNumbers(6)生成这个工具类是Hutool提供的生成的是纯数字的6位随机数。为什么不搞成字母加数字因为短信验证码的使用场景是用户在手机上一眼扫过并手动输入纯数字可以明显降低输错概率。当然现在很多大厂已经改用“4位或6位纯数字语音播报”的方案了本质还是为了提高转化率。验证码的key设计为login:code:手机号这个前缀login:code是业务标识中间用冒号分隔冒号在Redis里是默认的key层级分隔符在可视化工具里可以形成目录树结构便于管理和排查。过期时间设置为5分钟是业务上能接受的验证码有效期——太短影响用户体验太长增加被暴力破解的风险。3.2 验证码校验与登录态创建用户输入验证码提交后后端执行login方法这是整个模块的核心。逻辑上分成三个关键步骤校验验证码、查询或创建用户、保存用户到Redis。Override public Result login(LoginFormDTO loginForm, HttpSession session) { String phone loginForm.getPhone(); String code loginForm.getCode(); // 1. 校验验证码 String cacheCode stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY phone); if (cacheCode null || !cacheCode.equals(code)) { return Result.fail(验证码错误); } // 2. 查询用户不存在则创建 User user userService.query() .eq(phone, phone).one(); if (user null) { user createUserWithPhone(phone); } // 3. 保存用户信息到Redis并生成token String token UUID.randomUUID().toString(true); // 转Hash并存储... return Result.ok(token); }校验验证码时一定要先判断cacheCode是否为null再判断是否相等。如果Redis里没有这个key说明验证码已过期或者压根没发过cacheCode.equals(code)会抛空指针这是新手最容易写的bug。有个细节值得思考为什么校验完验证码之后不删除Redis里的验证码key按道理验证码是一次性的用完之后应该立刻删掉防止被重放攻击。我见过很多生产项目是删除的。但黑马点评的做法是不删因为验证码本身的过期时间只有5分钟且token已经生成风险窗口极短。你可以在面试时主动提到这个点然后补充一句“如果要防止并发重复提交可以在校验前先删除验证码key并判断删除结果只有删除成功才继续执行登录”这一下就能体现你对并发场景的思考。查询用户这里用了MyBatis Plus的query().eq(phone, phone).one()one()方法要求结果必须是一条如果查到多条会直接报错。实际业务里手机号在用户表里是唯一索引所以不会出现多条。但在写代码的时候你也可以用selectOne加条件构造器或者手动加last(limit 1)兜底保证查询不会出问题。用户不存在时自动注册这个逻辑挺有意思。它把“注册”和“登录”合并成了一个动作用户第一次验证手机号成功系统自动帮你建号免去了填用户名、设密码的繁琐流程。这也是现在很多App的主流做法——验证码即身份。createUserWithPhone里面就是new一个User对象set手机号set一个随机生成的昵称然后调用save方法入库。随机昵称这里项目里用的是user_加随机数字你也可以用用户 手机号后四位提高辨识度。3.3 登录成功后跳转首页的问题热词里有一条“黑马点评为什么登录后跳转首页”这个在前后端分离项目里其实是前端路由该管的事。登录成功后后端返回token前端拿到token后调用router.push(/)跳转到首页。但很多人会把“跳转”理解成后端行为其实后端只管返回数据URL变化是浏览器的行为。有一种特殊情况是后端用重定向比如return redirect:/index.html这种是非前后端分离项目的写法。黑马点评的前端是Vue项目和后端接口完全分离所以登录成功跳首页的实现在前端的axios响应拦截器或登录页的提交回调里。具体代码如下async handleLogin() { const res await axios.post(/user/login, this.loginForm); if (res.data.success) { localStorage.setItem(token, res.data.data); this.$router.push(/); } else { this.$message.error(res.data.errorMsg); } }如果你在面试中被问到“登录成功后前端是怎么跳转的”回答的核心在于前端保存token到本地存储然后通过Vue Router的编程式导航切换到首页首页的接口请求会携带token让后端确认用户身份。4. 登录校验机制的实现原理与核心代码4.1 拦截器设计方案从最简单的校验讲起登录校验的常规套路是使用Spring MVC的拦截器HandlerInterceptor在请求进入Controller之前校验token是否有效无效则直接返回401有效则放行并让Controller能拿到当前用户信息。黑马点评项目里的拦截器实现分为两层。这里面的设计很有意思第一层拦截器“只刷新不拦截”第二层拦截器“不刷新只拦截”两者配合把“所有请求都能续期”和“受保护接口必须登录”两个需求同时满足。先看最简单的版本一个拦截器搞定所有校验public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 获取token String token request.getHeader(authorization); if (StrUtil.isBlank(token)) { response.setStatus(401); return false; } // 查询用户 MapObject, Object userMap stringRedisTemplate.opsForHash() .entries(LOGIN_USER_KEY token); if (userMap.isEmpty()) { response.setStatus(401); return false; } // 保存用户到ThreadLocal UserHolder.saveUser(BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false)); // 刷新过期时间 stringRedisTemplate.expire(LOGIN_USER_KEY token, LOGIN_USER_TTL, TimeUnit.MINUTES); return true; } }第一层的token是从请求头authorization里取的这是一种约定俗成的规范。前端在axios请求拦截器里统一加上axios.interceptors.request.use(config { config.headers.authorization localStorage.getItem(token); return config; });后端从header里取token而不是从参数里取是为了避免token出现在请求URL里被日志记录减少token泄露的风险。但是单个拦截器有一个问题查看店铺、浏览笔记这些接口是不需要登录也能访问的如果统一拦截所有路径会导致游客根本没法浏览页面。所以项目里做了路径白名单——对于/user/login、/user/code、/shop/**、/shop-type/**这些接口不拦截只有需要登录的接口才校验token。4.2 双层拦截器方案刷新与拦截分离黑马点评的完整方案里最值得学习的是把“刷新token有效期”和“登录校验”拆成两个拦截器。为什么要拆因为如果只有一个拦截器遇到那些不需要登录的接口比如用户逛店铺拦截器直接放行了token的过期时间不会被刷新用户的登录态就会在访问过程中悄悄过期。等你逛完店铺再去点赞时发现已经掉登录了体验非常差。拆成两层的思路是第一层拦截所有请求只要携带的token有效就刷新过期时间并把这个用户放进ThreadLocal如果没带token或者token无效什么都不做直接放行。第二层只拦截需要登录的接口检查ThreadLocal里有没有用户没有就直接拒绝。第一层拦截器public class RefreshTokenInterceptor implements HandlerInterceptor { private StringRedisTemplate stringRedisTemplate; public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate stringRedisTemplate; } Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(authorization); if (StrUtil.isBlank(token)) { return true; } MapObject, Object userMap stringRedisTemplate.opsForHash() .entries(RedisConstants.LOGIN_USER_KEY token); if (userMap.isEmpty()) { return true; } UserDTO userDTO BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); UserHolder.saveUser(userDTO); // 刷新有效期 stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY token, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); } }第二层拦截器public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (UserHolder.getUser() null) { response.setStatus(401); return false; } return true; } }两层拦截器通过WebMvcConfigurer统一注册时注意顺序必须左边先执行Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)) .addPathPatterns(/**).order(0); registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/user/**, /blog/**, /upload/**) .excludePathPatterns(/user/login, /user/code) .order(1); }第一层拦截所有路径order为0第二层只拦截需要登录的路径order为1。执行顺序上Spring MVC拦截器按order值从小到大执行所以第一层永远先跑。这样游客访问公开接口时第一层直接放行第二层不会执行登录用户访问受保护接口时第一层刷新了过期时间第二层校验通过两个需求同时满足。4.3 ThreadLocal的用法和内存泄漏陷阱ThreadLocal在这里的作用是线程级别的变量共享。一个HTTP请求从进入Tomcat到响应返回整个过程由同一个线程处理所以在拦截器里set进去的用户信息在Controller和Service里都能通过UserHolder.getUser()拿到。这就是“一次请求内全局可见”的语义。但ThreadLocal有个老生常谈的坑使用完必须remove否则在线程池场景下会发生内存泄漏或脏数据。Tomcat的工作线程是复用的这次请求处理完线程归还给线程池ThreadLocal里的数据还留在线程里下个请求又拿到这个线程时ThreadLocalMap里还有上次残留的值就可能出现“串号”。所以项目里UserHolder的实现也考虑到了这一点public class UserHolder { private static final ThreadLocalUserDTO tl new ThreadLocal(); public static void saveUser(UserDTO user) { tl.set(user); } public static UserDTO getUser() { return tl.get(); } public static void removeUser() { tl.remove(); } }而afterCompletion方法里调用UserHolder.removeUser()保证了请求结束后无论如何都会清理ThreadLocal。注意要放到afterCompletion而不是postHandle因为afterCompletion是视图渲染完成之后、整个请求链路结束之前一定会执行的钩子即使Controller抛了异常也会进入这里。5. Redis保存登录态的参数设计与注意事项5.1 key前缀与过期时间怎么定Redis的key设计是一个容易被忽略但非常影响维护体验的细节。黑马点评里定义了一个RedisConstants常量类把所有key前缀和过期时间统一管理public class RedisConstants { public static final String LOGIN_CODE_KEY login:code:; public static final Long LOGIN_CODE_TTL 5L; public static final String LOGIN_USER_KEY login:token:; public static final Long LOGIN_USER_TTL 30L; }key统一带业务前缀用冒号分层这样在Redis里看到login:code:138xxxx就知道这对应手机号138xxxx的验证码看到login:token:xxxxx就知道这对应一个登录token。过期时间这块验证码5分钟是业务通用值登录态30分钟是黑马点评项目的默认值。但在实际生产环境里登录态的时间要根据业务形态来定金融类App通常15分钟就过期电商类App通常是30分钟到2小时社区类App普遍做“7天免登录”。如果你在简历上写了这个项目最好能说清楚“登录态过期时间是可配置的我们项目里默认30分钟”。5.2 登录态续期机制是怎么运作的双层拦截器里第一层对每个携带有效token的请求都会执行expire操作把过期时间重新设置为30分钟。这个做法的本质是只要用户在不断操作登录态就永远不会过期一旦用户超过30分钟没有发任何请求token自然过期需要重新登录。这里有一个性能上的考虑点每次请求都执行一次expire比较好理解也就是多一次Redis写入操作但Redis本身就是内存操作微秒级响应相比一次HTTP请求动辄几十毫秒的耗时这个开销完全可接受。而且expire命令在Redis内部是把TTL数据更新到字典里O(1)复杂度不会随着数据量增加而变慢。5.3 用户信息存Hash还是存String前面提到了Hash的几个优点这里再做一次对比方便你在面试时把这个问题讲透对比项HashStringJSON存储结构field-value映射序列化后的字符串单字段更新支持hsetO(1)需要整个get、反序列化、修改、序列化、set内存效率字段少时用ziplist压缩取决于JSON长度读取全量hgetall一次搞定get一次搞定读取单字段hget指定field必须全量get后反序列化取值从项目角度看Hash的“部分更新”能力在用户信息变更场景里很有用比如用户改头像你只需要hset一个avatar字段而不用把整个用户对象重新序列化存一遍。不过Hash也有不方便的地方BeanUtil.beanToMap转出来所有值都是String从Redis读出来再BeanUtil.fillBeanWithMap填充回对象时需要手动处理类型转换。黑马点评的做法是把User对象转换成一个精简的UserDTO只保留id、手机号、昵称、头像这几个脱敏字段避免把一些敏感数据如密码存到Redis里。这是一个很值得借鉴的安全习惯——Redis里不要放完整用户对象尤其不能放密码。6. 环境配置与项目初始化的几个注意点6.1 数据库和Redis的准备黑马点评项目用到的数据库是MySQLRedis版本要求4.0以上主要是为了用LOGIN_CODE_TTL这种TimeUnit参数其实核心功能不挑版本但建议用5.0以上稳定版。我实操下来有两个环节特别容易卡住一是数据库初始化脚本。项目提供的有hmdp.sql文件里面包含了店铺、用户、笔记等表结构和初始数据。导入时注意MySQL的版本8.0以上的utf8mb4字符集对中文支持更好如果你用5.7建议手动确认一下表的字符集防止出现中文乱码。二是Redis的启动。如果你在Windows上开发Redis官方不提供原生Windows版本通常用微软维护的旧版本或借助WSL运行。我个人的建议是直接在项目里用Docker起一个Redis容器一条命令搞定还能顺便学习Docker的使用docker run -d --name redis -p 6379:6379 redis:7.0Windows下Docker Desktop如果资源紧张也可以直接用压缩包版Redis但对新版本支持有限。优先推荐Docker方案。6.2 前端项目的联调配置黑马点评的前端是Vue项目启动前要确认后端端口和前端代理配置一致。前端项目里通常有一个vue.config.js里面配置了devServer的proxy把/api开头的请求代理到后端的localhost:8080。如果你改了后端的端口这里要同步改否则请求会404。前端和后端的登录流程联调时最常遇到的坑是跨域。黑马点评项目在后端加了CorsConfig处理跨域具体是允许http://localhost:8080这个前端地址的请求允许携带头信息。如果你自己写接口时遇到了跨域报错检查三件事后端是否放行了对应来源、是否放行了authorization这个自定义请求头、是否允许了OPTIONS预检请求。6.3 项目跑起来后第一时间验证什么环境配置好、项目启动成功后建议不要急着写功能先按以下顺序把登录链路完整跑一遍打开前端页面进入登录页输入一个手机号点击获取验证码。看后端控制台日志确认验证码生成并打印出来了。输入验证码登录看Network面板里登录接口的响应是否返回了token。打开Redis客户端输入keys login:*确认能看到login:code:手机号和login:token:xxx。在Redis客户端里输入hgetall login:token:xxx确认Hash结构的字段值都是String。随便访问一个需要登录的接口确认请求头里带了authorization且请求能正常返回。这一套验证做完你就把登录模块从“能跑”推进到了“懂跑”的阶段。7. 常见问题与面试高频考点整理7.1 常见问题速查表问题可能原因排查方向获取验证码后Redis里没有keyRedis连接配置错误或key过期检查Redis连接池配置确认login:code:前缀拼写登录提示验证码错误但Redis里有值比对时前后类型不一致或空指针在比对前打印cacheCode和code确认两者确实相同token返回了但后续接口全部401拦截器没生效或token没传到后端检查拦截器注册的pathPatterns用浏览器Network看请求头登录后操作一会就掉线第一层拦截器没配order或路径没覆盖全检查RefreshTokenInterceptor的order是否为0addPathPatterns是否包含所有路径多实例部署后登录态闪烁Redis的key没做持久化或不同环境Redis独立确认所有实例连同一个RedisThreadLocal里取不到用户第一层拦截器没放行或数据没存进去在preHandle里打日志确认userMap非空且fillBeanWithMap执行成功7.2 面试时怎么把这个模块讲出深度黑马点评几乎成了Java后端简历里的“标配项目”面试官对这个项目太熟了如果你只讲“我用了Redis存验证码和用户信息”那跟没写这个项目没区别。要拉开差距你至少要能在逻辑上回答清楚下面几个问题为什么用Redis存验证码而不是用session或数据库验证码是高频写入和读取的数据用数据库存储会平白增加磁盘IO而且还要自己处理过期逻辑用session存的话验证码数据散落在各个Tomcat进程里分布式环境下对不上号。Redis天然支持过期时间读写都是内存操作是最匹配这个场景的中间件。Redis存用户信息为什么用Hash结构可以从单字段更新、内存压缩、字段隔离三个角度回答结合hset只改一个字段的场景说明白就行。两层拦截器的设计你是怎么想的回答核心就一句话第一层负责给所有有效token续期第二层负责拦截未登录请求。然后补充说明如果不拆开会出现“用户浏览公开页面太久导致登录态过期”的问题。token过期后前端怎么处理前端axios响应拦截器里判断HTTP状态码为401然后清除本地token跳转登录页。你可以补充说项目里用了重试机制通过响应拦截器统一处理401减少重复写业务代码。如何防止验证码被刷或并发重复提交限制同一手机号发送验证码的频率比如60秒内只能发一次登录时先删除验证码key再比对保证验证码一次性。你主动提起这些话题面试官一般会顺着深入问这时把你踩过的坑和经验说出来效果会非常好。7.3 项目中登录模块可以继续扩展的方向这个模块做完之后你还可以自己往深了加一些东西既能加深理解也能为面试准备加分点加一个登录错误次数限制连续输错5次验证码就锁定该手机号10分钟防止暴力破解。把token改成JWT利用JWT的无状态特性去掉Redis的“保存用户信息”环节但要注意JWT无法主动失效的问题。给登录接口加限流比如用Redis的INCR命令统计单位时间内的请求次数超过阈值直接拒绝。用布隆过滤器拦截已经注销的手机号在登录前快速过滤无效用户。这些扩展点每一个都可以单独花半天时间实现实现完之后你对Redis的理解会提升一个档次。8. 代码结构梳理与简历上该怎么描述8.1 登录模块涉及的核心类一览类名作用UserController登录接口入口包含sendCode和login两个方法UserService / UserServiceImpl核心业务逻辑验证码校验、用户创建、token生成LoginInterceptor登录校验拦截器检查用户是否已登录RefreshTokenInterceptor刷新token过期时间拦截器所有请求都会执行WebMvcConfig拦截器注册与路径配置UserHolder基于ThreadLocal的当前用户信息持有器RedisConstantsRedis key前缀和过期时间常量UserDTO脱敏后的用户信息传输对象面试前把这个表里的每个类的职责都默写一遍比反复看代码更高效。画结构图也可以自己用纸笔画一遍请求的流转路径从发起登录请求到Redis存储再到后续接口校验每个环节对应哪个类哪个方法都要熟练。8.2 简历中如何描述这个模块简历上不要写“实现了手机号验证码登录”这种描述没有任何区分度。推荐这样写负责用户短信登录模块基于Redis缓存验证码与登录token使用Hash结构存储脱敏用户信息设计双层Spring MVC拦截器实现登录校验与token有效期动态续期通过ThreadLocal实现请求级用户信息共享避免多实例部署下的session不共享问题。这样一段话覆盖了技术栈Redis、Spring MVC、核心技术点Hash存储、双层拦截器、动态续期、解决的问题session不共享面试官一看就知道你对这个模块有深入理解。写简历的时候还可以加一句“验证码接口做了手机号格式校验登录逻辑支持用户不存在时自动注册”体现你对业务细节的关注。我个人在实际带新人的过程中发现大多数人对这个模块的记忆停留在“代码能跑”的层面一深入问就含糊。真正有效的方法是把整个请求链路在草稿纸上画一遍从浏览器发起请求到Redis数据变更每一步都写清楚对应的方法和数据结构画完一遍再配合上面的常见问题做模拟面试效果立竿见影。最后再分享一个小技巧调试这个模块时可以把Redis的过期时间临时调成10秒然后反复请求接口观察登录态什么时候过期亲身感受一下双层拦截器续期的效果。这种动手实验比盯着代码想当然要深刻得多也是理解“为什么第一层拦截所有请求”最快的方式。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价