SpringSecurity+Redis实现图片验证码的创建与验证

1 整体概述

本博客接上两篇博客,整合谷歌Captcha动态验证码图片生成工具和Redis本地缓存实现了用户登录时的图片验证码图片生成及验证操作。

Maven配置文件中导入相关依赖,见下:

<!--谷歌的验证码图片生成器-->
<dependency>
    <groupId>com.github.axet</groupId>
    <artifactId>kaptcha</artifactId>
    <version>0.0.9</version>
</dependency>
<!--redis工具包-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- hutool工具类-->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.3.3</version>
</dependency>

2 整合配置Redis

2.1 封装Redis工具类

封装一个Redis工具类,实现缓存信息的增删改查等操作,具体见仓库代码:

https://gitee.com/strivezhangp/java-study-log.git

2.2 Redis配置类

只用redis观察数据是看不出来的,因为它本身使用了序列化的手段 所以在此进行一个配置,目的 是为Spring Boot项目提供一个自定义的RedisTemplate,该RedisTemplate使用JSON作为值的序列化格式,而键使用字符串格式进行序列化。这有助于在Redis中存储复杂的Java对象,并以可读的方式存储和检索这些对象。

主要实现了以下几个方面:

  • 重写redisTemplate 改变原有的序列化的方式,RedisConnectionFactory 连接池。
  • 选择序列化方式,该对象用于将Java对象序列化为JSON格式的字符串,以便存储在Redis中。
@Configuration
public class RedisConfig {
    /**
     * 重写redisTemplate 改变原有的序列化的方式
     * RedisConnectionFactory 连接池
     */
    @Bean
    RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        /**
         * 该对象用于将Java对象序列化为JSON格式的字符串,以便存储在Redis中
         */
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(new ObjectMapper());

        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        return redisTemplate;
    }
}

3 引入谷歌验证码生成工具

3.1 Kaptcha工具配置

通过配置文件设定验证码图片的一些属性的设置,具体的配置项见文章末尾。

/**
 * 验证码生成工具配置类
 */
@Configuration
public class CaptchaConfig {
    @Bean
    public DefaultKaptcha producer() {
        // 设置动态验证码的属性
        Properties properties = new Properties();
        properties.put("kaptcha.border", "no");
        properties.put("kaptcha.textproducer.font.color", "black");
        properties.put("kaptcha.textproducer.char.space", "4");
        properties.put("kaptcha.textproducer.font.size", "30");
        properties.put("kaptcha.image.height", "40");
        properties.put("kaptcha.image.width", "120");

        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

3.2 添加验证码请求的接口

通过接口,用户请求到验证码后,实现验证码的生成、以及base64格式的转换,然后将特定格式的验证码图片信息以及唯一识别码uuid一起返回给前端,前端将uuid保存到表单提交信息中,在输入验证码后,提交表单,实现验证码的展示。

与此同时,生成验证码的同时,生成一个UUID随机键值,和验证码code一起存储到Redis缓存中,便于用户后续登录与之进行比较,实现验证码的校验。

值得注意的是,验证码图片的格式有很多种,一定要转化为base64编码,返回前端。此处我使用了在后端拼接验证码图片前缀,也可以在前端实现这个拼接。(前缀一定要加上)

在将Code和uuid信息存储到Redis缓存中时,添加一个KEY值,此处使用一个常量来定义,将uuid存入到Redis中。

为什么要用uuid?

***uuid保证了生成的验证码code的唯一性,确保用户验证的是当前生成的验证码,而不是之前生成的某一个验证码。***通常会将验证码与一个UUID(Universally Unique Identifier,通用唯一识别码)关联,并将这个UUID作为键(key)存入Redis中,同时验证码本身(或者其加密形式)作为值(value)存储。这样做的主要原因有以下几点:

  • 验证与防止重放攻击
    当用户请求验证码时,后端生成一个验证码和对应的UUID,并将它们存入Redis。当用户提交验证码进行验证时,后端可以从Redis中根据UUID检索出原始的验证码,然后与用户提交的验证码进行比较。这样可以确保用户提交的是之前请求过的有效验证码,从而防止重放攻击(即使用之前请求过的验证码进行非法操作)。
  • 有效期管理
    通过Redis,可以为每个验证码设置过期时间。这样,即使验证码被泄露,它在一段时间后也会自动失效,增加了系统的安全性。
public static final String CAPTCHA_KEY = "captchaKey";
@RestController
public class CaptchaController {
    // 注入验证码生成工具
    @Autowired
    private Producer producer;
    @Autowired
    RedisUtil redisUtil;

    @GetMapping("/captcha")
    public Result captcha() throws IOException {
        // 生成验证码
        String uuid = UUID.randomUUID().toString();
        String code = producer.createText(); // 随机五位数

        // 生成图片
        BufferedImage image = producer.createImage(code);
        // 转换图片流
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ImageIO.write(image, "jpg", outputStream);
        Base64Encoder encoder = new Base64Encoder();
        String str = "data:image/jpeg;base64,"; // 固定图片url前缀
        String base64Image = str + encoder.encode(outputStream.toByteArray());

        System.out.println(base64Image);
        // 存入缓存
        redisUtil.hset(CaptchaFilter.CAPTCHA_KEY, uuid, code, 120);
        return Result.success(
                MapUtil.builder()
                        .put("uuid", uuid)
                        .put("captchaImg", base64Image)
                        .build()
        );
    }
}

4 验证码校验

4.1 添加一个验证码校验异常处理类

验证码处理类,继承于一个Security中的异常类AuthenticationException,继承父类的构造方法,返回一个自定义的异常信息。

/**
 * 验证码异常类
 */
public class CaptchaExpection extends AuthenticationException {
    /**
     * 继承父类构造方法
     * @param msg
     */
    public CaptchaExpection(String msg){
        super(msg);
    }
}

4.2 定义验证码过滤器

过滤器在Security用户名密码验证之前,通过访问路径以及前端表单提交的字符串信息,进行验证码校验。校验成功,继续按照security配置往前面过滤器走,否则跳转到登陆失败处理器,对错误异常进行处理。

@Component
public class CaptchaFilter extends OncePerRequestFilter {
    public static final String CAPTCHA_KEY = "captchaKey";

    @Autowired
    UserLoginFailureHandler userLoginFailureHandler;
    @Autowired
    RedisUtil redisUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        // 拦截请求路径
        String url = httpServletRequest.getRequestURI();
        if ("/login".equals(url) && httpServletRequest.getMethod().equals("POST")) {
            // 验证么校验
            try {
                validateCaptcha(httpServletRequest);
            } catch (CaptchaExpection e) {
                // 验证码错误 捕捉异常 返回登陆失败处理器
                userLoginFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
            }
        }
        // 正确 继续往前走到下一个过滤器
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

    private void validateCaptcha(HttpServletRequest httpServletRequest) throws CaptchaExpection {
        String code = httpServletRequest.getParameter("code"); // 获取请求中的验证码
        String uuid = httpServletRequest.getParameter("uuid"); // 获取请求中的验证码对应的key

        // 验证码错误的两种情况 为空 字符比对
        if (StringUtils.isBlank(code) || StringUtils.isBlank(uuid)) {
            throw new CaptchaExpection("验证码错误");
        } else if (!code.equalsIgnoreCase(redisUtil.hget(CAPTCHA_KEY, uuid).toString())) {
            throw new CaptchaExpection("验证码错误");
        } else {
            // 正确情况 清除缓存 保证验证码一次性使用
            redisUtil.hdel(CAPTCHA_KEY, uuid);
        }
    }
}

4.3 验证码校验异常处理

登陆失败处理器,对错误类型进行判断,然后返回给用户不同的异常信息。

String errorMessage;
// 判断异常类型是否为一个异常的实例,进而返回不同的错误信息
if (e instanceof BadCredentialsException) {
    errorMessage = "用户名或密码错误";
} else if (e instanceof CaptchaExpection){
    errorMessage = e.getMessage();
} else {
    errorMessage = "其他异常";
}
Result result = Result.failure(errorMessage);

5 测试效果

为了直观的看到验证码图片,结合VUE写了一个简单的登陆表单,展示了验证码图片效果如下:

// api 封装
export function captcha() {
    return request({
        url: "/captcha",
        method: "get"
    })
}

// 页面文件请求路由
const getCaptcha = () => {
    captcha().then(res => {
        console.log(res.data);
        captchaImg.value = res.data.data.captchaImg;
        loginForm.value.uuid = res.data.data.uuid;
    })
}
getCaptcha();

// 模板文件
const captchaImg = ref('')

<div class="login-code">
    <img :src="captchaImg" class="login-code-img" />
</div>

springboot图片素材管理_springboot图片素材管理

假设生成的验证码固定,进项登录接口请求测试,结果表示成功。

String uuid = "11111";
String code = "Ac111";

// 验证码比对忽略大小写
!code.equalsIgnoreCase(redisUtil.hget(CAPTCHA_KEY, uuid).toString()))

springboot图片素材管理_Redis_02

附:验证码配置相关属性

kaptcha.border  是否有边框  默认为true  我们可以自己设置yes,no   
kaptcha.border.color   边框颜色   默认为Color.BLACK   
kaptcha.border.thickness  边框粗细度  默认为1   
kaptcha.producer.impl   验证码生成器  默认为DefaultKaptcha   
kaptcha.textproducer.impl   验证码文本生成器  默认为DefaultTextCreator   
kaptcha.textproducer.char.string   验证码文本字符内容范围  默认为abcde2345678gfynmnpwx   
kaptcha.textproducer.char.length   验证码文本字符长度  默认为5   
kaptcha.textproducer.font.names    验证码文本字体样式  默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)   
kaptcha.textproducer.font.size   验证码文本字符大小  默认为40   
kaptcha.textproducer.font.color  验证码文本字符颜色  默认为Color.BLACK   
kaptcha.textproducer.char.space  验证码文本字符间距  默认为2   
kaptcha.noise.impl    验证码噪点生成对象  默认为DefaultNoise   
kaptcha.noise.color   验证码噪点颜色   默认为Color.BLACK   
kaptcha.obscurificator.impl   验证码样式引擎  默认为WaterRipple   
kaptcha.word.impl   验证码文本字符渲染   默认为DefaultWordRenderer   
kaptcha.background.impl   验证码背景生成器   默认为DefaultBackground   
kaptcha.background.clear.from   验证码背景颜色渐进   默认为Color.LIGHT_GRAY   
kaptcha.background.clear.to   验证码背景颜色渐进   默认为Color.WHITE   
kaptcha.image.width   验证码图片宽度  默认为200   
kaptcha.image.height  验证码图片高度  默认为50