验证码的使用场景
图形验证码在我们的日常使用中是非常常见的,比如一些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 {
@Bean
public 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也存在刚才获取的验证码,说明我们获取验证码的接口是没问题的。
再去看下校验验证码的这个接口,
可以看到,校验验证码的接口也是没问题的,校验验证码后,里面会删除缓存,这时候再来看下缓存中是否还存在:
刷新缓存为空了,说明检验验证码的接口逻辑是没问题的。后续就可以执行发送短信验证码了。
上述我们主要讲述了用户登录获取验证码的逻辑,用户注册,找回密码逻辑都是差不多一样的,只是后续的处理不太一样,在这里就不再过多讲述了。
总之,图形验证码与手机号验证码的结合使用,不仅增强了系统的安全性,也为用户提供了便捷的操作体验。未来,随着更多创新技术的应用,验证码系统将会变得更加智能和人性化,更好地服务于广大用户。希望本文能为开发者们提供一些有价值的参考,帮助大家在实际工作中构建更加稳固的安全防线。
更多精彩内容请关注以下公众号