前言:今天跟安卓同学闹了点小不愉快,他在测试安卓TV端的时候,突然发现接口报错于是问我.但是我看到其报错接口并未改动,并且运行公众号H5端接口正常,于是产生了分歧;
出问题原因:为了记录详细日志,配置了全局过滤器记录JSON信息,于是继承HttpServletRequestWrapper重写了一些逻辑,下面说明具体细节;
1.我直接将过滤器的chain.doFilter(request), response)方法中的request继承HttpServletRequestWrapper重写了逻辑:
package com.wfcm.config.filter;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.util.LinkedHashMap;
import java.util.Map;
public class BodyHttpServletRequestWrapper extends HttpServletRequestWrapper {
private byte[] requestBody;
HttpServletRequest orgRequest;
private final static HTMLFilter htmlFilter = new HTMLFilter();
public BodyHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
orgRequest = request;
if (null == this.requestBody) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
IOUtils.copy(orgRequest.getInputStream(), baos);
this.requestBody = baos.toByteArray();
}
}
@Override
public String getParameter(String name) {
String value = super.getParameter(xssEncode(name));
if (StringUtils.isNotBlank(value)) {
value = xssEncode(value);
}
return StringEscapeUtils.unescapeHtml(value);
}
@Override
public String[] getParameterValues(String name) {
String[] parameters = super.getParameterValues(name);
if (parameters == null || parameters.length == 0) {
return null;
}
for (int i = 0; i < parameters.length; i++) {
parameters[i] = xssEncode(parameters[i]);
parameters[i] = StringEscapeUtils.unescapeHtml(parameters[i]);
}
return parameters;
}
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> map = new LinkedHashMap<>();
Map<String, String[]> parameters = super.getParameterMap();
for (String key : parameters.keySet()) {
String[] values = parameters.get(key);
for (int i = 0; i < values.length; i++) {
values[i] = xssEncode(values[i]);
values[i] = StringEscapeUtils.unescapeHtml(values[i]);
}
map.put(key, values);
}
return map;
}
@Override
public String getHeader(String name) {
String value = super.getHeader(xssEncode(name));
if (StringUtils.isNotBlank(value)) {
value = xssEncode(value);
}
return StringEscapeUtils.unescapeHtml(value);
}
private String xssEncode(String input) {
return htmlFilter.filter(input);
}
/**
* 获取最原始的request
*/
public HttpServletRequest getOrgRequest() {
return orgRequest;
}
/**
* 获取最原始的request
*/
public static HttpServletRequest getOrgRequest(HttpServletRequest request) {
if (request instanceof BodyHttpServletRequestWrapper) {
return ((BodyHttpServletRequestWrapper) request).getOrgRequest();
}
return request;
}
/**
* @return
* @throws IOException
*/
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
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() {
return bais.read();
}
};
}
public byte[] getRequestBody() {
return requestBody;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
可以看到,我将body中的字节流提取出来了,便于记录日志;这里是因为如果不重写逻辑,则字节流只能读取一次,不能达到记录日志的功能;
此时出现安卓端传参消失,公众号端没问题,打断点debug分析得出:
(1)安卓端,请求方法为POST,内容类型content-type 为application/x-www-form-urlencoded,
参数也是表单编码后拼接参数body体传过来的
(2)H5端,请求方法为POST,内容类型content-type 为application/json,参数为直接url后面拼接参数而成
(3)接口接收参数方式为普通表单接收
结论:由于重写的方法,取出字节流后,getParameter再次读取request是拿不到值的,所以接口接收不到安卓端参数;
由于H5端是拼接到url上的参数,所以根本没有在body体中,getParameter正常取值,所以接口接收参数成功;
坑就坑在,他们不同端的请求实现不一致啊~~~
最终解决方案:
根据具体请求,判断后,使用不同的HttpServletRequestWrapper逻辑;
1.filter:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse responseServlet = (HttpServletResponse) response;
responseServlet.setHeader("Access-Control-Allow-Origin", "*");
responseServlet.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS,PUT, DELETE");
responseServlet.setHeader("Access-Control-Max-Age", "3600");
responseServlet.addHeader("Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept,timestamp,token,sign,wx," + WfLoginConstant.TOKEN_SOURCE);
if (!(request instanceof HttpServletRequest)) {
chain.doFilter(request, responseServlet);
}
chain.doFilter(getRequest((HttpServletRequest) request), responseServlet);
}
private HttpServletRequest getRequest(HttpServletRequest httpServletRequest) throws IOException {
String method = httpServletRequest.getMethod().toUpperCase();
String contentType = httpServletRequest.getContentType();
XssHttpServletRequestWrapper xssWrapper = new XssHttpServletRequestWrapper(httpServletRequest);
if (!HttpMethod.POST.name().equals(method)) {
return xssWrapper;
}
if (contentType == null) {
return xssWrapper;
}
if (!contentType.equals(MediaType.APPLICATION_JSON_VALUE) &&
!contentType.equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
return xssWrapper;
}
return new BodyHttpServletRequestWrapper(httpServletRequest);
}
判断请求类型,如果是非Json请求的,则走正常的表单方式,否则获取字节流信息;
此处注意一点,如果写出这样,将出问题:
private HttpServletRequest getRequest(HttpServletRequest httpServletRequest) throws IOException {
String method = httpServletRequest.getMethod().toUpperCase();
String contentType = httpServletRequest.getContentType();
XssHttpServletRequestWrapper xssWrapper = new XssHttpServletRequestWrapper(httpServletRequest);
XssHttpServletRequestWrapper bodyWrapper = new BodyHttpServletRequestWrapper(httpServletRequest);
if (!HttpMethod.POST.name().equals(method)) {
return xssWrapper;
}
if (contentType == null) {
return xssWrapper;
}
if (!contentType.equals(MediaType.APPLICATION_JSON_VALUE) &&
!contentType.equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
return xssWrapper;
}
return bodyWrapper ;
同时创建HttpServletRequestWrapper后,正常在走Xss中时,getParameter获取值为null,但是当使用idea断点打在构造方法中,使用表达式动态取值能取到,然后后续也能取到;
2.不同HttpServletRequestWrapper
(1)提供获取字节流信息的
package com.wfcm.config.filter;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.util.LinkedHashMap;
import java.util.Map;
public class BodyHttpServletRequestWrapper extends HttpServletRequestWrapper {
private byte[] requestBody;
HttpServletRequest orgRequest;
private final static HTMLFilter htmlFilter = new HTMLFilter();
public BodyHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
orgRequest = request;
if (null == this.requestBody) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
IOUtils.copy(orgRequest.getInputStream(), baos);
this.requestBody = baos.toByteArray();
}
}
@Override
public String getParameter(String name) {
String value = super.getParameter(xssEncode(name));
if (StringUtils.isNotBlank(value)) {
value = xssEncode(value);
}
return StringEscapeUtils.unescapeHtml(value);
}
@Override
public String[] getParameterValues(String name) {
String[] parameters = super.getParameterValues(name);
if (parameters == null || parameters.length == 0) {
return null;
}
for (int i = 0; i < parameters.length; i++) {
parameters[i] = xssEncode(parameters[i]);
parameters[i] = StringEscapeUtils.unescapeHtml(parameters[i]);
}
return parameters;
}
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> map = new LinkedHashMap<>();
Map<String, String[]> parameters = super.getParameterMap();
for (String key : parameters.keySet()) {
String[] values = parameters.get(key);
for (int i = 0; i < values.length; i++) {
values[i] = xssEncode(values[i]);
values[i] = StringEscapeUtils.unescapeHtml(values[i]);
}
map.put(key, values);
}
return map;
}
@Override
public String getHeader(String name) {
String value = super.getHeader(xssEncode(name));
if (StringUtils.isNotBlank(value)) {
value = xssEncode(value);
}
return StringEscapeUtils.unescapeHtml(value);
}
private String xssEncode(String input) {
return htmlFilter.filter(input);
}
/**
* 获取最原始的request
*/
public HttpServletRequest getOrgRequest() {
return orgRequest;
}
/**
* 获取最原始的request
*/
public static HttpServletRequest getOrgRequest(HttpServletRequest request) {
if (request instanceof BodyHttpServletRequestWrapper) {
return ((BodyHttpServletRequestWrapper) request).getOrgRequest();
}
return request;
}
/**
* @return
* @throws IOException
*/
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
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() {
return bais.read();
}
};
}
public byte[] getRequestBody() {
return requestBody;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
(2)普通过滤参数的
package com.wfcm.config.filter;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
// 没被包装过的HttpServletRequest(特殊场景,需求自己过滤)
HttpServletRequest orgRequest;
// html过滤
private final static HTMLFilter htmlFilter = new HTMLFilter();
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
orgRequest = request;
}
@Override
public String getParameter(String name) {
String value = super.getParameter(xssEncode(name));
if (StringUtils.isNotBlank(value)) {
value =xssEncode(value);
}
//SQL注入检查
// value = SQLFilter.sqlInject(value);
return StringEscapeUtils.unescapeHtml(value);
}
@Override
public String[] getParameterValues(String name) {
String[] parameters = super.getParameterValues(name);
if (parameters == null || parameters.length == 0) {
return null;
}
for (int i = 0; i < parameters.length; i++) {
parameters[i] = xssEncode(parameters[i]);
//SQL注入检查
// parameters[i] = SQLFilter.sqlInject(parameters[i]);
parameters[i] = StringEscapeUtils.unescapeHtml(parameters[i]);
}
return parameters;
}
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> map = new LinkedHashMap<>();
Map<String, String[]> parameters = super.getParameterMap();
for (String key : parameters.keySet()) {
String[] values = parameters.get(key);
for (int i = 0; i < values.length; i++) {
values[i] = xssEncode(values[i]);
//SQL注入检查
// values[i] = SQLFilter.sqlInject(values[i]);
values[i] = StringEscapeUtils.unescapeHtml(values[i]);
}
map.put(key, values);
}
return map;
}
@Override
public String getHeader(String name) {
String value = super.getHeader(xssEncode(name));
if (StringUtils.isNotBlank(value)) {
value = xssEncode(value);
}
//SQL注入检查
// value = SQLFilter.sqlInject(value);
return StringEscapeUtils.unescapeHtml(value);
}
private String xssEncode(String input) {
return htmlFilter.filter(input);
}
/**
* 获取最原始的request
*/
public HttpServletRequest getOrgRequest() {
return orgRequest;
}
/**
* 获取最原始的request
*/
public static HttpServletRequest getOrgRequest(HttpServletRequest request) {
if (request instanceof XssHttpServletRequestWrapper) {
return ((XssHttpServletRequestWrapper) request).getOrgRequest();
}
return request;
}
@Override
public ServletInputStream getInputStream() throws IOException {
return super.getInputStream();
}
}
基础知识复习:
get请求带参数的话,提供了request.getParameter()方式获取;
post请求中,如果url后拼接的参数,依然可以使用request.getParameter()方式获取,不会与request.getInputStream()冲突;
application/x- www-form-urlencoded是Post请求默认的请求体内容类型,也是form表单默认的类型。Servlet API规范中对该类型的请求内容提供了request.getParameter()方法来获取请求参数值。但当请求内容不是该类型时,需要调用request.getInputStream()或request.getReader()方法来获取请求内容值。
当请求体内容(注意:get请求没有请求体)类型是application/x- www-form-urlencoded时也可以直接调用request.getInputStream()或request.getReader()方法获取到请求内容再解析出具体都参数,但前提是还没调用request.getParameter()方法。此时当request.getInputStream()或request.getReader()获取到请求内容后,无法再调request.getParameter()获取请求内容。即对该类型的请求,三个方法互斥,只能调其中一个。
注意:在一个请求链中,请求对象被前面对象方法中调用request.getInputStream()或request.getReader()获取过内容后,后面的对象方法里再调用这两个方法也无法获取到客户端请求的内容,但是调用request.getParameter()方法获取过内容后,后面的对象方法里依然可以调用它获取到参数的内容。
当请求体内容是其它类型时,比如 multipart/form-data或application/json时,无法通过request.getParameter()获取到请求内容,此时只能通过request.getInputStream()和request.getReader()方法获取请求内容,此时调用request.getParameter()也不会影响第一次调用request.getInputStream()或request.getReader()获取到请求内容。
request.getInputStream()返回请求内容字节流,多用于文件上传,request.getReader()是对前者返回内容的封装,可以让调用者更方便字符内容的处理(不用自己先获取字节流再做字符流的转换操作)。
所以就是如果为get请求则request.getParameter()无任何影响;
post请求则使用request.getParameter()和使用request.getInputStream()互斥,使用其中一个则另一个无法获得值,当然仅影响body体正文中的数据,url后面拼接的参数request.getParameter()无影响