错误分析:Harness 失败案例的复盘
2026/5/14 14:01:25
在白嫖之前,希望你会内疚,最起码点个赞收藏再自取吧,源码在最后,自取;
在白嫖之前,希望你会内疚,最起码点个赞收藏再自取吧,源码在最后,自取;
在白嫖之前,希望你会内疚,最起码点个赞收藏再自取吧,源码在最后,自取;
总结:
前端触发登录流程,调用/phoneCaptcha接口并传入account,查询对应的User和Role信息,封装包含手机号和是否需要验证码(isVerify)的PhoneAndCaptchaVO返回给前端;若isVerify为1,前端调用/captcha接口并传入手机号,先校验手机号格式,合法则生成6位随机验证码,将验证码存入Redis并设置60秒过期,再调用短信服务发送对应模板的验证码,最后返回“发送成功”提示;前端输入验证码后调用/verifyCaptcha接口,传入手机号和验证码,先校验两者格式,合法则从Redis读取缓存的验证码,若验证码存在且匹配,删除Redis中的验证码并返回true以继续登录,若不存在或不匹配则记录失败日志并返回false提示用户;若isVerify不为1,则直接走后续登录流程。
该功能围绕角色登录的验证码流程展开,核心包含「生成验证码、验证验证码、登录前置校验(判断是否需要验证码)」三个核心接口,整体遵循「参数校验→核心逻辑→结果返回」的分层设计思路,具体拆解如下:
前端触发登录 → 调用/phoneCaptcha接口(传入账号)→ 获取手机号+是否需要验证码 → ├─ 若需要验证码 → 调用/captcha接口(传入手机号)→ 生成并发送短信验证码 → │ 前端输入验证码 → 调用/verifyCaptcha接口 → 校验验证码有效性 → 返回校验结果 └─ 若不需要验证码 → 直接走后续登录逻辑| 模块/接口 | 核心逻辑 | 技术选型/设计细节 |
|---|---|---|
| /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查询枚举的方法,增强可读性和维护性 |
bladeRedis.del(redisKey)),避免同一验证码被多次校验,是登录验证码的核心安全要求。setEx),既保证用户有足够时间输入,又避免验证码长期留存带来的安全风险。R.data()/R.status()),前端可统一解析响应状态和数据;phoneAndCaptchaVO.setPhone(StringUtils.defaultIfBlank(user.getPhone(), ""));、phoneAndCaptchaVO.setIsVerify(role == null ? 0 : role.getIsVerify());。log.info("手机号:{}", phone)),存在数据泄露风险;phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")。StringrateLimitKey="role_login_captcha_rate_limit:"+phone;if(bladeRedis.hasKey(rateLimitKey)){thrownewServiceException("验证码发送过于频繁,请60秒后重试");}bladeRedis.setEx(rateLimitKey,"1",VERIFY_CODE_EXPIRE_SECONDS);@Async),控制器立即返回“验证码发送中”,前端轮询获取发送结果;该功能的核心思路是「安全、可靠、易用」:通过Redis保证验证码的时效性和唯一性,通过分层设计降低耦合,通过异常处理和日志保证可维护性;重难点集中在短信发送可靠性、Redis原子性、边界场景处理、并发控制,需重点关注这些环节以避免生产问题。
/** * 生成验证码接口 */@GetMapping("/captcha")@ApiOperationSupport(order=24)@Operation(summary="生成验证码接口")publicR<String>captcha(Stringphone){returnR.data(userService.captcha(phone));}/** * 验证验证码 */@PostMapping("/verifyCaptcha")@ApiOperationSupport(order=25)@Operation(summary="验证验证码")publicR<Boolean>verifyCaptcha(Stringphone,Stringcaptcha){returnR.status(userService.verifyCaptcha(phone,captcha));}/** * 登录时返回手机号和是否需要验证码字段给前端 */@GetMapping("/phoneCaptcha")@ApiOperationSupport(order=26)@Operation(summary="登录时返回手机号和是否需要验证码字段给前端")publicR<PhoneAndCaptchaVO>phoneCaptcha(Stringaccount){returnR.data(userService.phoneCaptcha(account));}/** * 生成角色登录验证码 * @param phone 手机号 * @return 包含验证码发送状态的Map(接口约定返回格式) */Stringcaptcha(Stringphone);/** * 验证角色登录验证码是否正确 * @param phone 手机号 * @param captcha 用户输入的验证码 * @return 验证结果(true=成功,false=失败) */booleanverifyCaptcha(Stringphone,Stringcaptcha);PhoneAndCaptchaVOphoneCaptcha(Stringaccount);/** * 生成验证码 */@OverridepublicStringcaptcha(Stringphone){// 1. 手机号格式校验if(!PhoneUtil.isMobile(phone)){thrownewServiceException("手机号格式不正确");}try{// 2. 生成6位随机验证码StringverifyCode=RandomUtil.randomNumbers(6);StringredisKey=getRoleLoginVerifyCodeKey(phone);// 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(Exceptione){log.error("生成角色登录验证码失败,手机号:{}",phone,e);if(einstanceofServiceException){throw(ServiceException)e;}thrownewServiceException("验证码发送失败,请稍后重试");}}/** * 验证验证码 */@OverridepublicbooleanverifyCaptcha(Stringphone,Stringcaptcha){// 1. 基础格式校验if(!PhoneUtil.isMobile(phone)){thrownewServiceException("手机号格式不正确");}if(!StringUtils.hasText(captcha)||captcha.length()!=6){thrownewServiceException("验证码格式不正确(需6位数字)");}// 2. 获取Redis中的验证码StringredisKey=getRoleLoginVerifyCodeKey(phone);StringredisCode=bladeRedis.get(redisKey);// 3. 校验逻辑if(!StringUtils.hasText(redisCode)){log.warn("角色登录验证码已过期/不存在,手机号:{}",phone);returnfalse;// 验证码过期/未生成}// 4. 验证码匹配booleanisMatch=redisCode.equals(captcha);if(isMatch){// 验证成功后删除Redis验证码,防止重复使用bladeRedis.del(redisKey);log.info("角色登录验证码校验成功,手机号:{}",phone);}else{log.warn("角色登录验证码校验失败,手机号:{},输入验证码:{},正确验证码:{}",phone,captcha,redisCode);}returnisMatch;}@OverridepublicPhoneAndCaptchaVOphoneCaptcha(Stringaccount){PhoneAndCaptchaVOphoneAndCaptchaVO=newPhoneAndCaptchaVO();Useruser=this.lambdaQuery().eq(User::getAccount,account).one();if(user!=null){Rolerole=roleService.lambdaQuery().eq(Role::getId,user.getRoleId()).one();if(role!=null){phoneAndCaptchaVO.setIsVerify(role.getIsVerify());}phoneAndCaptchaVO.setPhone(user.getPhone());}returnphoneAndCaptchaVO;}/** * 生成角色登录验证码的Redis Key * @param phone 手机号 * @return 完整的Redis Key */privateStringgetRoleLoginVerifyCodeKey(Stringphone){if(!StringUtils.hasText(phone)){thrownewServiceException("手机号不能为空,无法生成Redis Key");}returnROLE_LOGIN_VERIFY_CODE_PREFIX+phone;}importio.swagger.v3.oas.annotations.media.Schema;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;@Data@NoArgsConstructor@AllArgsConstructorpublicclassPhoneAndCaptchaVO{@Schema(description="手机号")privateStringphone;@Schema(description="是否需要验证码")privateIntegerisVerify;}importlombok.AllArgsConstructor;importlombok.Getter;@Getter@AllArgsConstructorpublicenumTemplateCodeEnum{/** * 阿里云短信模板枚举 */TO_BE_PROCESSED(1,"SMS_123456","收到一条待处理更新提醒,用户需要您进入系统处理。"),ROLE_LOGIN_SMS_TEMPLATE(2,"SMS_123456","登录验证码,请勿泄露给其他人");/** * 模板ID */privatefinalIntegerid;/** * 阿里云短信模板CODE */privatefinalStringcode;/** * 模板描述 */privatefinalStringdesc;/** * 根据阿里云模板CODE获取枚举 * * @param code 阿里云模板CODE * @return 对应的枚举 * @throws IllegalArgumentException 如果code不存在 */publicstaticTemplateCodeEnumgetByCode(Stringcode){for(TemplateCodeEnumtemplate:TemplateCodeEnum.values()){if(template.getCode().equals(code)){returntemplate;}}thrownewIllegalArgumentException("未知的阿里云短信模板CODE: "+code);}}