手机验证码功能实现(附带源码)

在白嫖之前,希望你会内疚,最起码点个赞收藏再自取吧,源码在最后,自取;

在白嫖之前,希望你会内疚,最起码点个赞收藏再自取吧,源码在最后,自取;

在白嫖之前,希望你会内疚,最起码点个赞收藏再自取吧,源码在最后,自取;

总结:

前端触发登录流程,调用/phoneCaptcha接口并传入account,查询对应的User和Role信息,封装包含手机号和是否需要验证码(isVerify)的PhoneAndCaptchaVO返回给前端;若isVerify为1,前端调用/captcha接口并传入手机号,先校验手机号格式,合法则生成6位随机验证码,将验证码存入Redis并设置60秒过期,再调用短信服务发送对应模板的验证码,最后返回"发送成功"提示;前端输入验证码后调用/verifyCaptcha接口,传入手机号和验证码,先校验两者格式,合法则从Redis读取缓存的验证码,若验证码存在且匹配,删除Redis中的验证码并返回true以继续登录,若不存在或不匹配则记录失败日志并返回false提示用户;若isVerify不为1,则直接走后续登录流程。

一、功能实现思路

该功能围绕角色登录的验证码流程展开,核心包含「生成验证码、验证验证码、登录前置校验(判断是否需要验证码)」三个核心接口,整体遵循「参数校验→核心逻辑→结果返回」的分层设计思路,具体拆解如下:

1. 核心流程总览
复制代码
前端触发登录 → 调用/phoneCaptcha接口(传入账号)→ 获取手机号+是否需要验证码 → 
  ├─ 若需要验证码 → 调用/captcha接口(传入手机号)→ 生成并发送短信验证码 → 
  │    前端输入验证码 → 调用/verifyCaptcha接口 → 校验验证码有效性 → 返回校验结果
  └─ 若不需要验证码 → 直接走后续登录逻辑
2. 各模块具体实现思路
模块/接口 核心逻辑 技术选型/设计细节
/phoneCaptcha 登录前置校验,根据账号查询用户+角色信息,返回「手机号+是否需要验证码」 1. 基于MyBatis-Plus的lambdaQuery查询用户/角色; 2. 封装VO对象统一返回格式; 3. 空值兼容(用户/角色为空时VO字段默认null)
/captcha 生成并发送短信验证码,同时缓存到Redis 1. 手机号格式校验(PhoneUtil); 2. 生成6位随机数(RandomUtil); 3. Redis设置60秒过期(防止验证码长期有效); 4. 调用短信服务发送验证码; 5. 异常统一封装为ServiceException
/verifyCaptcha 校验用户输入的验证码是否有效 1. 格式校验(手机号+6位数字验证码); 2. 从Redis读取缓存的验证码; 3. 匹配成功后删除Redis验证码(防止重复使用); 4. 日志记录校验结果(便于排查问题)
枚举/TemplateCodeEnum 统一管理短信模板CODE,避免硬编码 1. 封装模板ID/CODE/描述; 2. 提供根据CODE查询枚举的方法,增强可读性和维护性
3. 分层设计
  • Controller层:仅负责接收参数、调用Service、统一返回R格式结果(Blade框架的通用响应体),无业务逻辑;
  • Service层:封装核心业务逻辑(校验、Redis操作、短信发送、数据库查询),异常统一抛出ServiceException;
  • VO层:PhoneAndCaptchaVO封装前端所需的手机号和是否需要验证码字段,符合接口返回的结构化要求;
  • 枚举层:TemplateCodeEnum统一管理短信模板,降低硬编码风险。

二、功能重难点分析

1. 核心重点(保证功能可用、安全、稳定)
(1)验证码的安全性设计
  • 重点1:验证码防重复使用
    验证成功后立即删除Redis中的验证码(bladeRedis.del(redisKey)),避免同一验证码被多次校验,是登录验证码的核心安全要求。
  • 重点2:验证码时效性控制
    Redis设置60秒过期(setEx),既保证用户有足够时间输入,又避免验证码长期留存带来的安全风险。
  • 重点3:格式校验前置
    手机号、验证码的格式校验(如6位数字)在业务逻辑最前端执行,提前拦截无效请求,减少Redis/数据库/短信服务的无效调用。
(2)异常处理与日志记录
  • 重点1:异常统一封装
    捕获底层异常(如Redis异常、短信发送异常)并封装为ServiceException,保证前端接收到统一的异常提示,避免暴露底层报错;
  • 重点2:关键日志记录
    验证码生成/校验的关键节点(Redis存入、短信发送、校验结果)都打印日志,便于排查「验证码收不到、校验失败」等问题(如日志中记录正确验证码,仅用于排查,生产需注意脱敏)。
(3)接口通用性与扩展性
  • 重点1:统一响应格式
    所有接口返回Blade框架的R对象(R.data()/R.status()),前端可统一解析响应状态和数据;
  • 重点2:枚举管理模板CODE
    TemplateCodeEnum将短信模板CODE抽象为枚举,后续新增模板只需扩展枚举,无需修改业务代码,符合「开闭原则」。
2. 核心难点(易出问题、需重点关注)
(1)短信发送的可靠性问题
  • 难点表现:短信发送接口可能超时、失败(如运营商接口波动、网络问题),若未处理会导致「验证码存入Redis但短信未发送」,用户收不到验证码但Redis有记录,引发体验问题;
  • 解决方案
    ① 短信发送增加重试机制(如重试2次);
    ② 短信发送失败时,删除Redis中已存入的验证码(避免脏数据);
    ③ 对接短信服务商的回调接口,确认短信是否真正发送成功。
(2)Redis操作的原子性与异常处理
  • 难点表现:Redis操作(setEx/get/del)可能抛出异常(如Redis集群不可用),若未捕获会导致请求报错,且可能出现「验证码已生成但未存入Redis」的情况;
  • 解决方案
    ① 对Redis操作增加try-catch,异常时抛出明确的ServiceException(如"验证码生成失败,请稍后重试");
    ② 生产环境建议使用Redis分布式锁(若有并发生成验证码场景),避免同一手机号短时间内生成多个验证码。
(3)空值处理与边界场景
  • 难点1:/phoneCaptcha接口的空值场景
    若传入的account不存在(user=null),返回的VO中phone和isVerify为null,前端若未处理空值会引发前端报错;
    ✅ 优化:默认值兜底,如phoneAndCaptchaVO.setPhone(StringUtils.defaultIfBlank(user.getPhone(), ""));phoneAndCaptchaVO.setIsVerify(role == null ? 0 : role.getIsVerify());
  • 难点2:手机号脱敏
    日志中直接打印完整手机号(如log.info("手机号:{}", phone)),存在数据泄露风险;
    ✅ 优化:日志中对手机号脱敏,如phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")
(4)并发场景下的验证码冲突
  • 难点表现:同一手机号短时间内多次调用/captcha接口,会生成多个验证码覆盖Redis中的旧值,导致用户收到多条短信,且只能用最后一条验证码;

  • 解决方案
    ① 增加验证码发送频率限制(如同一手机号60秒内只能发送1次),Redis存入「发送时间戳」,调用/captcha时先校验是否在冷却期;
    ② 示例代码:

    java 复制代码
    String rateLimitKey = "role_login_captcha_rate_limit:" + phone;
    if (bladeRedis.hasKey(rateLimitKey)) {
        throw new ServiceException("验证码发送过于频繁,请60秒后重试");
    }
    bladeRedis.setEx(rateLimitKey, "1", VERIFY_CODE_EXPIRE_SECONDS);
(5)与开篇异常(AsyncRequestNotUsableException)的关联
  • 难点表现:若短信发送接口耗时较长(如超过前端超时时间),前端可能提前断开连接,导致控制器返回R对象时触发「流已关闭」异常;
  • 解决方案
    ① 短信发送改为异步处理(@Async),控制器立即返回"验证码发送中",前端轮询获取发送结果;
    ② 调整前端请求超时时间(如设置为10秒),后端增加响应流状态检测(参考上篇回答的异常处理器)。

三、优化建议(补充)

  1. 验证码脱敏:日志中隐藏验证码明文(如只打印后两位),避免数据泄露;
  2. 接口限流:对/captcha接口增加IP+手机号限流,防止恶意刷短信;
  3. 单元测试覆盖:针对「验证码过期、格式错误、重复校验、手机号不存在」等边界场景编写测试用例;
  4. 配置化管理:将Redis过期时间、短信模板CODE、限流时间等硬编码值抽离到配置文件(application.yml),便于动态调整。

总结

该功能的核心思路是「安全、可靠、易用」:通过Redis保证验证码的时效性和唯一性,通过分层设计降低耦合,通过异常处理和日志保证可维护性;重难点集中在短信发送可靠性、Redis原子性、边界场景处理、并发控制,需重点关注这些环节以避免生产问题。

源码

java 复制代码
	/**
	 * 生成验证码接口
	 */
	@GetMapping("/captcha")
	@ApiOperationSupport(order = 24)
	@Operation(summary = "生成验证码接口")
	public R<String> captcha(String phone) {
		return R.data(userService.captcha(phone));
	}

	/**
	 * 验证验证码
	 */
	@PostMapping("/verifyCaptcha")
	@ApiOperationSupport(order = 25)
	@Operation(summary = "验证验证码")
	public R<Boolean> verifyCaptcha(String phone, String captcha) {
		return R.status(userService.verifyCaptcha(phone, captcha));
	}

	/**
	 * 登录时返回手机号和是否需要验证码字段给前端
	 */
	@GetMapping("/phoneCaptcha")
	@ApiOperationSupport(order = 26)
	@Operation(summary = "登录时返回手机号和是否需要验证码字段给前端")
	public R<PhoneAndCaptchaVO> phoneCaptcha(String account) {
		return R.data(userService.phoneCaptcha(account));
	}
java 复制代码
	/**
	 * 生成角色登录验证码
	 * @param phone 手机号
	 * @return 包含验证码发送状态的Map(接口约定返回格式)
	 */
	String captcha(String phone);

	/**
	 * 验证角色登录验证码是否正确
	 * @param phone 手机号
	 * @param captcha 用户输入的验证码
	 * @return 验证结果(true=成功,false=失败)
	 */
	boolean verifyCaptcha(String phone, String captcha);

	PhoneAndCaptchaVO phoneCaptcha(String account);
java 复制代码
		/**
	 * 生成验证码
	 */
	@Override
	public String captcha(String phone) {
		// 1. 手机号格式校验
		if (!PhoneUtil.isMobile(phone)) {
			throw new ServiceException("手机号格式不正确");
		}

		try {
			// 2. 生成6位随机验证码
			String verifyCode = RandomUtil.randomNumbers(6);
			String redisKey = getRoleLoginVerifyCodeKey(phone);

			// 非pre环境,使用默认验证码为:123456
			if (!environment.acceptsProfiles(Profiles.of("prod", "pre"))) {
				log.info("====>非pre环境,使用默认验证码:123456");
				verifyCode = "123456";

				// 存入Redis,60秒过期
				bladeRedis.setEx(redisKey, verifyCode, VERIFY_CODE_EXPIRE_SECONDS);
				log.info("角色登录验证码已存入Redis,手机号:{},验证码:{},过期时间:60秒", phone, verifyCode);
				return "验证码发送成功,60秒内有效";
			}

			// 3. 存入Redis,60秒过期
			bladeRedis.setEx(redisKey, verifyCode, VERIFY_CODE_EXPIRE_SECONDS);
			log.info("角色登录验证码已存入Redis,手机号:{},验证码:{},过期时间:60秒", phone, verifyCode);

			// 4. 发送短信验证码
			Map<String, Object> smsParam = Map.of("code", verifyCode);
			smsSending.sendSms(phone, smsParam, TemplateCodeEnum.ROLE_LOGIN_SMS_TEMPLATE.getCode());

			// 5. 返回字符串结果(直接返回提示语)
			return "验证码发送成功,60秒内有效";

		} catch (Exception e) {
			log.error("生成角色登录验证码失败,手机号:{}", phone, e);
			if (e instanceof ServiceException) {
				throw (ServiceException) e;
			}
			throw new ServiceException("验证码发送失败,请稍后重试");
		}
	}

	/**
	 * 验证验证码
	 */
	@Override
	public boolean verifyCaptcha(String phone, String captcha) {
		// 1. 基础格式校验
		if (!PhoneUtil.isMobile(phone)) {
			throw new ServiceException("手机号格式不正确");
		}
		if (!StringUtils.hasText(captcha) || captcha.length() != 6) {
			throw new ServiceException("验证码格式不正确(需6位数字)");
		}

		// 2. 获取Redis中的验证码
		String redisKey = getRoleLoginVerifyCodeKey(phone);
		String redisCode = bladeRedis.get(redisKey);

		// 3. 校验逻辑
		if (!StringUtils.hasText(redisCode)) {
			log.warn("角色登录验证码已过期/不存在,手机号:{}", phone);
			return false; // 验证码过期/未生成
		}

		// 4. 验证码匹配
		boolean isMatch = redisCode.equals(captcha);
		if (isMatch) {
			// 验证成功后删除Redis验证码,防止重复使用
			bladeRedis.del(redisKey);
			log.info("角色登录验证码校验成功,手机号:{}", phone);
		} else {
			log.warn("角色登录验证码校验失败,手机号:{},输入验证码:{},正确验证码:{}", phone, captcha, redisCode);
		}
		return isMatch;
	}

	@Override
	public PhoneAndCaptchaVO phoneCaptcha(String account) {
		PhoneAndCaptchaVO phoneAndCaptchaVO = new PhoneAndCaptchaVO();
		User user = this.lambdaQuery().eq(User::getAccount, account).one();
		if (user != null) {
			Role role = roleService.lambdaQuery().eq(Role::getId, user.getRoleId()).one();
			if (role != null) {
				phoneAndCaptchaVO.setIsVerify(role.getIsVerify());
			}
			phoneAndCaptchaVO.setPhone(user.getPhone());
		}
		return phoneAndCaptchaVO;
	}

	/**
	 * 生成角色登录验证码的Redis Key
	 * @param phone 手机号
	 * @return 完整的Redis Key
	 */
	private String getRoleLoginVerifyCodeKey(String phone) {
		if (!StringUtils.hasText(phone)) {
			throw new ServiceException("手机号不能为空,无法生成Redis Key");
		}
		return ROLE_LOGIN_VERIFY_CODE_PREFIX + phone;
	}
java 复制代码
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class PhoneAndCaptchaVO {

	@Schema(description = "手机号")
	private String phone;

	@Schema(description = "是否需要验证码")
	private Integer isVerify;
}
java 复制代码
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum TemplateCodeEnum {
	/**
	 * 阿里云短信模板枚举
	 */
	TO_BE_PROCESSED(1, "SMS_123456", "收到一条待处理更新提醒,用户需要您进入系统处理。"),

	ROLE_LOGIN_SMS_TEMPLATE(2, "SMS_123456", "登录验证码,请勿泄露给其他人");

	/**
	 * 模板ID
	 */
	private final Integer id;

	/**
	 * 阿里云短信模板CODE
	 */
	private final String code;

	/**
	 * 模板描述
	 */
	private final String desc;

	/**
	 * 根据阿里云模板CODE获取枚举
	 *
	 * @param code 阿里云模板CODE
	 * @return 对应的枚举
	 * @throws IllegalArgumentException 如果code不存在
	 */
	public static TemplateCodeEnum getByCode(String code) {
		for (TemplateCodeEnum template : TemplateCodeEnum.values()) {
			if (template.getCode().equals(code)) {
				return template;
			}
		}
		throw new IllegalArgumentException("未知的阿里云短信模板CODE: " + code);
	}
}
相关推荐
Adellle2 小时前
Java-Stream流
java
fanruitian2 小时前
微信小程序 springboot获取手机号
spring boot·微信小程序·notepad++
xUxIAOrUIII2 小时前
JWT和拦截器使用【附Maven中操作步骤】
java·maven
带刺的坐椅2 小时前
Liquor(Java 脚本)替代 Groovy 作脚本引擎的可行性分析
java·groovy·liquor
加成BUFF2 小时前
C++入门讲解3:数组与指针全面详解
开发语言·c++·算法·指针·数组
武子康2 小时前
Java-203 RabbitMQ 生产者/消费者工作流程拆解:Connection/Channel、默认交换器、ACK
java·分布式·消息队列·rabbitmq·erlang·ruby·java-rabbitmq
Coder_Boy_2 小时前
前端和后端软件系统联调经典问题汇总
java·前端·驱动开发·微服务·状态模式
GoWjw2 小时前
C语言高级特性
c语言·开发语言·算法
自己的九又四分之三站台2 小时前
基于Python获取SonarQube的检查报告信息
开发语言·python