如何正确解码传递给servlet的Unicode参数

36

假设我有以下内容:

<a href="http://www.yahoo.com/" target="_yahoo" 
    title="Yahoo!&#8482;" onclick="return gateway(this);">Yahoo!</a>
<script type="text/javascript">
function gateway(lnk) {
    window.open(SERVLET +
        '?external_link=' + encodeURIComponent(lnk.href) +
        '&external_target=' + encodeURIComponent(lnk.target) +
        '&external_title=' + encodeURIComponent(lnk.title));
    return false;
}
</script>

我确认external_title被编码为Yahoo!%E2%84%A2并传递到SERVLET。如果在SERVLET中执行以下操作:

Writer writer = response.getWriter();
writer.write(request.getParameter("external_title"));

我在浏览器中看到了Yahoo!â„¢。如果我手动将浏览器字符编码切换为UTF-8,它就会变成Yahoo!TM(这就是我想要的)。

所以我认为我发送给浏览器的编码是错误的(它是Content-type: text/html; charset=ISO-8859-1)。我将SERVLET更改为:

response.setContentType("text/html; charset=utf-8");
Writer writer = response.getWriter();
writer.write(request.getParameter("external_title"));

现在浏览器字符编码为UTF-8,但输出内容为Yahoo!â¢,我无法让浏览器正确渲染出正确的字符。

我的问题是:是否有某种组合的Content-type和/或new String(request.getParameter("external_title").getBytes(), "UTF-8");以及/或其他方法可以使得Yahoo!TM出现在SERVLET输出中?

8个回答

46

你已经接近成功了。正确使用EncodeURIComponent会将内容正确编码为UTF-8格式,这是在今天的URL中应该始终使用的。

问题在于提交的查询字符串在进入服务器端脚本时被破坏了,因为getParameter()方法使用的是ISO-8559-1而不是UTF-8。这源于互联网在采用URI/IRI时还没有统一采用UTF-8编码,但Servlet规范尚未更新以匹配现实情况,或者至少提供一个可靠且受支持的选项。

(在Servlet 2.3中有request.setCharacterEncoding方法,但它不影响查询字符串解析,并且如果某个其他框架元素已经读取了单个参数,则它根本不起作用。)

因此,您需要使用特定于容器的方法来获取正确的UTF-8,通常涉及到server.xml中的一些内容。这对于分发应该在任何地方都可以运行的Web应用程序来说非常不方便。对于Tomcat,请参见https://cwiki.apache.org/confluence/display/TOMCAT/Character+EncodingWhat's the difference between "URIEncoding" of Tomcat, Encoding Filter and request.setCharacterEncoding


5
谢谢解释。至少我知道我不是疯了。在寻找解决方案的过程中,我尝试使用了request.setCharacterEncoding(),但像你所说的那样,它似乎并没有帮助我解决问题。 - Grant Wagner
这里提供Jetty的链接,如果有人在使用它(默认情况下,Jetty 6+使用UTF-8,除非另行配置):http://docs.codehaus.org/display/JETTY/International+Characters+and+Character+Encodings - Riyad Kalla
1
request.getParameter("name") 打印为 ÏηγÏÏÏÏηrequest.getQueryString() 打印为 name=%CF%84%CE%B7%CE%B3%CF%81%CF%84%CF%83%CF%82%CE%B7 - 如果传递给 URLDecoder.decode(),则可以正常解码。请问 _为什么 getParameter() 不返回百分号编码的字符串_?ISO-8559-1 不是 ASCII 的超集吗? - Mr_and_Mrs_D
2
getParameter旨在为您处理解码输入 - 浏览器在提交表单值时使用百分比对其进行编码,因此您必须对其进行解码以获取用户的输入。 必须使用某种编码将输入中的字节转换为字符,并且浏览器不总是使用相同的编码。 不幸的是,Servlet会为您选择一个编码,它的选择并不好,并且不允许您覆盖该选择 - 与URLDecoder.decode不同,没有“enc”参数。 - bobince
2
如果您想从原始URL获取百分比编码的内容,请使用 getQueryString() 并自行解析,而不是让Servlet来处理。 - bobince

21

我遇到了同样的问题,通过使用URLDecoder()解码Request.getQueryString()并提取参数来解决它。

String[] Parameters = URLDecoder.decode(Request.getQueryString(), 'UTF-8')
                       .splitat('&');

4
自己处理查询字符串是解决 "getParameter" 中出现问题的好主意,但这并不完全正确:应该在分离组件后进行 URL 解码,而不是之前。上面的代码会失败,如果参数中使用了 "&" 字符(编码为 "%26"),或者参数名称中使用了 "="("%3D")字符。 - bobince
1
POST参数怎么样? - lmo
请参考 https://dev59.com/FG855IYBdhLWcg3w0H53 获取多个手动解码示例。 - Vadzim

18

有一种在Java中完成此操作的方法(不需要与server.xml文件进行操作)。

无效:

protected static final String CHARSET_FOR_URL_ENCODING = "UTF-8";

String uname = request.getParameter("name");
System.out.println(uname);
// ÏηγÏÏÏÏη
uname = request.getQueryString();
System.out.println(uname);
// name=%CF%84%CE%B7%CE%B3%CF%81%CF%84%CF%83%CF%82%CE%B7
uname = URLDecoder.decode(request.getParameter("name"),
        CHARSET_FOR_URL_ENCODING);
System.out.println(uname);
// ÏηγÏÏÏÏη // !!!!!!!!!!!!!!!!!!!!!!!!!!!
uname = URLDecoder.decode(
        "name=%CF%84%CE%B7%CE%B3%CF%81%CF%84%CF%83%CF%82%CE%B7",
        CHARSET_FOR_URL_ENCODING);
System.out.println("query string decoded : " + uname);
// query string decoded : name=τηγρτσςη
uname = URLDecoder.decode(new String(request.getParameter("name")
        .getBytes()), CHARSET_FOR_URL_ENCODING);
System.out.println(uname);
// ÏηγÏÏÏÏη // !!!!!!!!!!!!!!!!!!!!!!!!!!!

Works :

final String name = URLDecoder
        .decode(new String(request.getParameter("name").getBytes(
                "iso-8859-1")), CHARSET_FOR_URL_ENCODING);
System.out.println(name);
// τηγρτσςη

虽然可以工作但如果默认编码不是utf-8就会出问题 - 请尝试使用这个方法(省略调用decode()的部分,因为它不是必需的):

final String name = new String(request.getParameter("name").getBytes("iso-8859-1"),
        CHARSET_FOR_URL_ENCODING);

如我上面所说,如果server.xml文件被篡改了,例如:


<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1"
                     redirectPort="8443"  URIEncoding="UTF-8"/> 

注意上面的URIEncoding="UTF-8",上述代码会出错(因为getBytes("iso-8859-1")应该改为getBytes("UTF-8"))。因此,为了确保解决方案的完全可靠,您需要获取URIEncoding属性的值。不幸的是,这似乎是容器特定的,甚至更糟糕的是,容器版本特定的。对于Tomcat 7,您需要使用以下内容:

import javax.management.AttributeNotFoundException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanException;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.ReflectionException;

import org.apache.catalina.Server;
import org.apache.catalina.Service;
import org.apache.catalina.connector.Connector;

public class Controller extends HttpServlet {

    // ...
    static String CHARSET_FOR_URI_ENCODING; // the `URIEncoding` attribute
    static {
        MBeanServer mBeanServer = MBeanServerFactory.findMBeanServer(null).get(
            0);
        ObjectName name = null;
        try {
            name = new ObjectName("Catalina", "type", "Server");
        } catch (MalformedObjectNameException e1) {
            e1.printStackTrace();
        }
        Server server = null;
        try {
            server = (Server) mBeanServer.getAttribute(name, "managedResource");
        } catch (AttributeNotFoundException | InstanceNotFoundException
                | MBeanException | ReflectionException e) {
            e.printStackTrace();
        }
        Service[] services = server.findServices();
        for (Service service : services) {
            for (Connector connector : service.findConnectors()) {
                System.out.println(connector);
                String uriEncoding = connector.getURIEncoding();
                System.out.println("URIEncoding : " + uriEncoding);
                boolean use = connector.getUseBodyEncodingForURI();
                // TODO : if(use && connector.get uri enc...)
                CHARSET_FOR_URI_ENCODING = uriEncoding;
                // ProtocolHandler protocolHandler = connector
                // .getProtocolHandler();
                // if (protocolHandler instanceof Http11Protocol
                // || protocolHandler instanceof Http11AprProtocol
                // || protocolHandler instanceof Http11NioProtocol) {
                // int serverPort = connector.getPort();
                // System.out.println("HTTP Port: " + connector.getPort());
                // }
            }
        }
    }
}

你仍然需要调整这些内容以适应多个连接器(请查看已注释的部分)。然后你可以使用类似以下的方式:

new String(parameter.getBytes(CHARSET_FOR_URI_ENCODING), CHARSET_FOR_URL_ENCODING);

如果使用 CHARSET_FOR_URI_ENCODING 对 parameter = request.getParameter("name"); 进行解码时出现损坏,那么 getBytes() 获得的字节就不是原始字节,这样可能导致操作失败(如果我理解的没错的话)。因此默认情况下会使用 "iso-8859-1" 编码 - 它将保留字节。你可以按以下方式手动解析查询字符串来消除所有这些问题:

URLDecoder.decode(request.getQueryString().split("=")[1],
        CHARSET_FOR_URL_ENCODING);

我仍在查找文档中提到的request.getParameter("name")调用URLDecoder.decode()而不是返回%CF%84%CE%B7%CE%B3%CF%81%CF%84%CF%83%CF%82%CE%B7字符串的位置。非常感谢源代码中的链接。
另外,我如何将字符串%CE作为参数的值传递?=>请参见注释:parameter=%25CE


1
如果您想传递 %CE,只需对其进行编码,因此 parameter=%25CE - Bart van Heukelom
是的,我更喜欢尽可能不改变平台配置。我会在我的自定义servlet配置(tomcat/conf中的自定义属性)中输入ISO-Charset,这样我就可以在运行时更改它,甚至在新的服务器部署中进行调整 - 如果需要的话。规范应始终优先于定制。 - Gunnar

2
我怀疑数据篡改发生在请求中,即请求声明的编码与实际用于数据的编码不匹配。 request.getCharacterEncoding() 返回什么?
我不太清楚JavaScript如何处理编码或如何使其使用特定的编码。
您需要确保在所有阶段正确使用编码 - 不要尝试在已经错误编码的情况下使用 new String()getBytes() 来“修复”数据。 编辑: 将原始页面(带有Javascript的页面)也编码为UTF-8并在其Content-Type中声明为UTF-8可能会有所帮助。然后我认为JavaScript可能会默认使用UTF-8进行请求 - 但这不是确定的知识,只是猜测。

request.getCharacterEncoding() 返回的是 ISO-8859-1。因此,我认为问题在于 encodeURIComponent() 将值编码为 UTF-8,但它被请求编码 ISO-8859-1 弄乱了。 - Grant Wagner

0

在某些版本的Jetty中存在一个错误,使其无法正确解析高位UTF-8字符。如果您的服务器可以正确接受阿拉伯字母但无法接受表情符号,则说明您使用的版本存在此问题,因为阿拉伯字母不在ISO-8859-1中,但在UTF-8字符的较低范围内(“较低”意味着Java将其表示为单个字符)。

我从版本7.2.0.v20101020升级到版本7.5.4.v20111024,这解决了问题;现在我可以使用getParameter(String)方法而不必自己解析它。

如果您真的很好奇,可以深入研究org.eclipse.jetty.util.Utf8StringBuilder.append(byte)的版本,并查看它是否在utf-8代码足够高时正确地向字符串添加多个字符,或者像7.2.0一样,只是将int强制转换为char并附加。


0

感谢您让我了解有关Tomcat、Jetty中使用的默认字符集的编码解码的知识,我使用这种方法来解决我的问题,使用了Google Guava。

        String str = URLDecoder.decode(request.getQueryString(), StandardCharsets.UTF_8.name());
        final Map<String, String> map = Splitter.on('&').trimResults().withKeyValueSeparator("=").split(str);
        System.out.println(map);
        System.out.println(map.get("aung"));
        System.out.println(map.get("aa"));

0

您可以使用JavaScript进一步操作文本。

<div id="test">a</div>
<script>
var a = document.getElementById('test');
alert(a.innerHTML);
a.innerHTML = decodeURI("Yahoo!%E2%84%A2");
alert(a.innerHTML);
</script>

是的,decodeURIComponent() 返回正确的值,但只有在 JavaScript 中从 URL 中提取值时才会返回。如果我尝试解码 decodeURIComponent('<%= request.getParameter("external_title") %>'); 我得不到正确的值。 - Grant Wagner

0

我认为我可以让以下内容正常工作:

encodeURIComponent(escape(lnk.title))

这给了我%25u2122(对于&#8482)或%25AE(对于&#174),在servlet中将解码为%u2122%AE

然后,我应该能够相对容易地将%u2122转换为'\u2122',将%AE转换为'\u00AE',使用正则表达式的匹配和替换循环中的(char)(%uXXXX或%XX的十进制整数值)

即 - 匹配/%u([0-9a-f]{4})/i,提取匹配的子表达式,将其转换为十进制,将其转换为字符并附加到输出,然后对/%([0-9a-f]{2})/i执行相同操作。


这是一种可能的编码方案,您可以使用它来解决Servlet参数字符集问题。(不使用有问题的JavaScript escape()函数的方案可能更好。)但是,任何这样的方案都不是传递参数的标准方式,因此任何其他脚本/表单都无法与其通信。 - bobince
1
我同意使用escape()不是最佳选择,但我宁愿不在JavaScript中编写自己的编码程序。我已经在IE6、7和8、Firefox 2和3、Opera 9.6、Safari for Windows 3.2.1和Google Chrome中测试了我的设计,对于这些浏览器来说它都可以稳定工作。 - Grant Wagner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接