前段时间修改了一个web项目,遇到了一个bug是这样的:浏览器的header中的编码,服务器tomcat的编码,服务器机器默认编码,以及服务器代码的编码,都是UTF-8。服务器也在web.xml中使用字符编码过滤器,设置了UTF-8编码来进行字符串编码过滤。但是服务端接收到的参数却乱码。

查了好就最后确定了问题的原因:web.xml中关于字符的编码的过滤器没有放到第一位,那么问题的原因到底是什么呢?我们先从下面的一个URL分析开始讲解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
http://guochenglai.com/测试?param=测试
```
可以看到这个URL参数中有两个地方出现了中文字符。一个是URI部分,有一个中文字符“测试”,一个是Parameter部分出现了中文字符“测试”。这个地方就会出现一个问题浏览器对URI和Parameter的编码是不一样的。URI的编码是你的程序中指定的如果JS指定为UTF-8这个编码就会是UTF-8编码。而Parameter的编码则是取决于用户电脑环境的默认编码设置,以及浏览器的设置(现在新版的浏览器基本都会采用UTF-8统一编码,)。那么服务器会怎样处理这个两个部分的字符的解码呢?
## Tomca对URI的解码规则
我们先看看Tomcat对URI的解码的源代码(我只摘抄了关于URL解码的源代码):
```java
public class CoyoteAdapter implements Adapter {
/**
* Character conversion of the URI.
*/
protected void convertURI(MessageBytes uri, Request request)
throws Exception {
ByteChunk bc = uri.getByteChunk();
int length = bc.getLength();
CharChunk cc = uri.getCharChunk();
cc.allocate(length, -1);
//先去拿配置conf/server.xml文件中connector属性中的编码的配置
String enc = connector.getURIEncoding();
if (enc != null) {
B2CConverter conv = request.getURIConverter();
try {
if (conv == null) {
conv = new B2CConverter(enc);
request.setURIConverter(conv);
}
} catch (IOException e) {
// Ignore
log.error("Invalid URI encoding; using HTTP default");
connector.setURIEncoding(null);
}
if (conv != null) {
try {
conv.convert(bc, cc, cc.getBuffer().length - cc.getEnd());
uri.setChars(cc.getBuffer(), cc.getStart(),
cc.getLength());
return;
} catch (IOException e) {
log.error("Invalid URI character encoding; trying ascii");
cc.recycle();
}
}
}
//如果配置文件没有配置URI的编码字符,则采用本系统默认的编码
byte[] bbuf = bc.getBuffer();
char[] cbuf = cc.getBuffer();
int start = bc.getStart();
for (int i = 0; i < length; i++) {
cbuf[i] = (char) (bbuf[i + start] & 0xff);
}
uri.setChars(cbuf, 0, length);
}
}

通过分析上面的可以看到Tomcat对URI的解码处理是通过两个步骤来完成的。第一步如果配置文件中配置了URI的解码,则按照配置文件的字符码解码,如果没有配置,则按照本系统默认的字符集进行解码我们这里服务器的默认字符集都是UTF-8。也就是说URI的这个部分如果客户端使用UTF-8编码,并且服务器在Connectors中也配置了UTF-8,那么这个是不会出现问题的。真正容易出现问题的是Parameter部分的解码。
附件:
tomcat root path –>conf–>server.xml配置文件中关于connectors的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<Connector port="8888" protocol="HTTP/1.1"
maxThreads="200" connectionTimeout="20000"
enableLookups="false" compression="on"
redirectPort="8443"
URIEncoding="UTF-8"
compressableMimeType="text/csv,text/html,text/xml,text/css,text/plain,text/javascript,application/javascript,application/x-javascript,application/json,application/xml"
/>

Tomcat对Parameter的解码规则

我们项目出现问题的原因也就是没有理解Tomcat对Parameter解码的规则。其实Tomcat对Parameter的解码规则是如下的:

  • 所有的Parameter中的参数都作为Parameter保存的
  • Parameter的解码操作发生在第一次调用时。首先获取Request.setEncoding中对参数的编码(这个有两个地方设置1是客户端设置,2是服务器使用过滤器设置),并且查看是否将这个编码规则适用于URI的编码。
  • 如果没有就采用Tomcat的默认编码IOS8859-1,至于为什么默认是这个编码,原因是HTTP规范和Servlet规范都是默认使用这个编码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    public class Request implements HttpServletRequest {
    /**
    * Parse request parameters.
    */
    protected void parseParameters() {
    //当调用这个方法解码参数时,就设置解码标志位true,这样以后就直接从缓存中拿解码参数
    parametersParsed = true;
    //先获取未被解码的参数
    Parameters parameters = coyoteRequest.getParameters();
    //1 获取参数的解码字符编码,这里是获取的就是request.setEncoding中设置的字符编码
    String enc = getCharacterEncoding();
    //下面就是判断request中是否设置了编码,如果设置了编码,就使用request的编码,如果没有设置编码,就是用Tomcat默认的编码ISO8859-1,注意不是别的。因为这个默认编码是继承自http1.0的默认编码规范
    boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
    if (enc != null) {
    //设置参数的编码为enc
    parameters.setEncoding(enc);
    //useBodyEncodingForURI也是connector中配置的属性,如果配置为True,这里就会设置URI的编码为enc,否则就使用connector中的URIEncoding设置的编码来解码URI
    if (useBodyEncodingForURI) {
    parameters.setQueryStringEncoding(enc);
    }
    } else {
    parameters.setEncoding
    (org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);
    if (useBodyEncodingForURI) {
    parameters.setQueryStringEncoding
    (org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);
    }
    }
    //按照上述获取的编码规则,来解析参数
    parameters.handleQueryParameters();
    if (usingInputStream || usingReader)
    return;
    if (!getMethod().equalsIgnoreCase("POST"))
    return;
    String contentType = getContentType();
    if (contentType == null)
    contentType = "";
    int semicolon = contentType.indexOf(';');
    if (semicolon >= 0) {
    contentType = contentType.substring(0, semicolon).trim();
    } else {
    contentType = contentType.trim();
    }
    if ("multipart/form-data".equals(contentType)) {
    parseParts();
    return;
    }
    if (!("application/x-www-form-urlencoded".equals(contentType)))
    return;
    int len = getContentLength();
    if (len > 0) {
    int maxPostSize = connector.getMaxPostSize();
    if ((maxPostSize > 0) && (len > maxPostSize)) {
    if (context.getLogger().isDebugEnabled()) {
    context.getLogger().debug(
    sm.getString("coyoteRequest.postTooLarge"));
    }
    return;
    }
    byte[] formData = null;
    if (len < CACHED_POST_LEN) {
    if (postData == null)
    postData = new byte[CACHED_POST_LEN];
    formData = postData;
    } else {
    formData = new byte[len];
    }
    try {
    if (readPostBody(formData, len) != len) {
    return;
    }
    } catch (IOException e) {
    // Client disconnect
    if (context.getLogger().isDebugEnabled()) {
    context.getLogger().debug(
    sm.getString("coyoteRequest.parseParameters"), e);
    }
    return;
    }
    parameters.processParameters(formData, 0, len);
    } else if ("chunked".equalsIgnoreCase(
    coyoteRequest.getHeader("transfer-encoding"))) {
    byte[] formData = null;
    try {
    formData = readChunkedPostBody();
    } catch (IOException e) {
    // Client disconnect
    if (context.getLogger().isDebugEnabled()) {
    context.getLogger().debug(
    sm.getString("coyoteRequest.parseParameters"), e);
    }
    return;
    }
    if (formData != null) {
    parameters.processParameters(formData, 0, formData.length);
    }
    }
    }
    }

通过分析Parameter的参数编码规则可以看到我们代码的问题。我们现在的代码都没有在客户端显示设置request.setEncoding=XXXX,采用的替代方案是在服务器的web.xml中采用过滤器,过滤每个请求设置这个参数。而如果没有把过滤器放在第一位。就会出现一个问题,会采用Tomcat的默认编码IOS8859-1,来解析参数。这也就是为什么我的代码里面所有地方都设置了UTF-8编码。最后也会出现乱码的原因。所以这里提醒大家一定要把web.xml中关于字符编码的过滤器放在第一位。