我们经常有这样一个场景,比如:在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的时候,请求参数不见了



可以看到,过滤器确实拿到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的请求参数后,我们可以前置做很多操作,比如常用的服务端接口签名验证,敏感接口防重复请求等等。

最新文章

  1. 关于HTML语义化的一些理解
  2. [ACM训练] 算法初级 之 搜索算法 之 广度优先算法BFS (POJ 3278+1426+3126+3087+3414)
  3. box-shadow
  4. 使用a标签删除进行提示
  5. PowerDesigner 工具面板 association,inheritance,association link 不可用 解决方法
  6. Mysql 如何删除数据表中的重复数据!
  7. javascript 的对象
  8. ABP+AdminLTE+Bootstrap Table权限管理系统第二节--数据库脚本
  9. OpenCMS模板的导出和OpenCMS网站的导出
  10. 【javascript小案例】从0开始实现一个俄罗斯方块
  11. eclipse 迁移项目 乱码
  12. Unity Shader Graph(一)初次尝试
  13. windows模糊查询指定进程是否存在
  14. SqlServer 连接远程服务器数据库 实现跨服务器联合查询
  15. oracle如何设置表空间autoextensible自动扩容
  16. BFS广搜题目(转载)
  17. Java 浅析 Thread.join()
  18. linux内核模块的安全
  19. 【Redis】命令学习笔记——列表(list)+集合(set)+有序集合(sorted set)(17+15+20个超全字典版)
  20. Caffe源代码中Solver文件分析

热门文章

  1. gin领域层:用户实体编写和值对象(初步)
  2. EasyExcel对大数据量表格操作导入导出
  3. 5种典型 API 攻击及预防建议
  4. java:绘制图形
  5. win7使用onedrive右键托盘图标中文不显示问题
  6. Windows机器下VSCode安装及使用CmakeLists编译工程demo
  7. 数电第一周总结_by_yc
  8. Java 中 String 与 StringBuffer 和 StringBuilder 的区别
  9. IdentityServer4的最佳使用
  10. python + mysql +djagno +unittest 实现WEB、APP UI自动化测试平台--------(一)基础表