前语

咱们好,我是 god23bin,今日说说验证码功用的完成,信任咱们都经常触摸到验证码的,毕竟平时上网也能遇到各种验证码,需求咱们输入验证码进行验证咱们是人类,而不是机器人。

验证码有多种类型,比方图片验证码、短信验证码和邮件验证码等等,虽说多种类型,图片也好,短信也好,邮件也好,都是承载验证码的载体,最主要的核心便是一个验证码的生成、存储和校验。

本篇文章就从这几个方面出发说说验证码,废话不多说,下面开端正文。

完成思路

验证码验证的功用,其完成思路仍是挺简略的,不论是图片验证码、短信验证码仍是邮件验证码,无非就以下几点:

  1. 验证码本质便是一堆字符的组合(数字也好,英文字母也好),后端生成验证码,并存储到某个方位(比方存储到 Redis,并设置验证码的过期时刻)。
  2. 回来验证码给前端页面、发送短信验证码给用户或许发送邮件验证码给用户。验证码能够是以文字显现或许图片显现。
  3. 用户输入看到的验证码,并提交验证(验证也能够忽略巨细写,当然具体看需求)。
  4. 后端将用户输入的验证码拿过来进行校验,比照用户输入的验证码是否和后端生成的共同,共同就验证成功,否则验证失利。

验证码的生成

首先,需求知道的便是验证码的生成,这就涉及到生成验证码的算法,能够自己纯手写,也能够运用人家供给的东西,这儿我就介绍下面 4 种生成验证码的办法。

1. 纯原生手写生成文本验证码

需求:随机发生一个 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();
    }
}

2. 纯原生手写生成图片验证码

完成:运用 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();
        }
    }
}

3. 运用 Hutool 东西生成图形验证码

引进依靠:能够独自引进验证码模块或许悉数模块都引进

<!-- 验证码模块 -->
<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();
        }
    }
}

4. 运用 Kaptcha 生成验证码

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

引进 Redis 依靠项:

咱们运用 Spring Data Redis,它供给了 RedisTemplateStringRedisTemplate 模板类,简化了咱们运用 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

将验证码存储到 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等手段,对体系进行大规模的短信恳求,从而耗费资源或搅扰正常业务。为了应对这种情况,咱们需求设计一些防刷机制。

防刷机制

目前我了解到的防刷机制有下面几种,假如你有其他办法,欢迎谈论说出来噢!

  1. 图形验证码或许滑动验证:发送短信前先运用图形验证码或许滑动进行验证,验证成功才干调用发送短信验证码的接口。
  2. 时刻约束:从用户点击发送短信验证码开端,前端进行一个 60 秒的倒数,在这 60 秒之内,用户无法提交发送信息的恳求的,这样就约束了发送短信验证码的接口的调用次数。不过这种办法,假如被攻击者知道了发送短信的接口,那也是会被刷的。
  3. 手机号约束:对运用同一个手机号码进行注册或许其他发送短信验证码的操作的时分,体系能够对这个手机号码进行约束,例如,一天只能发送 5 条短信验证码,超出约束则做出提示(如:体系繁忙,请稍后再试)。但是,这也只能够防止人工手动刷短信罢了,关于批量运用不同手机号码来刷短信的机器,同样是会被刷。
  4. IP地址约束:记录恳求的IP地址,并对同一 IP 地址的恳求进行约束,比方约束某个 IP 地址在一定时刻内只能发送特定数量的验证码。同样,也是能够被轰炸的。

至于这些机制的完成,有机会再写写,你感兴趣的话能够自己去操作试试!

总结

本篇文章就说了验证码功用的完成思路和完成,包括验证码的生成、存储、展现和校验。

  • 生成验证码能够手写也能够凭借东西。

  • 存储一般是存储在 Redis 中的,当然你想存储在 MySQL 中也不是不能够,便是需求自己去完成比如过期时刻的功用。

  • 展现能够经过文本展现或许图片展现,咱们能够回来一个二进制流给前端,前端经过 img 标签的 src 特点去恳求咱们的接口。

  • 校验就拿到用户输入的验证码,和后端生成的验证码进行比对,相同就验证成功,否则失利。

最终咱们也说了验证码的防刷机制,这是需求考虑的,这儿的防刷机制关于运用大量不同手机号、不同 IP 地址是没作用的,依旧能够暴刷。所以这部分内容仍是有待研讨的。也欢迎咱们在谈论区说出你的看法!

最终的最终

希望各位屏幕前的靓仔靓女们给个三连!你轻轻地点了个赞,那将在我的心里世界增添一颗明亮而耀眼的星!

咱们下期再见!