• springboot:集成Kaptcha实现图片验证码


    springboot:集成Kaptcha实现图片验证码

    系统环境:

    windows 10

    jdk 1.8

    springboot版本: 2.1.10.RELEASE

    一、导入依赖

            <dependency>
                <groupId>com.github.pengglegroupId>
                <artifactId>kaptchaartifactId>
                <version>2.3.2version>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-data-redisartifactId>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-thymeleafartifactId>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    系统配置文件

    server:
      port: 81
    spring:
      redis:
        database: 1
        host: 127.0.0.1
        port: 6379
        password:      # 密码(默认为空)
        timeout: 6000ms  # 连接超时时长(毫秒)
        lettuce:
          pool:
            max-active: 1000  # 连接池最大连接数(使用负值表示没有限制)
            max-wait: -1ms      # 连接池最大阻塞等待时间(使用负值表示没有限制)
            max-idle: 10      # 连接池中的最大空闲连接
            min-idle: 5       # 连接池中的最小空闲连接
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    二、生成验证码

    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    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.core.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;
    
    /**
     * @ClassName 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88

    在这里插入图片描述

    三、校验验证码

    这里校验验证码,我用了过滤器来实现的,其中遇到了很多问题,下面有我详细的解决方法

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    然后需要把我们自定的过滤器加入到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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    这里我给他设置的拦截全部请求,并且优先级是第一位的

    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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122

    加入到配置中

    这里校验需要用到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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    遇到的问题及解决思路

    问题:流不能多次被调用

    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)
        at org.apache.catalina.connector.RequestFacade.getReader(RequestFacade.java:504)
    
    • 1
    • 2
    • 3
    • 4

    根据报错信息分析简单来说,就是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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    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 destroy() {
            Filter.super.destroy();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    加入到配置中

    这里需要注意,拦截的是所有请求,上传文件的时候需要排除,上传文件的路径

        @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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述

    测试成功:这里我原本用的form-data传参,然后一直获取到body为空,用这种方法是需要在raw中进行填写的

    获取form表单的数据

    		//方式一:getParameterMap(),获得请求参数map
            Map<String,String[]> map= request.getParameterMap();  //key 参数名称 value:具体值
    		//方式二:getParameterNames():获取所有参数名称
            Enumeration a = request.getParameterNames();
    
    • 1
    • 2
    • 3
    • 4

    5、注意

    自定义的过滤器不要交给spring管理,也就是说不要添加@Component注解,不然每一个请求都会进行过滤

  • 相关阅读:
    线性表的链式存储结构——链表
    【C++】用constexpr,constinit,consteval让程序跑的快一点
    【黑马-SpringCloud技术栈】【04】Nacos注册中心
    实例044:矩阵相加
    毕设准备---HelloServlet
    域渗透04-漏洞(CVE-2020-1472)
    01_从JDK源码级别剖析JVM类加载机制
    python3,设置只读属性
    聊聊分布式架构08——SpringBoot开启微服务时代
    QUIC为什么使用这样的connectionID?
  • 原文地址:https://blog.csdn.net/weixin_43296313/article/details/128207045