`
前言
上一篇我们实现了 Spring Boot + JWT 登录认证,用户通过用户名和密码登录,登录成功后后端生成 token 返回给前端。
这一篇继续扩展登录功能,实现一个更常见的业务场景:验证码登录。
验证码登录在项目中非常常见,比如:
- 手机号短信验证码登录
- 邮箱验证码登录
- 忘记密码校验验证码
- 注册账号校验验证码
- 修改手机号校验验证码
验证码这种数据有一个特点:
有效时间短,用完就可以删除。
所以它非常适合保存到 Redis 中。
这一篇我们就用 Spring Boot + Redis 实现验证码发送和验证码校验。
一、功能需求
本篇要实现两个接口:
- 发送验证码
- 验证码登录
整体流程:
用户输入手机号 ↓ 请求发送验证码 ↓ 后端生成 6 位验证码 ↓ 验证码保存到 Redis,设置 5 分钟过期 ↓ 用户输入手机号和验证码登录 ↓ 后端从 Redis 查询验证码 ↓ 验证码正确:登录成功,生成 token ↓ 验证码错误或过期:登录失败二、引入 Redis 依赖
在pom.xml中添加 Redis 依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>如果项目中已经有这个依赖,就不用重复添加。
三、配置 Redis
在application.yml中配置 Redis:
spring:redis:host:localhostport:6379database:0如果 Redis 有密码:
spring:redis:host:localhostport:6379password:你的密码database:0如果是较新版本 Spring Boot,也可能使用:
spring:data:redis:host:localhostport:6379database:0具体根据项目版本调整。
四、验证码 key 设计
Redis key 命名要清楚。
本篇验证码 key 设计为:
login:code:手机号比如手机号是:
13800000000那么 Redis key 就是:
login:code:13800000000value 保存验证码:
952713过期时间设置 5 分钟。
五、准备登录参数类
验证码登录时,前端传手机号和验证码。
publicclassCodeLoginDTO{privateStringphone;privateStringcode;// getter、setter 省略}发送验证码时,可以直接用@RequestParam接收手机号。
六、Mapper 层实现
验证码登录成功后,需要根据手机号查询用户。
@MapperpublicinterfaceUserMapper{@Select("select id, username, password, name, phone, create_time, update_time "+"from user where phone = #{phone}")UsergetByPhone(Stringphone);@Insert("insert into user(phone, name, create_time, update_time) "+"values(#{phone}, #{name}, #{createTime}, #{updateTime})")voidinsert(Useruser);}这里提供两个方法:
- 根据手机号查询用户
- 新增用户
如果手机号第一次登录,可以自动注册一个用户。
七、Service 层接口
publicinterfaceLoginService{voidsendCode(Stringphone);StringloginByCode(CodeLoginDTOcodeLoginDTO);}这里:
sendCode负责生成验证码并保存到 Redis。
loginByCode负责校验验证码,登录成功后返回 token。
八、Service 层实现
@ServicepublicclassLoginServiceImplimplementsLoginService{@AutowiredprivateStringRedisTemplatestringRedisTemplate;@AutowiredprivateUserMapperuserMapper;@OverridepublicvoidsendCode(Stringphone){if(phone==null||phone.length()!=11){thrownewRuntimeException("手机号格式不正确");}Stringcode=String.valueOf(newRandom().nextInt(900000)+100000);Stringkey="login:code:"+phone;stringRedisTemplate.opsForValue().set(key,code,5,TimeUnit.MINUTES);System.out.println("验证码:"+code);}@OverridepublicStringloginByCode(CodeLoginDTOcodeLoginDTO){Stringphone=codeLoginDTO.getPhone();Stringcode=codeLoginDTO.getCode();Stringkey="login:code:"+phone;StringredisCode=stringRedisTemplate.opsForValue().get(key);if(redisCode==null){thrownewRuntimeException("验证码已过期");}if(!redisCode.equals(code)){thrownewRuntimeException("验证码错误");}Useruser=userMapper.getByPhone(phone);if(user==null){user=newUser();user.setPhone(phone);user.setName("用户"+phone.substring(7));user.setCreateTime(LocalDateTime.now());user.setUpdateTime(LocalDateTime.now());userMapper.insert(user);}Map<String,Object>claims=newHashMap<>();claims.put("id",user.getId());claims.put("phone",user.getPhone());claims.put("name",user.getName());Stringtoken=JwtUtils.generateJwt(claims);stringRedisTemplate.delete(key);returntoken;}}九、Controller 层实现
@RestController@RequestMapping("/codeLogin")publicclassCodeLoginController{@AutowiredprivateLoginServiceloginService;@PostMapping("/send")publicResultsendCode(@RequestParamStringphone){loginService.sendCode(phone);returnResult.success();}@PostMappingpublicResultlogin(@RequestBodyCodeLoginDTOcodeLoginDTO){Stringtoken=loginService.loginByCode(codeLoginDTO);returnResult.success(token);}}文字说明
发送验证码接口:
POST /codeLogin/send?phone=13800000000验证码登录接口:
POST /codeLogin请求体:
{"phone":"13800000000","code":"952713"}登录成功后返回 token。
十、验证码登录流程说明
验证码登录的核心逻辑在 Service 层。
1. 发送验证码
Stringcode=String.valueOf(newRandom().nextInt(900000)+100000);生成 6 位验证码。
stringRedisTemplate.opsForValue().set(key,code,5,TimeUnit.MINUTES);保存到 Redis,并设置 5 分钟过期。
2. 校验验证码
StringredisCode=stringRedisTemplate.opsForValue().get(key);从 Redis 中取验证码。
如果取不到:
thrownewRuntimeException("验证码已过期");如果不一致:
thrownewRuntimeException("验证码错误");3. 查询或创建用户
Useruser=userMapper.getByPhone(phone);如果用户不存在,就自动创建一个新用户。
4. 生成 token
Stringtoken=JwtUtils.generateJwt(claims);登录成功后生成 JWT 返回给前端。
5. 删除验证码
stringRedisTemplate.delete(key);验证码使用成功后删除,避免重复使用。
十一、涉及知识点
1. Redis 过期时间
验证码是临时数据,非常适合设置过期时间。
.set(key,code,5,TimeUnit.MINUTES)5 分钟后 Redis 会自动删除。
2. StringRedisTemplate
这里使用:
StringRedisTemplate操作 Redis 字符串。
验证码本身就是字符串,所以用它非常合适。
3. JWT
验证码登录成功后,仍然需要生成 token。
因为后续接口依旧需要通过 token 判断用户是否登录。
4. 自动注册
如果手机号第一次登录,可以自动创建用户。
这在很多移动端项目中很常见。
十二、验证码接口限流
发送验证码接口不能无限调用。
可以加一个简单限流。
privatevoidcheckSendCodeLimit(Stringphone){Stringkey="limit:sendCode:"+phone;Longcount=stringRedisTemplate.opsForValue().increment(key);if(count!=null&&count==1){stringRedisTemplate.expire(key,60,TimeUnit.SECONDS);}if(count!=null&&count>5){thrownewRuntimeException("验证码发送过于频繁,请稍后再试");}}然后在发送验证码前调用:
@OverridepublicvoidsendCode(Stringphone){checkSendCodeLimit(phone);// 生成并保存验证码}这样可以限制同一个手机号 60 秒内最多发送 5 次验证码。
十三、实际开发建议
验证码登录开发时,可以注意下面几点:
- 验证码一定要设置过期时间
- 验证码校验成功后建议删除
- 发送验证码接口要限流
- 手机号格式要校验
- 验证码不要返回给前端,真实项目应该通过短信或邮箱发送
- 登录成功后仍然返回 token
- token 中不要保存敏感信息
- Redis key 命名要清楚
- 异常建议用自定义业务异常
- 密码登录和验证码登录可以共用 token 认证逻辑
十四、总结
这一篇主要实现了 Spring Boot + Redis 验证码登录。
整体流程是:用户请求发送验证码,后端生成 6 位验证码并保存到 Redis,设置 5 分钟过期;用户提交手机号和验证码登录,后端从 Redis 查询验证码并校验;校验成功后查询或创建用户,最后生成 JWT token 返回给前端。
这个功能把 Redis、JWT、Controller、Service、Mapper 都串起来了,比单独学知识点更接近真实项目开发。
下一篇我们继续写统一异常处理和业务异常封装,让项目中的错误返回更加规范。