验证码的使用场景
图形验证码在我们的日常使用中是非常常见的,比如一些App、小程序、PC网站等。涉及到的业务也比较广泛、例如用户登录流程、注册流程、找回密码。下面我们来大致梳理下上述流程:
- 登录流程
- 用户首先在登录界面输入手机号
- 然后通过图形验证码验证,只有成功通过验证码后才能继续下一步
- 成功通过图形验证码后,系统向用户手机发送一条包含数字验证码的短信
- 用户收入收到的验证码,系统验证无误后允许登录
- 注册流程
- 用户填写基本信息,包括手机号
- 提交信息前需要通过图形验证码验证
- 通过图形验证码后,系统发送短信验证码到用户手机
- 用户输入短信验证码完成注册过程
- 找回密码
- 用户输入与账号关联的手机号
- 进行图形验证码验证
- 通过验证后,系统发送密码重置链接或重置密码所需的验证码至用户的手机
- 用户按照指引重置密码
验证码的生成步骤
知道了验证码的使用区域,下面来简单说下验证码的生成
- 背景生成
- 选择一个随机颜色作为背景色。
- 可以添加一些随机的干扰线条或点来增加破解难度。
- 字符生成
- 从预设的字符集中随机选取几个字符组成验证码字符串。这些字符可以是数字、小写字母或大写字母
- 对于每个字符,可以随机选择不同的字体、大小以及倾斜度。
- 字符渲染
- 将生成的字符绘制在背景上。为了进一步提高安全性,可以对字符应用扭曲效果或者随机旋转一定角度
- 字符之间的间距也应该是随机的,以便于增加识别难度
- 噪声添加
- 在背景中随机位置添加噪声点或线,这有助于迷惑OCR(光学字符识别)工具。
- 可以使用不同的颜色和形状,使得噪声更加自然。
- 图形变形
- 整个图像可以应用轻微的扭曲或变形,以使得验证码更难以被自动化工具识别。
- 输出图像
- 最后将处理好的图像输出为JPEG或PNG格式,并且可能还需要设置适当的分辨率和压缩级别以保证质量
- 存储验证码值及过期机制
- 验证码的实际文本需要存储在服务器端,并且通常会关联一个会话ID或者令牌,这样当用户提交表单时,可以验证输入是否正确
- 设置验证码的有效时间,超出这个时间则认为验证码无效。
验证码的代码实现
创建springBoot项目,导入kaptcha相关依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>kaptcha-spring-boot-starter</artifactId><version>1.1.0</version></dependency>
配置验证码的生成方法
/*** @Author: LiFly* @Date: 2024/8/27 17:05* @Description:*/
@Configuration
public class CaptchaConfig {private static final String CHAR_LENGTH = "4";private static final String CHAR_SPACE = "8";private static final String CAPTCHA_NOISE_IMPL = "com.google.code.kaptcha.impl.NoNoise";private static final String CAPTCHA_SCARIFICATION_IMPL = "com.google.code.kaptcha.impl.WaterRipple";private static final String KAPTCHA_TEXTPRODUCER_CHAR_STRING = "0123456789";/*** 验证码配置* @return 获取默认验证码配置*/@Bean@Qualifier("captchaProducer")public DefaultKaptcha kaptcha(){DefaultKaptcha kaptcha = new DefaultKaptcha();Properties properties = new Properties();//验证码个数properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH,CHAR_LENGTH);//字体间隔properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_SPACE,CHAR_SPACE);//干扰实现类properties.setProperty(Constants.KAPTCHA_NOISE_IMPL, CAPTCHA_NOISE_IMPL);//图片样式properties.setProperty(Constants.KAPTCHA_OBSCURIFICATOR_IMPL, CAPTCHA_SCARIFICATION_IMPL);//文字来源properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING,KAPTCHA_TEXTPRODUCER_CHAR_STRING);Config config = new Config(properties);kaptcha.setConfig(config);return kaptcha;}
}
配置redis的序列化方式
/*** @Author: LiFly* @Date: 2024/8/27 16:39* @Description:*/
@Configuration
public class RedisTemplateConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(redisConnectionFactory);//使用Jackson2JsonRedisSerialize替换默认序列化Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper objectMapper = new ObjectMapper();objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);//设置key和value的序列化规则redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);//设置hashKey和hashValue的序列化规则redisTemplate.setHashKeySerializer(new StringRedisSerializer());redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);//设置支持事务redisTemplate.setEnableTransactionSupport(true);redisTemplate.afterPropertiesSet();return redisTemplate;}
}
redis相关链接配置也加上
spring.redis.host=127.0.0.1
spring.redis.port=6379
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active = 10
# 连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle = 10
# 连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle = 0
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait= -1ms
#指定客户端
spring.redis.client-type = lettuce
#配置文件指定缓存类型
spring.cache.type=redis
下面我们来开发获取验证码的接口
@ApiOperation("获取验证码")@GetMapping("/getCaptcha")public void getCaptcha(HttpServletRequest request, HttpServletResponse response) {//创建验证码String captchaText = captchaProducer.createText();//缓存验证码redisTemplate.opsForValue().set(getCaptchaKey(request), captchaText,CAPTCHA_EXPIRE_TIME, TimeUnit.MILLISECONDS);//创建图形验证码BufferedImage bufferedImage = captchaProducer.createImage(captchaText);//返回图形验证码ServletOutputStream out = null;try {out = response.getOutputStream();ImageIO.write(bufferedImage,"jpg",out);out.flush();out.close();}catch (Exception e){log.error("获取图形验证码失败!e={}",e.getMessage());}}/*** 获取缓存key* @param request 请求参数* @return 组装key*/private String getCaptchaKey(HttpServletRequest request){String ip = CommonUtil.getIpAddr(request);String userAgent = request.getHeader("User-Agent");return "user-service:captcha:" + CommonUtil.MD5(ip+userAgent);}
获取验证码的接口非常简单,调用验证码配置类获取验证码,将获取到的验证码缓存到redis中并设置过期时间,然后调用配置类传入验证码获取到图形验证码,通过IO流返回。下面校验验证码
@ApiOperation("校验验证码")@PostMapping("/sendCode")public JsonData sendCode(@RequestParam(value = "to",required = true)String to,@RequestParam(value = "captcha",required = true) String captcha,HttpServletRequest request) {String key = getCaptchaKey(request);String cacheCaptcha = redisTemplate.opsForValue().get(key);if (cacheCaptcha != null && captcha != null && cacheCaptcha.equals(captcha)) {redisTemplate.delete(key);//发送验证码return JsonData.buildSuccess();}else {return JsonData.buildResult("图形验证码错误");}}
获取前端传过来的手机号以及图形验证码,根据ip以及网络地址相关西信息获取缓存key,根据key获取验证码,然后比较验证码是否一致,如果一致验证通过,删除缓存,执行发送短信相关业务。如果不一致直接返回给前端错误信息。
接口测试
下面我们通过postman来测试下开发的接口,首先获取验证码:
查看到获取的验证码为5437,再去查看下redis是否存在:
redis也存在刚才获取的验证码,说明我们获取验证码的接口是没问题的。
再去看下校验验证码的这个接口,
可以看到,校验验证码的接口也是没问题的,校验验证码后,里面会删除缓存,这时候再来看下缓存中是否还存在:
刷新缓存为空了,说明检验验证码的接口逻辑是没问题的。后续就可以执行发送短信验证码了。
上述我们主要讲述了用户登录获取验证码的逻辑,用户注册,找回密码逻辑都是差不多一样的,只是后续的处理不太一样,在这里就不再过多讲述了。
总之,图形验证码与手机号验证码的结合使用,不仅增强了系统的安全性,也为用户提供了便捷的操作体验。未来,随着更多创新技术的应用,验证码系统将会变得更加智能和人性化,更好地服务于广大用户。希望本文能为开发者们提供一些有价值的参考,帮助大家在实际工作中构建更加稳固的安全防线。
更多精彩内容请关注以下公众号