1.背景
注册
-登录
-修改密码
一般需要发送验证码,但是容易被攻击恶意调用。
1.1 什么是短信-邮箱轰炸机
手机短信轰炸机是批量、循环给手机无限发送各种网站的注册验证码短信的方法。
1.2 公司带来的损失
短信一条5分钱,如果被大盗刷大家自己计算邮箱通知不用钱,但被大盗刷,带宽、连接等都被占用,导致无法正常使用。
2.如何避免自己的网站成为"肉鸡“或者被刷呢
- 增加图形验证码(开发人员)
图形验证码(CAPTCHA),是Completely Automated Public Turing Test to Tell Computers and Humans Apart (全自动区分计算机和人类的图灵测试)的简称。其本质是-种区分用户是计算机还是人的公共全自动程序。可以有效的防止某些特定程序以暴力方式不断进行登录尝试。验证码的不断发展其实是随着其破解功能的逐步强大而跟着演进的,这是一个攻防博弈的精彩世界。 - 单IP请求次数限制(开发人员)
- 限制号码发送(一般短信提供商会做)
- 攻防永远是有的,只过加大了攻击者的成本,ROI划不过来自然就放弃了
3.表单重复提交问题
- 用户正常提交,由于网络延迟等原因,未收到服务器的响应,这时,用户着急多点了几次提交操作,会造成表单重复提交。
- 用户正常提交,服务器也没有延迟,但是提交完成后,用户回退浏览器。重新提交,也会造成表单重复提交。
这两个表单重复提交问题的解决方案:验证码
4.Kaptcha框架介绍
谷歌开源的一个可高度配置的实用验证码生成工具:
- 验证码的字体/大小颜色
- 验证码内容的范围(数字,字母,中文汉字!)
- 验证码图片的大小,边框,边框粗细,边框颜色
- 验证码的干扰线验证码的样式(鱼眼样式、3D、 普通模糊)
5.前后端分离验证码实战开发-思路分析
这里以修改密码为例,规则是修改用户密码必须一同将验证码传给后端,如果验证码失效或者错误都需要重新生成并获取验证码。
- 前端向后端请求验证码。
- 后端通过谷歌开源工具Kaptcha生成图形验证码(实际是5个随机字符),缓存到redis,key键可以是 业务+用户id,value值就是那5个随机字符。设置TTL为2分钟。
- 后端将图形验证码转base64编码字符串,将该字符串返回给前端。
- 前端解析base64编码的字符串,即可在页面上显示图形验证码。
- 用户输入密码与验证码后提交表单到后端。
- 后端根据业务和用户id组成的key键到redis查找缓存的验证码信息,会有如下情况:
- 如果redis返回为空,则通知前端验证码失效,需要重新获取验证码。
- 如果redis返回不为空,但是不相等,说明验证码输入错误。则删除redis中对应验证码缓存,通知前端验证码错误,需要重新获取验证码。
- 如果redis返回不为空,并且相等,则校验成功,就删除redis中对应验证码缓存,并在mysql中修改密码。最后通知前端修改成功。
6.前后端分离验证码实战开发-后端代码
5.1 pom.xml核心依赖
<dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency>
5.2 KaptchaConfig配置类
package com.zhulang.waveedu.sms.config; import com.google.code.kaptcha.Producer; import com.google.code.kaptcha.impl.DefaultKaptcha; import com.google.code.kaptcha.util.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Properties; /** * 谷歌提供的图片验证码kaptcha * * @author 狐狸半面添 * @create 2023-01-29 11:02 */ @Configuration public class KaptchaConfig { @Bean public Producer kaptcha() { Properties properties = new Properties(); /* 设置图片有边框并且为蓝色 properties.setProperty("kaptcha.border", "no"); properties.setProperty("kaptcha.border.color", "blue"); */ // 设置图片无边框 properties.setProperty("kaptcha.border", "no"); // 背景颜色渐变开始,这里设置的是rgb值156,156,156 properties.put("kaptcha.background.clear.from", "156,156,156"); // 背景颜色渐变结束,这里设置以白色结束 properties.put("kaptcha.background.clear.to", "white"); // 字体颜色,这里设置为黑色 properties.put("kaptcha.textproducer.font.color", "black"); // 文字间隔,这里设置为10px properties.put("kaptcha.textproducer.char.space", "10"); /* 如果需要去掉干扰线,则如此配置: properties.put("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise"); */ // 干扰线颜色配置,这里设置成了idea的Darcula主题的背景色 properties.put("kaptcha.noise.color", "43,43,43"); // 字体 properties.put("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑"); // 图片宽度,默认也是200px properties.setProperty("kaptcha.image.width", "200"); // 图片高度,默认也是50px properties.setProperty("kaptcha.image.height", "50"); // 从哪些字符中产生 properties.setProperty("kaptcha.textproducer.char.string", "0123456789abcdefghijklmnopqrsduvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"); // 字符个数 properties.setProperty("kaptcha.textproducer.char.length", "5"); Config config = new Config(properties); DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha; } }
5.3 CaptchaServiceController生成与获取验证码
- RedisCacheUtils是一个自定义的redis缓存工具类
- RedisConstants是一个redis常量类,主要是设置缓存key前缀与TTL
- UserHolderUtils.getUserId():通过自定义UserHolderUtils工具类获取用户id(通过token从redis获取用户信息,包括了userId)
package com.zhulang.waveedu.sms.controller; import com.google.code.kaptcha.Producer; import com.zhulang.waveedu.common.constant.RedisConstants; import com.zhulang.waveedu.common.entity.Result; import com.zhulang.waveedu.common.util.RedisCacheUtils; import com.zhulang.waveedu.common.util.UserHolderUtils; import org.apache.tomcat.util.codec.binary.Base64; import org.springframework.util.FastByteArrayOutputStream; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; /** * 生成图片验证码 * * @author 狐狸半面添 * @create 2023-01-29 17:35 */ @RestController @RequestMapping("/captcha") public class CaptchaServiceController { @Resource private Producer producer; @Resource private RedisCacheUtils redisCacheUtils; /** * 获取修改密码的图片验证码base64编码 * 并缓存至redis中 * * @return base编码 */ @GetMapping("/pwd/vc.jpg") public Result getPwdCaptcha(){ // 1.生成验证码字符 String text = producer.createText(); // 2.生成图片 BufferedImage bi = producer.createImage(text); FastByteArrayOutputStream fos = new FastByteArrayOutputStream(); try { ImageIO.write(bi, "jpg", fos); // 3.缓存至 redis 中 redisCacheUtils.setCacheObject(RedisConstants.PWD_CODE_KEY+ UserHolderUtils.getUserId(),text,RedisConstants.PWD_CODE_TTL); // 4.返回验证码图片的base64编码 String imgEncode = Base64.encodeBase64String(fos.toByteArray()); fos.flush(); return Result.ok(imgEncode); }catch (Exception e){ return Result.error(); }finally { fos.close(); } } }
5.4 修改密码验证码校验代码
package com.zhulang.waveedu.basic.vo; import com.zhulang.waveedu.common.util.RegexUtils; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; /** * 修改密码时的VO * * @author 狐狸半面添 * @create 2023-01-29 22:09 */ @Data @NoArgsConstructor @AllArgsConstructor public class UpdatePwdVO { /** * 第一次输入的密码 */ @NotBlank(message = "输入密码为空") @Pattern(regexp = RegexUtils.RegexPatterns.PASSWORD_REGEX, message = "密码格式错误,应为8-16位的数字或字母") private String firPassword; /** * 第二次输入的密码(确认密码) */ @NotBlank(message = "输入密码为空") @Pattern(regexp = RegexUtils.RegexPatterns.PASSWORD_REGEX, message = "密码格式错误,应为8-16位的数字或字母") private String secPassword; /** * 图片验证码字符 */ @NotBlank(message = "验证码为空") @Pattern(regexp = RegexUtils.RegexPatterns.CAPTCHA_CODE_REGEX, message = "验证码格式错误") private String code; }
/** * User登录,注册,推出登录,注销的控制器 * * @author 狐狸半面添 * @create 2023-01-17 23:14 */ @RestController @RequestMapping("/user") public class UserController { /** * 修改密码 * * @param updatePwdVO 两个密码+code * @return 修改情况 */ @PutMapping("/updatePwd") public Result updatePwd(@Validated @RequestBody UpdatePwdVO updatePwdVO){ return userService.updatePwd(updatePwdVO); } }
/** * ServiceImpl实现了IService,提供了IService中基础功能的实现 * 若ServiceImpl无法满足业务需求,则可以使用自定的UserService定义方法,并在实现类中实现 * * @author 狐狸半面添 * @create 2023-01-17 23:31 */ @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Resource private RedisCacheUtils redisCacheUtils; @Override public Result updatePwd(UpdatePwdVO updatePwdVO) { // 1.如果两个密码不一致,则返回error if (!updatePwdVO.getFirPassword().equals(updatePwdVO.getSecPassword())){ return Result.error(HttpStatus.HTTP_VERIFY_FAIL.getCode(), "两次密码不一致"); } // 2.校验验证码是否正确 String code = redisCacheUtils.getCacheObject(RedisConstants.PWD_CODE_KEY + UserHolderUtils.getUserId()); // 2.1 不存在则返回 if (code==null){ return Result.error(HttpStatus.HTTP_VERIFY_FAIL.getCode(),"验证码已失效,请重新获取"); } // 2.2 存在但不一致则清除并返回(忽略大小写) if (!code.equalsIgnoreCase(updatePwdVO.getCode())){ redisCacheUtils.deleteObject(RedisConstants.PWD_CODE_KEY + UserHolderUtils.getUserId()); return Result.error(HttpStatus.HTTP_VERIFY_FAIL.getCode(),"验证码错误,请重新获取"); } // 3.验证码正确,则从缓存中移除 redisCacheUtils.deleteObject(RedisConstants.PWD_CODE_KEY + UserHolderUtils.getUserId()); // 4.修改密码 LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>(); wrapper.eq(User::getId,UserHolderUtils.getUserId()) .set(User::getPassword,PasswordEncoderUtils.encode(updatePwdVO.getFirPassword())); this.update(wrapper); // 5.返回 return Result.ok(); } }
5.5 测试图片验证码生成
我们对这段Base64进行解码,这里就使用一个在线工具:base64图片在线转换工具 - 站长工具 (chinaz.com)
📍 注意需要在上图中得到的字符串前面指定转换格式:data:image/jpg;base64,
7.验证码未来发展的讨论
验证码技术其实是一个攻防博弈的动态发展的技术,因此,随着验证码发展得越来越安全,相应的破解技术也跟着不断发展,随之而来的,是双方的
资源成本越来越高。
例如对于互联网应用来说,商业验证码、短信验证码等这些验证码,如果应用的业务流程控制不好,很容易被羊毛党、黑客等人利用,造成成本极大浪费。而网上逐渐出现的各种奇葩验证码,人眼难辨,也让很多正常用户叫苦不迭。
对于破解方来说,随着机器学习以及人工打码等技术的不断发展,可选择的技术手段也越来越多。验证码技术,也在破解方的大量资源投入下,变得越来越鸡肋。并且,很多互联网应用逐渐复杂的验证方式,安全性提高的同时,也提高了很多正常用户的使用门槛,成为了各种电信诈骗的温床。验证码技术逐渐开始偏离了最初的初衷。
而未来的验证码技术,一方面,会通过引入更多的验证元素来进一步提高验证码的安全性,例如刷脸、刷指纹、语音交互、点选你购买过的商品等。另一方面,通过对行为式验证码的研究逐渐深入,可以从传统的面向结果的验证,转化成面向过程的行为验证,并逐渐减少人工参与,降低关键信息被劫持的风险,形成更多对用户无感知的验证方式。例如分析用户鼠标轨迹、按键频率、使用习惯等。
总之,验证码技术,是一个所有人都将亲身参与的精彩世界。