大家好,我是 god23bin,今天说说验证码功能的实现,相信大家都经常接触到验证码的,毕竟平时上网也能遇到各种验证码,需要我们输入验证码进行验证我们是人类,而不是机器人。
验证码有多种类型,比如图片验证码、短信验证码和邮件验证码等等,虽说多种类型,图片也好,短信也好,邮件也好,都是承载验证码的载体,最主要的核心就是一个验证码的生成、存储和校验。
本篇文章就从这几个方面出发说说验证码,废话不多说,下面开始正文。
验证码验证的功能,其实现思路还是挺简单的,不论是图片验证码、短信验证码还是邮件验证码,无非就以下几点:
首先,需要知道的就是验证码的生成,这就涉及到生成验证码的算法,可以自己纯手写,也可以使用人家提供的工具,这里我就介绍下面 4 种生成验证码的方式。
需求:随机产生一个 n 位的验证码,每位可能是数字、大写字母、小写字母。
实现:本质就是随机生成字符串,字符串可包含数字、大写字母、小写字母。
准备一个包含数字、大写字母、小写字母的字符串,借助 Random 类,循环 n 次随机获取字符串的下标,就能拼接出一个随机字符组成的字符串了。
package cn.god23bin.demo.util; import java.util.Random; public class MyCaptchaUtil { /** * 生成 n 位验证码 * @param n 位数 * @return n 位验证码 **/ public static String generateCode(int n) { String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; StringBuilder sb = new StringBuilder(); Random random = new Random(); for (int i = 0; i < n; i++) { int index = random.nextInt(chars.length()); sb.append(chars.charAt(index)); } return sb.toString(); } }
实现:使用 Java 的 awt 和 swing 库来生成图片验证码。下面使用 BufferedImage 类创建一个指定大小的图片,然后随机生成 n 个字符,将其画在图片上,将生成的字符和图片验证码放到哈希表返回。后续我们就可以拿到验证码的文本值,并且可以将图片验证码输出到指定的输出流中。
package cn.god23bin.demo.util; import java.awt.*; import java.awt.image.BufferedImage; import java.util.HashMap; import java.util.Map; public class MyCaptchaUtil { /** * 生成 n 位的图片验证码 * @param n 位数 * @return 哈希表,code 获取文本验证码,img 获取 BufferedImage 图片对象 **/ public static Map<String, Object> generateCodeImage(int n) { int width = 100, height = 50; BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g = image.createGraphics(); g.setColor(Color.LIGHT_GRAY); g.fillRect(0, 0, width, height); String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; Random random = new Random(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < n; i++) { int index = random.nextInt(chars.length()); char c = chars.charAt(index); sb.append(c); g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255))); g.setFont(new Font("Arial", Font.BOLD, 25)); g.drawString(Character.toString(c), 20 + i * 15, 25); } Map<String, Object> res = new HashMap<>(); res.put("code", sb.toString()); res.put("img", image); return res; } }
我们可以写一个获取验证码的接口,以二进制流输出返回给前端,前端可以直接使用 img
标签来显示我们返回的图片,只需在 src
属性赋值我们的获取验证码接口。
@RequestMapping("/captcha") @RestController public class CaptchaController { @GetMapping("/code/custom") public void getCode(HttpServletResponse response) { Map<String, Object> map = MyCaptchaUtil.generateCodeImage(5); System.out.println(map.get("code")); BufferedImage img = (BufferedImage) map.get("img"); // 设置响应头,防止缓存 response.setHeader("Cache-Control", "no-store, no-cache"); response.setContentType("image/png"); try { ImageIO.write(img, "png", response.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } } }
引入依赖:可以单独引入验证码模块或者全部模块都引入
<!-- 验证码模块 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-captcha</artifactId> <version>5.8.15</version> </dependency> <!-- 全部模块都引入 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.15</version> </dependency>
// 设置图形验证码的宽和高,同时生成了验证码,可以通过 lineCaptcha.getCode() 获取文本验证码 LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100);
// 设置图形验证码的宽、高、验证码字符数、干扰元素个数 CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(200, 100, 4, 20);
// 定义图形验证码的宽、高、验证码字符数、干扰线宽度 ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(200, 100, 4, 4);
获取验证码接口:
@RequestMapping("/captcha") @RestController public class CaptchaController { @GetMapping("/code/hutool") public void getCodeByHutool(HttpServletResponse response) { LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100); System.out.println("线段干扰的验证码:" + lineCaptcha.getCode()); // 设置响应头,防止缓存 response.setHeader("Cache-Control", "no-store, no-cache"); response.setContentType("image/png"); try { lineCaptcha.write(response.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } } }
Kaptcha 是谷歌的一个生成验证码工具包,我们简单配置其属性就可以实现验证码的验证功能。
引入依赖项:它只有一个版本:2.3.2
<dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency>
简单看看 kaptcha 属性:
属性 | 描述 | 默认值 |
---|---|---|
kaptcha.border | 图片边框,合法值:yes , no | yes |
kaptcha.border.color | 边框颜色,合法值: r,g,b (and optional alpha) 或者 white,black,blue. | black |
kaptcha.border.thickness | 边框厚度,合法值:>0 | 1 |
kaptcha.image.width | 图片宽 | 200 |
kaptcha.image.height | 图片高 | 50 |
kaptcha.producer.impl | 图片实现类 | com.google.code.kaptcha.impl.DefaultKaptcha |
kaptcha.textproducer.impl | 文本实现类 | com.google.code.kaptcha.text.impl.DefaultTextCreator |
kaptcha.textproducer.char.string | 文本集合,验证码值从此集合中获取 | abcde2345678gfynmnpwx |
kaptcha.textproducer.char.length | 验证码长度 | 5 |
kaptcha.textproducer.font.names | 字体 | Arial, Courier |
kaptcha.textproducer.font.size | 字体大小 | 40px |
kaptcha.textproducer.font.color | 字体颜色,合法值: r,g,b 或者 white,black,blue. | black |
kaptcha.textproducer.char.space | 文字间隔 | 2 |
kaptcha.noise.impl | 干扰实现类 | com.google.code.kaptcha.impl.DefaultNoise |
kaptcha.noise.color | 干扰颜色,合法值: r,g,b 或者 white,black,blue. | black |
kaptcha.obscurificator.impl | 图片样式: 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy | com.google.code.kaptcha.impl.WaterRipple |
kaptcha.background.impl | 背景实现类 | com.google.code.kaptcha.impl.DefaultBackground |
kaptcha.background.clear.from | 背景颜色渐变,开始颜色 | light grey |
kaptcha.background.clear.to | 背景颜色渐变,结束颜色 | white |
kaptcha.word.impl | 文字渲染器 | com.google.code.kaptcha.text.impl.DefaultWordRenderer |
kaptcha.session.key | session key | KAPTCHA_SESSION_KEY |
kaptcha.session.date | session date | KAPTCHA_SESSION_DATE |
简单配置下 Kaptcha:
package cn.god23bin.demo.config; 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; @Configuration public class KaptchaConfig { /** * 配置生成图片验证码的bean * @return */ @Bean(name = "kaptchaProducer") public DefaultKaptcha getKaptchaBean() { DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); Properties properties = new Properties(); properties.setProperty("kaptcha.border", "no"); properties.setProperty("kaptcha.textproducer.font.color", "black"); properties.setProperty("kaptcha.textproducer.char.space", "4"); properties.setProperty("kaptcha.textproducer.char.length", "4"); properties.setProperty("kaptcha.textproducer.char.string", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); Config config = new Config(properties); defaultKaptcha.setConfig(config); return defaultKaptcha; } }
也是和 Hutool 一样,很简单就能生成验证码了。如下:
// 生成文字验证码 String text = kaptchaProducer.createText(); // 生成图片验证码 BufferedImage image = kaptchaProducer.createImage(text);
获取验证码接口:
@RequestMapping("/captcha") @RestController public class CaptchaController { @Autowired private Producer kaptchaProducer; @GetMapping("/code/kaptcha") public void getCodeByKaptcha(HttpServletResponse response) { // 生成文字验证码 String text = kaptchaProducer.createText(); System.out.println("文字验证码:" + text); // 生成图片验证码 BufferedImage image = kaptchaProducer.createImage(text); // 设置响应头,防止缓存 response.setHeader("Cache-Control", "no-store, no-cache"); response.setContentType("image/jpeg"); try { ImageIO.write(image, "jpg", response.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } } }
上面的验证码的生成,就仅仅是生成验证码,并没有将验证码存储在后端,所以现在我们需要做的是:将验证码存储起来,便于后续的校验对比。
那么存储到什么地方呢?如果你没接触过 Redis,那么第一次的想法可能就是存储到关系型数据库中,比如 MySQL。想当年,我最开始的想法就是这样哈哈哈。
不过,目前用得最多的就是将验证码存储到 Redis 中,好处就是减少了数据库的压力,加快了验证码的读取效率,还能轻松设置验证码的过期时间。
引入 Redis 依赖项:
我们使用 Spring Data Redis,它提供了 RedisTemplate
和 StringRedisTemplate
模板类,简化了我们使用 Java 进行 Redis 的操作。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
简单配置下 Redis:
spring: redis: host: localhost port: 6379 database: 1 timeout: 5000
@Configuration public class RedisConfig extends CachingConfigurerSupport { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { // 大多数情况,都是选用<String, Object> RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); // 使用JSON的序列化对象,对数据 key 和 value 进行序列化转换 Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); // ObjectMapper 是 Jackson 的一个工作类,作用是将 JSON 转成 Java 对象,即反序列化。或将 Java 对象转成 JSON,即序列化 ObjectMapper mapper = new ObjectMapper(); // 设置序列化时的可见性,第一个参数是选择序列化哪些属性,比如时序列化 setter? 还是 filed? 第二个参数是选择哪些修饰符权限的属性来序列化,比如 private 或者 public,这里的 any 是指对所有权限修饰的属性都可见(可序列化) mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); jackson2JsonRedisSerializer.setObjectMapper(mapper); // 设置 RedisTemplate 模板的序列化方式为 jacksonSeial template.setDefaultSerializer(jackson2JsonRedisSerializer); return template; } }
将验证码存储到 Redis 设置 5 分钟的过期时间,Redis 是 Key Value 这种形式存储的,所以需要约定好 Key 的命名规则。
命名的时候,为了区分为每个用户生成的验证码,所以需要一个标识,刚好可以通过当前请求的 HttpSession 中的 SessionID 作为唯一标识,拼接到 Key 的名称中。
当然,也不一定使用 SessionID 作为唯一标识,如果能知道其他的,也可以用其他的作为标识,比如拼接用户的手机号。
实现:
@RequestMapping("/captcha") @RestController public class CaptchaController { @Autowired private Producer kaptchaProducer; @Autowired private RedisTemplate<String, Object> redisTemplate; @GetMapping("/code") public void getCode(HttpServletRequest request, HttpServletResponse response) { // 生成文字验证码 String text = kaptchaProducer.createText(); System.out.println("文字验证码:" + text); // 生成图片验证码 BufferedImage image = kaptchaProducer.createImage(text); // 存储到 Redis 设置 5 分钟的过期时间 // 约定好存储的 Key 的命名规则,这里使用 code_sessionId_type_1 表示图形验证码 // Code_sessionId_Type_1:分为 3 部分,code 表明是验证码,sessionId 表明是给哪个用户的验证码,type_n 表明验证码类型,n 为 1 表示图形验证码,2 表示短信验证码,3 表示邮件验证码 String key = "code_" + request.getSession().getId() + "_type_1"; redisTemplate.opsForValue().set(key, text, 5, TimeUnit.SECONDS); response.setHeader("Cache-Control", "no-store, no-cache"); response.setContentType("image/jpeg"); try { ImageIO.write(image, "jpg", response.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } } }
上面代码中有一个额外的设计就是,由于发送的验证码有多种类型(图形验证码、短信验证码、邮件验证码),所以加多了一个 type_n
来标识当前存储的验证码是什么类型的,方便以后出现问题快速定位。
实际上,这里的命名规则,可以根据你的具体需求来定制,又比如说,登录的时候需要验证码、注册的时候也需要验证码、修改用户密码的时候也需要验证码,为了便于出现问题进行定位,也可以继续加多一个标识 when_n
,n 为 1 表示注册、n 为 2 表示登录,以此类推。
我们模拟登录的时候进行验证码的校验,使用一个 LoginDTO 对象来接收前端的登录相关的参数。
package cn.god23bin.demo.model.domain.dto; import lombok.Data; @Data public class LoginDTO { private String username; private String password; /** * 验证码 */ private String code; }
写一个登录接口,登录的过程中,校验用户输入的验证码。
@RequestMapping("/user") @RestController public class UserController { @Autowired private RedisTemplate<String, Object> redisTemplate; @PostMapping("/login") public Result<String> login(@RequestBody LoginDTO loginDTO, HttpServletRequest request) { if (!"root".equals(loginDTO.getUsername()) || !"123456".equals(loginDTO.getPassword())) { return Result.fail("登录失败!账号或密码不正确!"); } // 校验用户输入的验证码 String code = loginDTO.getCode(); String codeInRedis = (String) redisTemplate.opsForValue().get("code_" + request.getSession().getId() + "_type_1"); if (!code.equals(codeInRedis)) { return Result.fail("验证码不正确!"); } return Result.ok("登录成功!"); } }
至此,便完成了验证码功能的实现。
验证码功能的实现现在是OK的,但还有一点需要注意,那就是防止验证码被随意调用获取,或者被大量调用。如果不做限制,那么谁都能调用,就非常大的可能会被攻击了。
我们上面实现的验证码功能是图形验证码,是校验用户从图形验证码中看到后输入的数字字母组合跟后端生成的组合是否是一致的。对于图形验证码,到这里就可以了,不用限制(当然想限制也可以)。但是对于短信验证码,就还不可以。我们需要额外考虑一些防刷机制,以保障系统的安全性和可靠性(因为发短信是要钱的啊!)。
对于短信来说,一种常见的攻击方式是「短信轰炸」,攻击者通过自动批量提交手机号码、模拟IP等手段,对系统进行大规模的短信请求,从而消耗资源或干扰正常业务。为了应对这种情况,我们需要设计一些防刷机制。
目前我了解到的防刷机制有下面几种,如果你有别的方法,欢迎评论说出来噢!
至于这些机制的实现,有机会再写写,你感兴趣的话可以自己去操作试试!
本篇文字就说了验证码功能的实现思路和实现,包括验证码的生成、存储、展示和校验。
生成验证码可以手写也可以借助工具。
存储一般是存储在 Redis 中的,当然你想存储在 MySQL 中也不是不可以,就是需要自己去实现诸如过期时间的功能。
展示可以通过文本展示或者图片展示,我们可以返回一个二进制流给前端,前端通过 img
标签的 src
属性去请求我们的接口。
校验就拿到用户输入的验证码,和后端生成的验证码进行比对,相同就验证成功,否则失败。
最后我们也说了验证码的防刷机制,这是需要考虑的,这里的防刷机制对于使用大量不同手机号、不同 IP 地址是没效果的,依旧可以暴刷。所以这部分内容还是有待研究的。也欢迎大家在评论区说出你的看法!
希望各位屏幕前的靓仔靓女们
给个三连!你轻轻地点了个赞,那将在我的心里世界增添一颗明亮而耀眼的星!
咱们下期再见!