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

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

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

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

总结:

前端触发登录流程,调用/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);
	}
}
相关推荐
Scott9999HH6 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔6 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社7 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海7 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖8 小时前
JDK 26 新特性详解
java·开发语言
马优晨8 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区10 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大11 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai11 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说11 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json