springboot集成Kaptcha实现图片验证码过程
目录
- springboot:集成Kaptcha实现图片验证码
- 一、导入依赖
- 系统配置文件
- 二、生成验证码
- 1、Kaptcha的配置
- 2、自定义验证码文本生成器
- 3、具体实现
- 三、校验验证码
- 1、controller接口
- 2、自定义前端过滤器
- 3、自定义验证码处理过滤器
- 4、自定义BodyReaderFilter解决读取body错误问题
- 5、注意
- 总结
springboot:集成Kaptcha实现图片验证码
- 系统环境:
- Windows 10
- jdk 1.8
- springboot版本: 2.1.10.RELEASE
一、导入依赖
<dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-Redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
系统配置文件
server: port: 81 spring: redis: database: 1 host: 127.0.0.1 port: 6379www.devze.com password: # 密码(默认为空) timeout: 6000ms # 连接超时时长(毫秒) lettuce: pool: max-active: 1000 # 连接池最大连接数(使用负值表示没有限制) max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制) max-idle: 10 # 连接池中的最大空闲连接 min-idle: 5 # 连接池中的最小空闲连接
二、生成验证码
1、Kaptcha的配置
验证码文本生成器:这个需要自己生成并且修改下面的配置文件为你文件的路径
package com.yolo.springboot.kaptcha.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; /** * @ClassName CaptchaConfig * @Description 验证码配置 * @Author hl * @Date 2022/12/6 9:37 * @Version 1.0 */ @Configuration public class CaptchaConfig { @Bean(name = "captchaProducerMath") public DefaultKaptcha getKaptchaBeanMath() { DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); Properties properties = new Properties(); // 是否有边框 默认为true 我们可以自己设置yes,no properties.setProperty("kaptcha.border", "yes"); // 边框颜色 默认为Color.BLACK properties.setProperty("kaptcha.border.color", "105,179,90"); // 验证码文本字符颜色 默认为Color.BLACK properties.setProperty("kaptcha.textproducer.font.color", "blue"); // 验证码图片宽度 默认为200 properties.setProperty("kaptcha.image.width", "160"); // 验证码图片高度 默认为50 properties.setProperty("kaptcha.image.height", "60"); // 验证码文本字符大小 默认为40 properties.setProperty("kaptcha.textproducer.font.size", "35"); // KAPTCHA_SESSION_KEY properties.setProperty("kaptcha.session.key", "kaptchaCodeMath"); // 验证码文本生成器 properties.setProperty("kaptcha.textproducer.impl", "com.yolo.springboot.kaptcha.config.KaptchaTextCreator"); // 验证码文本字符间距 默认为2 properties.setProperty("kaptcha.textproducer.char.space", "3"); // 验证码文本字符长度 默认为5 properties.setProperty("kaptcha.textproducer.char.length", "6"); // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, // fontSize) properties.setProperty("kaptcha.textproducer.font.names", "Arial,Courier"); // 验证码噪点颜色 默认为Color.BLACK properties.setProperty("kaptcha.noise.color", "white"); // 干扰实现类 properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise"); // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple // 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy // 阴影com.google.code.kaptcha.impl.ShadowGimpy properties.setProperty("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.ShadowGimpy"); Config config = new Config(properties); defaultKaptcha.setConfig(config); return defaultKaptcha; } }
2、自定义验证码文本生成器
package com.yolo.springboot.kaptcha.config; import com.google.code.kaptcha.text.impl.DefaultTextCreator; import java.util.Random; /** * @ClassName KaptchaTextCreator * @Description 验证码文本生成器 * @Author hl * @Date 2022/12/6 10:14 * @Version 1.0 */ public class KaptchaTextCreator extends DefaultTextCreator { private static final String[] Number = "0,1,2,3,4,5,6,7,8,9,10".split(","); @Override public String getText() { int result; Random random = new Random(); int x = random.nextInt(10); int y = random.nextInt(10); StringBuilder suChinese = new StringBuilder(); int randomOperand = (int) Math.round(Math.random() * 2); if (randomOperand == 0) { result = x * y; suChinese.append(Number[x]); suChinese.append("*"); suChinese.append(Number[y]); } else if (randomOperand == 1) { if (!(x == 0) && y % x == 0) { result = y / x; suChinese.append(Number[y]); suChinese.append("/"); suChinese.append(Number[x]); } else { result = x + y; suChinese.append(Number[x]); suChinese.append("+"); suChinese.append(Number[y]); } } else if (randomOperand == 2) { if (x >= y) { result = x - y; suChinese.append(Number[x]); suChinese.append("-"); suChinese.append(Number[y]); } else { result = y - x; suChinese.append(Number[y]); suChinese.append("-"); suChinese.append(Number[x]); } } else { result = x + y; suChinese.append(Number[x]); suChinese.append("+"); suChinese.append(Number[y]); } suChinese.append("=?@").append(result); return suChinese.toString(); } }
3、具体实现
package com.yolo.springboot.kaptcha.controller; import cn.hutool.json.JSONUtil; import com.google.code.kaptcha.Producer; import com.hl.springbootcommon.common.HttpResponseTemp; import com.hl.springbootcommon.common.ResultStat; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.cophpre.StringRedisTemplate; import org.springframework.http.MediaType; import org.springframework.util.FastByteArrayOutputStream; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import javax.imageio.ImageIO; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * @ClandroidassName CaptchaController * @Description 验证码 * @Author hl * @Date 2022/12/6 9:45 * @Version 1.0 */ @RestController @Slf4j public class CaptchaController { @Autowired private Producer producer; @Autowired private StringRedisTemplate redisTemplate; public static final String DEFAULT_CODE_KEY = "random_code_"; /** * @MethodName createCaptcha * @Description 生成验证码 * @param httpServletResponse 响应流 * @Author hl * @Date 2022/12/6 10:30 */ @GetMapping("/create/captcha") public void createCaptcha(HttpServletResponse httpServletResponse) throws IOException { // 生成验证码 String capText = producer.createText(); String capStr = capText.substring(0, capText.lastIndexOf("@")); String result = capText.substring(capText.lastIndexOf("@") + 1); BufferedImage image = producer.createImage(capStr); // 保存验证码信息 String randomStr = UUID.randomUUID().toString().replaceAll("-", ""); System.out.println("随机数为:" + randomStr); redisTemplate.opsForValue().set(DEFAULT_CODE_KEY + randomStr, result, 3600, TimeUnit.SECONDS); // 转换流信息写出 FastByteArrayOutputStream os = new FastByteArrayOutputStream(); try { ImageIO.write(image, "jpg", os); } catch (IOException e) { log.error("ImageIO write err", e); httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND); return; } // 定义response输出类型为image/jpeg类型,使用response输出流输出图片的byte数组 byte[] bytes = os.toByteArray(); //设置响应头 httpServletResponse.setHeader("Cache-Control", "no-store"); //设置响应头 httpServletResponse.setHeader("randomstr",randomStr); //设置响应头 httpServletResponse.setHeader("Pragma", "no-cache"); //在代理服务器端防止缓冲 httpServletResponse.setDateHeader("Expires", 0); //设置响应内容类型 ServletOutputStream responseoutputStream = httpServletResponse.getOutputStream(); responseOutputStream.write(bytes); responseOutputStream.flush(); responseOutputStream.close(); } }
三、校验验证码
这里校验验证码,我用了过滤器来实现的,其中遇到了很多问题,下面有我详细的解决方法
1、controller接口
@PostMapping("/login") public HttpResponseTemp<?> login(@RequestBody LoginDto loginDto){ System.out.println(JSONUtil.toJsonStr(loginDto)); return ResultStat.OK.wrap("","成功"); } @Data public class LoginDto { private String captcha; private String randomStr; }
2、自定义前端过滤器
这里是我写了一个简单的前端页面,然后发现这里会有一些前端的文件,所以需要过滤一下
package com.yolo.springboot.kaptcha.filter; import cn.hutool.core.collection.ListUtil; import org.apache.commons.lang3.StringUtils; import org.springframework.web.filter.ShallowEtagHeaderFilter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; /** * @ClassName SuffixFilter * @Description 前端文件过滤 * @Author hl * @Date 2022/12/6 12:40 * @Version 1.0 */ public class FrontFilter extends ShallowEtagHeaderFilter implements Filter { private static final List<String> suffix = ListUtil.of(".css",".eot",".gif",".ico",".js",".map",".png",".svg",".swf",".ttf",".TTF",".woff",".woff2"); @Override protected boolean shouldNotFilterAsyncDispatch() { return false; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { response.setHeader("Server", "Apache-Coyote/1.1"); response.setHeader("Cache-Control", "max-age=0"); String uri = request.getRequestURI(); if (!StringUtils.isBlank(uri)) { int index = uri.lastIndexOf("."); if (index > 0 && suffix.contains(uri.substring(index))) { response.setHeader("Cache-Control", "max-age=3600"); } if (uri.startsWith("/lib")) { response.setHeader("Cache-Control", "max-age=3600, immutable"); } } super.doFilterInternal(request, response, filterChain); } }
然后需要把我们自定的过滤器加入到spring中让他生效
package com.yolo.springboot.kaptcha.config; import com.yolo.springboot.kaptcha.filter.FrontFilter; import com.yolo.springboot.kaptcha.filter.ImgCodeFilter; import com.yolo.springboot.kaptcha.filter.BodyReaderFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.StringRedisTemplate; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Configuration public class FilterConfig { @Bean public FilterRegistrationBean<?> frontFilterRegistration() { FilterRegistrationBean<FrontFilter> registration = new FilterRegistrationBean<>(); // 将过滤器配置到FilterRegistrationBean对象中 registration.setFilter(new FrontFilter()); // 给过滤器取名 registration.setName("frontFilter"); // 设置过滤器优先级,该值越小越优先被执行 registration.setOrder(0); List<String> urlPatterns = new ArrayList<>(); urlPatterns.add("/*"); // 设置urlPatterns参数 registration.setUrlPatterns(urlPatterns); return registration; } }
这里我给他设置的拦截全部请求,并且优先级是第一位的
3、自定义验证码处理过滤器
package com.yolo.springboot.kaptcha.filter; import com.alibaba.fastjson.JSONObject; import lombok.AllArgsConstructor; import lombok.SneakyThrows; import org.apache.commons.lang3.StringUtils; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.stream.Collectors; /** * @ClassName ImgCodeFilter * @Description 验证码处理 * @Author hl * @Date 2022/12/6 10:35 * @Version 1.0 */ @AllArgsConstructor public class ImgCodeFilter implements Filter { private final StringRedisTemplate redisTemplate; private final static String AUTH_URL = "/login"; public static final String DEFAULT_CODE_KEY = "random_code_"; /** * filter对象只会创建一次,init方法也只会执行一次。 */ @Override public void init(FilterConfig filterConfig) throws ServletException { Filter.super.init(filterConfig); } /** * 主要的业务代码编写方法 */ @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { //只有转换为HttpServletRequest 对象才可以获取路径参数 HttpServletRequest request = (HttpServletRequest) servletRequest; String requestURI = request.getRequestURI(); if (!AUTH_URL.equalsIgnoreCase(requestURI)){ //放行 filterChain.doFilter(servletRequest, servletResponse); } try { String bodyStr = resolveBodyFromRequest(request); JSONObject bodyJson=JSONObject.parseObject(bodyStr); String code = (String) bodyJson.get("captcha"); String randomStr = (String) bodyJson.get("randomStr"); // 校验验证码 checkCode(code, randomStr); } catch (Exception e) { HttpServletResponse response = (HttpServletResponse) servletResponse; response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); response.setHeader("Content-Type", "application/json;charset=UTF-8"); response.sendError(HttpStatus.UNAUTHORIZED.value(),"验证码认证失败或者过期"); } filterChain.doFilter(servletRequest, servletResponse); } /** * 检查code */ @SneakyThrows private void checkCode(String code, String randomStr) { if (StringUtils.isBlank(code)) { throw new RuntimeException("验证码不能为空"); } if (StringUtils.isBlank(randomStr)) { throw new RuntimeException("验证码不合法"); } String key = DEFAULT_CODE_KEY + randomStr; String result = redisTemplate.opsForValue().get(key); redisTemplate.delete(key); if (!code.equalsIgnoreCase(result)) { throw new RuntimeException("验证码不合法"); } } /** * @MethodName resolveBodyFromRequest * @Description 不能和@Requestbody搭配使用 * 原因: getInputStream() has already been called for this request,流不能读取第二次,@Requestbody已经读取过一次了 * @param request 请求流 * 解决方案: 重写HttpServletRequestWrapper类,将HttpServletRequest的数据读到wrapper的缓存中去(用 byte[] 存储),再次读取时读缓存就可以了 * 当接口涉及到上传下载时,会有一些异常问题,最好在过滤器中排除这些路径 * @return: java.lang.String * @Author hl * @Date 2022/12/6 15:18 */ private String resolveBodyFromRequest(HttpServletRequest request){ String bodyStr = null; // 获取请求体 if ("POST".equalsIgnoreCase(request.getMethod())){ try { bodyStr = request.getReader().lines().collect(Collectors.joining(System.lineSeparator())); } catch (IOException e) { throw new RuntimeException(e); } } return bodyStr; } /** * 在销毁Filter时自动调用。 */ @Override public void destroy() { Filter.super.destroy(); } }
加入到配置中
这里校验需要用到redis,用构造方法给他注入
@Autowired private StringRedisTemplate redisTemplate; @Bean public FilterRegistrationBean<?> imgCodeFilterRegistration() { FilterRegistrationBean<ImgCodeFilter> registration = new FilterRegistrationBean<>(); // 将过滤器配置到FilterRegistrationBean对象中 registration.setFilter(new ImgCodeFilter(redisTemplate)); // 给过滤器取名 registration.setName("imgCodeFilter"); // 设置过滤器优先级,该值越小越优先被执行 registration.setOrder(2); List<String> urlPatterns = new ArrayList<>(); urlPatterns.add("/login"); // 设置urlPatterns参数 registration.setUrlPatterns(urlPatterns); return registration; }
遇到的问题及解决思路
问题:流不能多次被调用
ERROR m.e.handler.GlobalExceptionHandler - getInputStream() has already been called for this request
java.lang.IllegalStateException: getInputStream() has already been called for this request at org.apache.catalina.connector.Request.getReader(Request.java:1212)&nbswww.devze.comp; at org.apache.catalina.connector.RequestFacade.getReader(RequestFacade.java:504)
根据报错信息分析简单来说,就是getInputStream()已经被调用了,不能再次调用。可是我看代码上,我也没调用。经过一番检索,原来@RequestBody注解配置后,默认会使用流来读取数据
具体原因:
- 默认配置时,getInputStream()和getReader()一起使用会报错,使用两遍getInputStream(),第二遍会为空
- 当存在@RequestBody等注解时,springMVC已读取过一遍流,默认单独使用getInputStream()或getReader()都为空。
实测,不加@RequestBody注解,可以如期获得请求中的json参数,但是又不得不加@RequestBody注解。这样就需要新的思路
解决思路:
写filter继承HttpServletRequestWrapper,缓存InputStream,覆盖getInputStream()和getReader()方法,使用ByteArrayInputStream is = new ByteArrayInputStream(body.getBytes());读取InputStream。下面自定义BodyReaderFilter和BodyReaderWrapper就是具体解决方法
4、自定义BodyReaderFilter解决读取body错误问题
BodyReaderWrapper
package com.yolo.springboot.kaptcha.filter; import org.springframework.util.StreamUtils; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; /** * 自定义 BodyReaderWrapper * 问题原因:在controller中我们通过@RequestBody注解来获取前端传过来的json数据,这里已经使用了一次request来获取body中的值。再次通过request获取body中的值,就会报错 * 使用场景:通过request能获取到一次body中的值,有时候我们需要多次获取body中的值的需求,因此需要对流再次封装再次传递 */ public class BodyReaderWrapper extends HttpServletRequestWrapper { private byte[] body; public BodyReaderWrapper(HttpServletRequest request) throws IOException { super(request); //保存一份InputStream,将其转换为字节数组 body = StreamUtils.copyToByteArray(request.getInputStream()); } //转换成String public String getBodyString(){ return new String(body,StandardCharsets.UTF_8); } @Override public BufferedReader getReader() { return new BufferedReader(new InputStreamReader(getInputStream())); } //把保存好的InputStream,传下去 @Override public ServletInputStream getInputStream() { final ByteArrayInputStream bais = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public int read() { return bais.read(); } @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } }; } public void setInputStream(byte[] body) { this.body = body; } }
BodyReaderFilter
package com.yolo.springboot.kaptcha.filter; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import org.apache.commons.lang3.StringUtils; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * @ClassName RequestFilter * @Description 自定义BodyReaderFilter解决读取controller中使用@Requestbody重复读取流错误问题 * @Author hl * @Date 2022/12/6 15:44 * @Version 1.0 */ public class BodyReaderFilter implements Filter { private List<String> noFilterUrls; @Override public void init(FilterConfig filterConfig){ // 从过滤器配置中获取initParams参数 String noFilterUrl = filterConfig.getInitParameter("noFilterUrl"); // 将排除的URL放入成员变量noFilterUrls中 if (StringUtils.isNotBlank(noFilterUrl)) { noFilterUrls = new ArrayList<>(Arrays.asList(noFilterUrl.split(","))); } } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { ServletRequest requestWrapper = null; String requestURI = null; if (servletRequest instanceof HttpServletRequest) { //获取请求中的流如何,将取出来的字符串,再次转换成流,然后把它放入到新request对象中。 requestWrapper = new BodyReaderWrapper((HttpServletRequest) servletRequest); requestURI = ((HttpServletRequest) servletRequest).getRequestURI(); } //如果请求是需要排除的,直接放行,例如上传文件 if ((CollUtil.isNotEmpty(noFilterUrls) && StrUtil.isNotBlank(requestURI) && noFilterUrls.contains(requestURI)) || requestWrapper == null){ chain.doFilter(servletRequest, servletResponse); }else { // 在chain.doFiler方法中传递新的request对象 chain.doFilter(requestWrapper, servletResponse); } } @Override public void d编程estroy() { Filter.super.destroy(); } }
加入到配置中
这里需要注意,拦截的是所有请求,上传文件的时候需要排除,上传文件的路径
@Bean public FilterRegistrationBean<?> bodyReaderFilterRegistration() { FilterRegistrationBean<BodyReaderFilter> registration = new FilterRegistrationBean<>(); // 将过滤器配置到FilterRegistrationBean对象中 registration.setFilter(new BodyReaderFilter()); // 给过滤器取名 registration.setName("bodyReaderFilter"); // 设置过滤器优先级,该值越小越优先被执行 registration.setOrder(1); List<String> urlPatterns = new ArrayList<>(); //这里需要填写排除上传文件的接口 Map<String, String> paramMap = new HashMap<>(); paramMap.put("noFilterUrl", "/test"); // 设置initParams参数 registration.setInitParameters(paramMap); urlPatterns.add("/*"); // 设置urlPatterns参数 registration.setUrlPatterns(urlPatterns); return registration; }
测试成功:这里我原本用的form-data传参,然后一直获取到body为空,用这种方法是需要在raw中进行填写的
获取form表单的数据
//方式一:getParameterMap(),获得请求参数map Map<String,String[]> map= request.getParameterMap(); //key 参数名称 value:具体值 //方式二:getParameterNames():获取所有参数名称 Enumeration a = request.getParameterNames();
5、注意
自定义的过滤器不要交给spring管理,也就是说不要添加@Component注解,不然每一个请求都会进行过滤
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。
精彩评论