--- title: "05-业务讲解-用户注册-到底如何使用图形验证码" created: 2025-12-12 aliases: - 业务讲解-用户注册-到底如何使用图形验证码 tags: - 项目 --- # 业务讲解-用户注册-到底如何使用图形验证码 本文主要介绍用户注册的流程是怎么使用图形验证码的,如果对图形验证码不是很熟悉的小伙伴,建议先跳转到相应的图形验证码介绍的文档查看 [组件讲解-图形验证码使用全解析,提升系统安全性的必备技巧](https://www.yuque.com/u22210564/ykdrdh/rtr7n0n38mlhdcb6) 我们先来梳理下用户注册的完整的流程,理清楚用户注册的流程在什么时候,什么步骤进行了获取验证码,提交验证码 ![[image-7f037bab.webp]] 通过流程可以清晰的看到整个流程 开始进行用户注册 填写注册需要的表单数据 点击注册按钮 检查是否需要验证码进行验证 如果上一步返回的数据需要验证,那么执行获取图形验证码 将表单数据和验证码数据提交给服务端 执行用户验证逻辑 UserRegisterVerifyCaptcha执行校验验证码的逻辑 接下来我们从此流程来详细的讲解 ## 流程 ### 检查是否需要验证码进行验证 com.damai.controller.UserCaptchaController#checkNeedCaptcha ```java @ApiOperation(value = "检查是否需要验证码") @PostMapping(value = "/check/need") public ApiResponse checkNeedCaptcha(){ return ApiResponse.ok(userCaptchaService.checkNeedCaptcha()); } ``` ```java /** * 当每秒的注册请求达到阈值触发校验验证码的操作 */ @Value("${verify_captcha_threshold:10}") private int verifyCaptchaThreshold; /** * 校验验证码id的过期时间 */ @Value("${verify_captcha_id_expire_time:60}") private int verifyCaptchaIdExpireTime; /** * 始终进行校验验证码 */ @Value("${always_verify_captcha:0}") private int alwaysVerifyCaptcha; public CheckNeedCaptchaDataVo checkNeedCaptcha() { //当前时间戳 long currentTimeMillis = System.currentTimeMillis(); //验证码唯一标识id long id = uidGenerator.getUid(); List keys = new ArrayList<>(); //计数器的键 keys.add(RedisKeyBuild.createRedisKey(RedisKeyManage.COUNTER_COUNT).getRelKey()); //计数器的时间戳的键 keys.add(RedisKeyBuild.createRedisKey(RedisKeyManage.COUNTER_TIMESTAMP).getRelKey()); //校验验证码唯一标识id的键 keys.add(RedisKeyBuild.createRedisKey(RedisKeyManage.VERIFY_CAPTCHA_ID,id).getRelKey()); String[] data = new String[4]; //每秒的注册请求的阈值 data[0] = String.valueOf(verifyCaptchaThreshold); //时间戳 data[1] = String.valueOf(currentTimeMillis); //校验验证码id的过期时间 data[2] = String.valueOf(verifyCaptchaIdExpireTime); //设置是否需要验证码 data[3] = String.valueOf(alwaysVerifyCaptcha); //执行计数器计算 Boolean result = checkNeedCaptchaOperate.checkNeedCaptchaOperate(keys, data); //将结果返回 CheckNeedCaptchaDataVo checkNeedCaptchaDataVo = new CheckNeedCaptchaDataVo(); checkNeedCaptchaDataVo.setCaptchaId(id); checkNeedCaptchaDataVo.setVerifyCaptcha(result); return checkNeedCaptchaDataVo; } ``` checkNeedCaptchaOperate.checkNeedCaptchaOperate的计数器功能是通过lua + redis来实现计数的,通过lua语言在redis中执行可以保证整个过程都是原子性的 CheckNeedCaptchaOperate ```java @Slf4j @Component public class CheckNeedCaptchaOperate extends AbstractApplicationPostConstructHandler { @Autowired private RedisCache redisCache; private DefaultRedisScript redisScript; @Override public Integer executeOrder() { return 1; } @Override public void executeInit(final ConfigurableApplicationContext context) { try { redisScript = new DefaultRedisScript<>(); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/checkNeedCaptcha.lua"))); redisScript.setResultType(String.class); } catch (Exception e) { log.error("redisScript init lua error",e); } } public Boolean checkNeedCaptchaOperate(List keys, String[] args){ Object object = redisCache.getInstance().execute(redisScript, keys, args); return Boolean.parseBoolean((String)object); } } ``` CheckNeedCaptchaOperate为lua脚本执行器,继承了AbstractApplicationPostConstructHandler,会在spring启动后进行加载初始化执行时,关于初始化执行管理的详细介绍可查看相关文档 [组件讲解-统一服务初始化操作,提升系统启动效率的秘诀](https://www.yuque.com/u22210564/ykdrdh/nw8op985wkbv43gs) 当执行checkNeedCaptchaOperate方法是,会执行加载好的checkNeedCaptcha.lua脚本 checkNeedCaptcha.lua ```lua -- 计数器的键 local counter_count_key = KEYS[1] -- 时间戳的键 local counter_timestamp_key = KEYS[2] -- 校验验证码id的键 local verify_captcha_id = KEYS[3] -- 每秒最大请求次数 local verify_captcha_threshold = tonumber(ARGV[1]) -- 当前时间戳 local current_time_millis = tonumber(ARGV[2]) -- 校验验证码id过期时间 local verify_captcha_id_expire_time = tonumber(ARGV[3]) -- 始终开启校验验证码开关 0:不开启 1:开启 local always_verify_captcha = tonumber(ARGV[4]) -- 时间窗口大小,1000毫秒,即1秒 local differenceValue = 1000 -- 如果开启校验验证码开关,则直接返回结果 if always_verify_captcha == 1 then redis.call('set', verify_captcha_id,'yes') redis.call('expire',verify_captcha_id,verify_captcha_id_expire_time) return 'true' end -- 获取当前计数和上次重置时间 local count = tonumber(redis.call('get', counter_count_key) or "0") local lastResetTime = tonumber(redis.call('get', counter_timestamp_key) or "0") -- 检查时间窗口是否已过,如果是,则重置计数和时间戳 if current_time_millis - lastResetTime >= differenceValue then count = 0 redis.call('set', counter_count_key, count) redis.call('set', counter_timestamp_key, current_time_millis) end -- 更新计数 count = count + 1 -- 超过阈值限制 if count > verify_captcha_threshold then -- 重置计数和时间戳 count = 0 redis.call('set', counter_count_key, count) redis.call('set', counter_timestamp_key, current_time_millis) -- 设置校验验证码标识 为yes,表示需要验证码操作 redis.call('set', verify_captcha_id,'yes') redis.call('expire',verify_captcha_id,verify_captcha_id_expire_time) return 'true' end -- 未超过限制,更新计数 redis.call('set', counter_count_key, count) -- 设置校验验证码标识 为no,表示不需要验证码操作 redis.call('set',verify_captcha_id,'no') redis.call('expire',verify_captcha_id,verify_captcha_id_expire_time) return 'false' ``` 整个lua执行的脚本的验证逻辑: 判断两次请求时间是否大于1秒,大于了说明请求不频繁,将计数器重置 判断每秒的请求数是否超过了配置的verify\_captcha\_threshold阈值 超过了则将计数器,更新时间戳重置,校验标识设置为需要验证,返回true结果 如果没有超过,则将计数器更新计数,校验标识设置为不需要验证,返回false结果 其实总结起来,检查是否需要验证码进行验证的核心逻辑就是在redis计算每秒的请求数是否达到规定的阈值,然后把设置一个标识存到redis中,在用户注册时根据此标识,判断是否需要校验验证码,当执行脚本后,将结果返回 ### 始终进行校验验证码 如果想模拟验证码的操作,可通过配置项always\_verify\_captcha = 1进行开启,开启后,无论计数器是否达到阈值,都会开启校验验证码 CheckNeedCaptchaDataVo ```java @Data @ApiModel(value="CheckNeedCaptchaDataVo", description ="是否需要进行校验验证码") public class CheckNeedCaptchaDataVo { @ApiModelProperty(name ="verifyCaptcha", dataType ="Boolean", value ="是否需要验证码 true:是 false:否") private Boolean verifyCaptcha; @ApiModelProperty(name ="id", dataType ="captchaId", value ="唯一标识id,用户注册接口需要传入此id") private Long captchaId; } ``` CheckNeedCaptchaDataVo为返回结果,captchaId唯一标识id,在调用用户注册接口时需要传入,以此判断是否需要进行校验验证码 ### 获取验证码 如果需要进行校验验证码操作的话,那么调用获取验证码接口 com.damai.controller.UserCaptchaController#getCaptcha ```java @ApiOperation(value = "获取验证码") @PostMapping(value = "/get") public ResponseModel getCaptcha(@RequestBody CaptchaVO captchaVO){ return userCaptchaService.getCaptcha(captchaVO); } ``` ```java public ResponseModel getCaptcha(CaptchaVO captchaVO) { return captchaHandle.getCaptcha(captchaVO); } ``` captchaHandle.getCaptcha(captchaVO)是图形验证码组件提供的api,关于图形验证码的详细使用和组件的介绍,可跳转到相关文档查看 [组件讲解-图形验证码使用全解析,提升系统安全性的必备技巧](https://www.yuque.com/u22210564/ykdrdh/rtr7n0n38mlhdcb6) 返回结果 ```json { "repCode": "0000", "repMsg": null, "repData": { "captchaId": null, "projectCode": null, "captchaType": null, "captchaOriginalPath": null, "captchaFontType": null, "captchaFontSize": null, "secretKey": "Ja43N2mgGDMEf61Y", "originalImageBase64": "VBORw0KGgoAAAANSUhEUg...", "point": null, "jigsawImageBase64": "iVBORw0KGgoAAAANSUhEUg...", "wordList": null, "pointList": null, "pointJson": null, "token": "5740b8dba2c742beae014598a95f9968", "result": false, "captchaVerification": null, "clientUid": null, "ts": null, "browserInfo": null }, "success": true } ``` originalImageBase64 原始图片的base64 jigsawImageBase64 拼接图片的base64 token 加密的token值,校验验证码时需要 当获取验证码,用户执行滑动或者拼接完后,调用用户注册接口,我们看下入参实体中关于验证码的参数 UserRegisterDto ```java @Data @ApiModel(value="UserRegisterDto", description ="注册用户") public class UserRegisterDto implements Serializable { // 省略... @ApiModelProperty(name ="id", dataType ="captchaId", value ="captchaId 调用是否需要校验验证码接口返回") @NotBlank private String captchaId; @ApiModelProperty(name ="captchaType", dataType ="String", value ="验证码类型:(clickWord,blockPuzzle)") private String captchaType; @ApiModelProperty(name ="pointJson", dataType ="String", value ="点坐标(base64加密传输)") private String pointJson; @ApiModelProperty(name ="token", dataType ="String", value ="UUID(每次请求的验证码唯一标识)") private String token; } ``` captchaId 校验验证码id,调用是否需要验证码进行验证接口返回 captchaType验证码类型 clickWord:点击 blockPuzzle:滑动 pointJson 用户执行滑动或者点击验证码后产生的坐标 token 验证码唯一标识,调用获取验证码接口返回 以上将获取验证码的操作介绍完毕,接下来就是进行验证码验证的流程了,请小伙伴跳转到相关的文档查看 [业务讲解-用户注册-使用组合模式处理复杂的验证功能](https://www.yuque.com/u22210564/ykdrdh/gh8b0iazadk9p5gk) --- **企业级项目导航**:⬅️ [[04-业务讲解-为什么说购票人的功能不容小觑|04-业务讲解-为什么说购票人的功能不容小觑]] | 05-业务讲解-用户注册-到底如何使用图形验证码 | ➡️ [[06-业务讲解-用户注册-如何巧妙应对缓存穿透|06-业务讲解-用户注册-如何巧妙应对缓存穿透]]