一、getReader()问题分析
1、获取请求参数的方式
对于 GET 请求和 POST 表单请求,参数都是包含在 URL 查询字符串中的,因此在拦截器中都可以通过使用 request.getParameter(“paramName”)来获取这些参数。
对于 POST JSON 请求,参数通常包含在请求体中,并且请求的 Content-Type为application/json。在这种情况下,我们需要获取请求体数据。
下面是 GET 请求、POST 表单请求和 POST JSON 请求获取参数的方式:
- 获取 GET 请求和 POST 表单请求参数:
String paramName = request.getParameter("paramName");
- 获取 POST JSON 请求参数:
// 获取 POST JSON请求参数
String requestBody = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
注意:POST JSON 请求参数会提示:getReader() has already been called for this request。
这个问题一般出现在使用 ServletInputStream对象的时候。Servlet规范只允许读取请求体一次,如果我们在调用了 getReader()方法读取了请求体之后,又调用了 getInputStream()方法读取请求体,就会出现这个问题,因为 getReader()方法底层也是调用 getInputStream()来实现的。
即多次调用 request.getReader()方法时导致的。比如:拦截器和 Controller控制器(@RequestBody)中多次读取请求体中的数据。
2、getReader()解决方案
解决方案:尽量不要重复读取请求体。如果需要多次读取请求体,可以将请求体缓存起来。我们就需要使用 HttpServletRequestWrapper类来包装 HttpServletRequest对象的类。
具体实现:我们自定义一个 RepeatableHttpServletRequestWrapper类继承 HttpServletRequestWrapper类,把 body 保存在 RepeatableHttpServletRequestWrapper类中,并重写 getReader()和 getInputStream()方法,返回新的流对象。这样就可以绕过Servlet规范的限制,多次读取请求体。
二、解决 getReader()实战
为了在拦截器中获取请求体数据,同时又要保持控制器能够正常使用请求体或实体类接口。在这种情况下,我们自定义 RepeatableHttpServletRequestWrapper类来解决 getReader()这个问题。
1、自定义 RepeatableHttpServletRequestWrapper类
public class RepeatableHttpServletRequestWrapper extends HttpServletRequestWrapper {
private static final int BUFFER_SIZE = 1024 * 8;
private byte[] body;
public RepeatableHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
/**
* 如果请求是多部分请求,就直接返回,不进行后续的处理。
* 多部分请求包含了文件数据和其他表单字段数据。
*/
if (ServletFileUpload.isMultipartContent(request)) {
return;
}
BufferedReader reader = request.getReader();
try (StringWriter writer = new StringWriter()) {
int read;
char[] buf = new char[BUFFER_SIZE];
while ((read = reader.read(buf)) != -1) {
writer.write(buf, 0, read);
}
this.body = writer.getBuffer().toString().getBytes(StandardCharsets.UTF_8);
}
}
/**
* 获取请求体数据
*
* @return
*/
public String getBody() {
return new String(this.body, StandardCharsets.UTF_8);
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
}
}
ServletFileUpload.isMultipartContent(request)方法源码如下:
2、自定义过滤器
@Slf4j
public class RequestBodyWrapperFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ServletRequest myRequestWrapper = null;
if (servletRequest instanceof HttpServletRequest) {
myRequestWrapper = new RepeatableHttpServletRequestWrapper((HttpServletRequest) servletRequest);
}
if (myRequestWrapper == null) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
log.info("使用可重复读取请求体包装类");
filterChain.doFilter(myRequestWrapper, servletResponse);
}
}
}
3、自定义拦截器
/**
* HandlerInterceptorAdapter类在 Spring 5.3之后就过时了,推荐使用 HandlerInterceptor类
*/
@Slf4j
public class ApiSignInterceptor implements HandlerInterceptor {
/**
* timestamp过期时间,单位:毫秒
*/
private final static Long TIMESTAMP_EXPIRE_TIME = 1000 * 60 * 5L;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestMethod = request.getMethod();
log.info("ApiSignInterceptor -> preHandle 请求方式 = {},url = {}, ", requestMethod, request.getRequestURI());
JSONObject jsonObject = new JSONObject();
// 获取请求数据
if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod)) {
jsonObject = getSignRequestParameter(request);
} else if (HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) {
if (MediaType.APPLICATION_JSON_VALUE.equals(request.getContentType())) {
/**
* Filter包装处理之后,在拦截器中上面两种方式都可以,推荐使用方式2。
*/
//try {
// // 方式1
// String requestBody = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
// jsonObject = JSON.parseObject(requestBody);
//} catch (IOException e) {
// throw new ApiBaiduException("读取请求体中的数据流异常");
//}
// 方式2
RepeatableHttpServletRequestWrapper requestWrapper = (RepeatableHttpServletRequestWrapper) request;
String requestBody = requestWrapper.getBody();
jsonObject = JSON.parseObject(requestBody);
} else {
jsonObject = getSignRequestParameter(request);
}
}
// 验签
verifyRequestSign(jsonObject);
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("ApiSignInterceptor -> postHandle 请求方式 = {},url = {}, ", request.getMethod(), request.getRequestURI());
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
/**
* 校验请求签名
*
* @param jsonObject
*/
private void verifyRequestSign(JSONObject jsonObject) {
String appId = jsonObject.getString("appId");
Long timestamp = jsonObject.getLong("timestamp");
String sign = jsonObject.getString("sign");
Long now = System.currentTimeMillis();
if (StringUtils.isBlank(appId)) {
throw new ApiException("appId 不能为空");
}
if (timestamp == null) {
throw new ApiException("timestamp 不能为空");
}
//if ((now - timestamp) > TIMESTAMP_EXPIRE_TIME) {
// throw new ApiException("请求时间超过规定范围时间 5分钟");
//}
if (StringUtils.isBlank(sign)) {
throw new ApiException("sign 不能为空");
}
String generateSign = ApiSignUtils.generateSign(jsonObject, appSecret);
log.info("ApiSignInterceptor -> sign = {}, generateSign = {}", sign, generateSign);
if (!generateSign.equals(sign)) {
throw new ApiException("签名不匹配");
}
}
/**
* 获取签名请求参数
*
* @param request
* @return
*/
private JSONObject getSignRequestParameter(HttpServletRequest request) {
JSONObject jsonObject = new JSONObject();
String appId = request.getParameter("appId");
String timestamp = request.getParameter("timestamp");
String sign = request.getParameter("sign");
jsonObject.put("appId", appId);
jsonObject.put("timestamp", timestamp);
jsonObject.put("sign", sign);
return jsonObject;
}
}
拦截器使用方式:Filter包装处理之后,在拦截器中上面两种方式都可以,推荐使用方式2。
Controller Post json请求还是和之前一样,使用 @RequestBody注解获取请求体数据。
4、Web配置类
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 配置过滤器
*/
@Bean
public FilterRegistrationBean<RequestBodyWrapperFilter> addRequestBodyWrapperFilter() {
FilterRegistrationBean<RequestBodyWrapperFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(getRequestBodyWrapperFilter());
bean.addUrlPatterns("/*"); // 拦截所有的资源
//bean.addUrlPatterns(WebConstant.API + "/*"); // 拦截 API所有的资源
bean.setOrder(1);
bean.setAsyncSupported(true);
return bean;
}
/**
* 配置拦截器
*
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getDaemonLoginInterceptor())
.addPathPatterns(WebConstant.DAEMON + "/**");
registry.addInterceptor(getApiSignInterceptor())
.addPathPatterns(WebConstant.API + "/**");
}
/**
* 配置静态资源访问拦截
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
/**
* 配置 CORS跨域问题
*
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
/**
* allowedOrigins("*") // allowedOrigins 是用于显式列出允许的源
* allowedOriginPatterns("*") // allowedOriginPatterns 是用于基于模式来匹配允许的源。
* 使用 allowedOriginPatterns 可以更加灵活地配置允许的跨域访问,特别是在需要处理多个类似的源时会更加方便。
*/
.allowedOriginPatterns("*")
.allowedHeaders("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600);
}
@Bean
public RequestBodyWrapperFilter getRequestBodyWrapperFilter() {
return new RequestBodyWrapperFilter();
}
@Bean
public DaemonLoginInterceptor getDaemonLoginInterceptor() {
return new DaemonLoginInterceptor();
}
@Bean
public ApiSignInterceptor getApiSignInterceptor() {
return new ApiSignInterceptor();
}
}
正常访问 Controller 一切 ok。
– 求知若饥,虚心若愚。