springboot如何在拦截器中拦截post请求参数以及解决文件类型上传问题

  我们经常有这样一个场景,比如:在springboot拦截器中想截取post请求的body参数做一些中间处理,或者用到自定义注解,想拦截一些特定post请求的方法的参数,记录一些请求日志。

想到了使用拦截器来实现这个功能

  当请求来到过滤器时,会有一个Request参数,通过该参数就能获取到请求路径和请求参数,以及相关内容,但是getParameterMap()方法只能够获取到GET请求的参数,如果是POST方法传的JSON那就没法获取到,那如何获取呢,POST的请求是在请求体body中,而POST请求中的body参数是已流形式存在的

所以我们可以通过获取到输入流来获取body

val inputStream: ServletInputStream = httpRequest.getInputStream()
val reader = InputStreamReader(inputStream, StandardCharsets.UTF_8)
val bfReader = BufferedReader(reader)
val sb = StringBuilder()
var line: String?
while (bfReader.readLine().also { line = it } != null) {
    sb.append(line)
}
println(sb.toString())

  通过上面的方法,我们确实能在过滤器中获取到POST的JSON参数了,但是按照上面的方法实现的过滤器,我们会发现,当请求经过过滤器来到Controller的时候,请求参数不见了
202108281058227
可以看到,过滤器确实拿到JSON参数,但是接着报了一个request body missing的异常,也就是请求来到Controller时,参数没有了,这是为啥呢?

从源码分析我们可以看到

  SpringBoot也是通过获取request的输入流来获取参数,这样上面的疑问就能解开了,为什么经过过滤器来到Controller请求参数就没了,这是因为 InputStream read方法内部有一个,postion,标志当前流读取到的位置,每读取一次,位置就会移动一次,如果读到最后,InputStream.read方法会返回-1,标志已经读取完了,如果想再次读取,可以调用inputstream.reset方法,position就会移动到上次调用mark的位置,mark默认是0,所以就能从头再读了。但是呢 是否能reset又是由markSupported决定的,为true能reset,为false就不能reset,从源码可以看到,markSupported是为false的,而且一调用reset就是直接异常
  所以这也就代表,InputStream只能被读取一次,后面就读取不到了。因此我们在过滤器的时候,已经将InputStream读取过了一次,当来到Controller,SpringBoot读取InputStream的时候自然是什么都读取不到了
  既然InputStream只能读取一次,那我们可以把InputStream给保存下来,然后完整的传下去SpringBoot就可以读取到了,这里就需要用到HttpServletRequest的包装类HttpServletRequestWrapper了,该类可以自定义一些方法

自定义 HttpServletRequestWrapper

为了 重写 ServletInputStream 的 getInputStream()方法,我们需要自定义一个 HttpServletRequestWrapper

package com.qianxin.scm.interceptor

import org.springframework.util.StreamUtils
import java.io.BufferedReader
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
import javax.servlet.ReadListener
import javax.servlet.ServletInputStream
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletRequestWrapper


class RequestWrapper(request: HttpServletRequest) : HttpServletRequestWrapper(request) {

    private var body: ByteArray = StreamUtils.copyToByteArray(request.inputStream)

    //转换成String
    fun getBodyString(): String? {
        return String(body, StandardCharsets.UTF_8)
    }

    @Throws(IOException::class)
    override fun getReader(): BufferedReader? {
        return BufferedReader(InputStreamReader(inputStream))
    }

    //把保存好的InputStream,传下去
    @Throws(IOException::class)
    override fun getInputStream(): ServletInputStream? {
        val bais = ByteArrayInputStream(body)
        return object : ServletInputStream() {
            @Throws(IOException::class)
            override fun read(): Int {
                return bais.read()
            }

            override fun isFinished(): Boolean {
                return false
            }

            override fun isReady(): Boolean {
                return false
            }

            override fun setReadListener(readListener: ReadListener?) {}
        }
    }
}

然后定义一个 DispatcherServlet子类来分派 上面自定义的 RequestWrapper,这里需要特别注意,在这个派发类处理派发request的自定义包装类的时候,如果是普通的post请求不会有问题,但是如果一旦是上传文件类型之类的post请求,则这个request必须用StandardServletMultipartResolver类的方法包装,所以这里做了一个请求类型的判断,很多文章都没有考虑这种情况,会导致如果项目中有上传文件的接口,都会不可用,所以这里需要特别注意!注意!注意!

package com.qianxin.scm.interceptor

import org.springframework.web.multipart.support.StandardServletMultipartResolver
import org.springframework.web.servlet.DispatcherServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

class PostReqeustDispatcherServlet : DispatcherServlet() {
    override fun doDispatch(req: HttpServletRequest, resp: HttpServletResponse) {
        var request: HttpServletRequest = req
        val contentType: String? = req.contentType
        val method = "multipart/form-data"
        //如果是文件类型上传,则需要用这个request
        if (contentType != null && contentType.contains(method)) {
            // 将转化后的 request 放入过滤链中
            request = StandardServletMultipartResolver().resolveMultipart(request)
        }
        val requestWrapper = RequestWrapper(request)
        super.doDispatch(requestWrapper, resp)

    }
}

然后配置一下:

package com.qianxin.scm.interceptor

import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.DispatcherServlet
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer


@Configuration
class WebConfig : WebMvcConfigurer {

    @Bean
    fun getLoginInterceptor(): LoginInterceptor? {
        return LoginInterceptor()
    }

    override fun addInterceptors(registry: InterceptorRegistry) {

        registry.addInterceptor(getLoginInterceptor()!!)
            .addPathPatterns("/**")
            .excludePathPatterns("/static/**")
            .excludePathPatterns("/webjars/**")
            .excludePathPatterns("/swagger-ui/**")
            }

    @Bean
    @Qualifier(DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
    fun dispatcherServlet(): DispatcherServlet? {
        return PostReqeustDispatcherServlet()
    }
}

拦截器中读取post的body参数,可以用如下方法:

 val requestWrapper = RequestWrapper(request)
 val postData = requestWrapper.getBodyString()

总结一下

  如果你想对HTTP请求做些骚操作,那么前置获取HTTP请求参数是前提,为此文本给出了使用MVC拦截器获取参数的样例。
  在获取HTTP Body 的时候,出现了 Required request body is missing 的错误,同时拦截器还出现执行了两遍的问题,这是因为 ServletInputStream被读取了两遍导致的,tomcat截取到异常后就转发到 /error 页面 被拦截器拦截到了,拦截器也就执行了两遍。
为此我们通过自定义 HttpServletRequestWrapper 来包装一个可被重读读取的输入流,来达到期望的拦截效果。
  在获取到HTTP的请求参数后,我们可以前置做很多操作,比如常用的服务端接口签名验证,敏感接口防重复请求等等。

posted @   爱踢蓝月  阅读(3255)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示