一.背景
用java重构PHP项目,因为以前接口设计不合理,各式各样的参数满天飞。为了逐步替代程java代码,并且不影响线上业务。第一步决定参数和接口方式不变,这也导致了出现这个问题。
二.问题出现
同一个接口:
go -> java和 客户端 -> java,Content-Type都是application/x-www-form-urlencoded,可是客户窜(安卓)对参数进行转码,然而唯一的%没有进行转码
比如 {"param":"%"} 正确转码为:%7B%22param%22%3A%22%25%22%7D,可是转换成了%7B%22param%22%3A%22%%22%7D
导致后台接收不到数据。
三.解决方案:
tomcat会出现一个日志错误(需要开启debug模式才能看到):org.apache.tomcat.util.http.Parameters:173 [http-nio-20977-exec-1] - Character decoding failed. Parameter
第一种:客户端(安卓)需要修改代码,将所有的数据进行转义,这个需要强制升级客户端(放弃)
第二种:修改java服务端tomcat源码
进入Parameters这个类去研究代码,最终发现processParameters这个方法中,%没后被解析,代码如下:
private void processParameters(byte bytes[], int start, int len, Charset charset) {
if(log.isDebugEnabled()) {
log.debug(sm.getString("parameters.bytes",
new String(bytes, start, len, DEFAULT_BODY_CHARSET)));
}
int decodeFailCount = 0;
int pos = start;
int end = start + len;
while(pos < end) {
int nameStart = pos;
int nameEnd = -1;
int valueStart = -1;
int valueEnd = -1;
boolean parsingName = true;
boolean decodeName = false;
boolean decodeValue = false;
boolean parameterComplete = false;
do {
switch(bytes[pos]) {
case '=':
if (parsingName) {
// Name finished. Value starts from next character
nameEnd = pos;
parsingName = false;
valueStart = ++pos;
} else {
// Equals character in value
pos++;
}
break;
case '&':
if (parsingName) {
// Name finished. No value.
nameEnd = pos;
} else {
// Value finished
valueEnd = pos;
}
parameterComplete = true;
pos++;
break;
case '%':
case '+':
// Decoding required
if (parsingName) {
decodeName = true;
} else {
decodeValue = true;
}
pos ++;
break;
default:
pos ++;
break;
}
} while (!parameterComplete && pos < end);
if (pos == end) {
if (nameEnd == -1) {
nameEnd = pos;
} else if (valueStart > -1 && valueEnd == -1){
valueEnd = pos;
}
}
if (log.isDebugEnabled() && valueStart == -1) {
log.debug(sm.getString("parameters.noequal",
Integer.valueOf(nameStart), Integer.valueOf(nameEnd),
new String(bytes, nameStart, nameEnd-nameStart, DEFAULT_BODY_CHARSET)));
}
if (nameEnd <= nameStart ) {
if (valueStart == -1) {
// &&
if (log.isDebugEnabled()) {
log.debug(sm.getString("parameters.emptyChunk"));
}
// Do not flag as error
continue;
}
// &=foo&
UserDataHelper.Mode logMode = userDataLog.getNextMode();
if (logMode != null) {
String extract;
if (valueEnd > nameStart) {
extract = new String(bytes, nameStart, valueEnd - nameStart,
DEFAULT_BODY_CHARSET);
} else {
extract = "";
}
String message = sm.getString("parameters.invalidChunk",
Integer.valueOf(nameStart),
Integer.valueOf(valueEnd), extract);
switch (logMode) {
case INFO_THEN_DEBUG:
message += sm.getString("parameters.fallToDebug");
//$FALL-THROUGH$
case INFO:
log.info(message);
break;
case DEBUG:
log.debug(message);
}
}
setParseFailedReason(FailReason.NO_NAME);
continue;
// invalid chunk - it's better to ignore
}
tmpName.setBytes(bytes, nameStart, nameEnd - nameStart);
if (valueStart >= 0) {
tmpValue.setBytes(bytes, valueStart, valueEnd - valueStart);
} else {
tmpValue.setBytes(bytes, 0, 0);
}
// Take copies as if anything goes wrong originals will be
// corrupted. This means original values can be logged.
// For performance - only done for debug
if (log.isDebugEnabled()) {
try {
origName.append(bytes, nameStart, nameEnd - nameStart);
if (valueStart >= 0) {
origValue.append(bytes, valueStart, valueEnd - valueStart);
} else {
origValue.append(bytes, 0, 0);
}
} catch (IOException ioe) {
// Should never happen...
log.error(sm.getString("parameters.copyFail"), ioe);
}
}
try {
String name;
String value;
if (decodeName) {
urlDecode(tmpName);
}
tmpName.setCharset(charset);
name = tmpName.toString();
if (valueStart >= 0) {
if (decodeValue) {
urlDecode(tmpValue);
}
tmpValue.setCharset(charset);
value = tmpValue.toString();
} else {
value = "";
}
try {
addParameter(name, value);
} catch (IllegalStateException ise) {
// Hitting limit stops processing further params but does
// not cause request to fail.
UserDataHelper.Mode logMode = maxParamCountLog.getNextMode();
if (logMode != null) {
String message = ise.getMessage();
switch (logMode) {
case INFO_THEN_DEBUG:
message += sm.getString(
"parameters.maxCountFail.fallToDebug");
//$FALL-THROUGH$
case INFO:
log.info(message);
break;
case DEBUG:
log.debug(message);
}
}
break;
}
} catch (IOException e) {
setParseFailedReason(FailReason.URL_DECODING);
decodeFailCount++;
if (decodeFailCount == 1 || log.isDebugEnabled()) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("parameters.decodeFail.debug",
origName.toString(), origValue.toString()), e);
} else if (log.isInfoEnabled()) {
UserDataHelper.Mode logMode = userDataLog.getNextMode();
if (logMode != null) {
String message = sm.getString(
"parameters.decodeFail.info",
tmpName.toString(), tmpValue.toString());
switch (logMode) {
case INFO_THEN_DEBUG:
message += sm.getString("parameters.fallToDebug");
//$FALL-THROUGH$
case INFO:
log.info(message);
break;
case DEBUG:
log.debug(message);
}
}
}
}
}
tmpName.recycle();
tmpValue.recycle();
// Only recycle copies if we used them
if (log.isDebugEnabled()) {
origName.recycle();
origValue.recycle();
}
}
if (decodeFailCount > 1 && !log.isDebugEnabled()) {
UserDataHelper.Mode logMode = userDataLog.getNextMode();
if (logMode != null) {
String message = sm.getString(
"parameters.multipleDecodingFail",
Integer.valueOf(decodeFailCount));
switch (logMode) {
case INFO_THEN_DEBUG:
message += sm.getString("parameters.fallToDebug");
//$FALL-THROUGH$
case INFO:
log.info(message);
break;
case DEBUG:
log.debug(message);
}
}
}
}
追溯byte数组的来源是在Request类中,发现byte数组最终是由 postData过来的,所以先对postData进行转义替换:
if(null != postData){
String param = new String(postData);
if(param.contains("%%")){
param = param.replace("%%","%25%");
postData = param.getBytes();
len += 2;
}
}
在Request类的3212行添加这段代码,运行发现,还是不能解决问题。因为第一次请求是,postData为空,就不会进入这个代码,formData发现postData为空,就会从body中取数据,所以还是未解决。
研究代码发现只能改变formData的数据:
if(null != formData){
String param = new String(formData);
if(param.contains("%%")){
param = param.replace("%%","%25%");
formData = param.getBytes();
len += 2;
}
}
完美解决!!!
如果发现文章的问题,请留言,我会第一时间回复,谢谢