如何确定一个字符串是否包含无效的编码字符

35

使用场景

我们实现了一个 web 服务,我们的网站前端开发人员通过 php api 内部使用它来显示产品数据。在网站上,用户输入某些内容(即查询字符串)。在内部,网站通过 api 调用服务来执行操作。

注意:我们使用 restlet,而不是 tomcat

原始问题

Firefox 3.0.10 看起来会遵循浏览器中选择的编码格式并根据所选的编码格式对 url 进行编码。这会导致 ISO-8859-1 和 UTF-8 的查询字符串不同。

我们的网站将用户输入转发到服务,但不进行转换(应该转换),因此可能通过调用包含德语元音字母的查询字符串的 api 来调用 web 服务。

例如,查询部分可能如下所示:

    ...v=abcädef

如果选择了“ISO-8859-1”,则发送的查询部分会如下所示:

...v=abc%E4def

但如果选择了“UTF-8”,发送的查询部分看起来就像这样:

...v=abc%C3%A4def

期望解决方案

由于我们控制这个服务,因为我们实现了它,我们希望在服务器端检查调用是否包含非utf-8字符,如果是,则以4xx http状态响应。

当前详细解决方案

检查每个字符(== string.substring(i,i + 1))

  1. 如果character.getBytes()[0]等于'?'的63
  2. 如果Character.getType(character.charAt(0))返回OTHER_SYMBOL

代码

protected List< String > getNonUnicodeCharacters( String s ) {
  final List< String > result = new ArrayList< String >();
  for ( int i = 0 , n = s.length() ; i < n ; i++ ) {
    final String character = s.substring( i , i + 1 );
    final boolean isOtherSymbol = 
      ( int ) Character.OTHER_SYMBOL
       == Character.getType( character.charAt( 0 ) );
    final boolean isNonUnicode = isOtherSymbol 
      && character.getBytes()[ 0 ] == ( byte ) 63;
    if ( isNonUnicode )
      result.add( character );
  }
  return result;
}

问题

这个代码能够捕获所有无效(非utf编码)字符吗?你们中是否有更好(更简单)的解决方案?

注意:我已经使用以下代码检查了URLDecoder

final String[] test = new String[]{
  "v=abc%E4def",
  "v=abc%C3%A4def"
};
for ( int i = 0 , n = test.length ; i < n ; i++ ) {
    System.out.println( java.net.URLDecoder.decode(test[i],"UTF-8") );
    System.out.println( java.net.URLDecoder.decode(test[i],"ISO-8859-1") );
}

这将打印:

v=abc?def
v=abcädef
v=abcädef
v=abcädef

没有 抛出 IllegalArgumentException 异常 叹气

10个回答

35

我曾经问过同样的问题:

如何处理Tomcat上URI中的字符编码

最近我找到了解决方法,它对我非常有效。你也可以试试。以下是你需要做的:

  1. 将URI编码保留为Latin-1。在Tomcat中,在server.xml的Connector中添加URIEncoding="ISO-8859-1"。
  2. 如果必须手动URL解码,请使用Latin1作为字符集。
  3. 使用fixEncoding()函数来修复编码问题。

例如,要从查询字符串获取参数:

  String name = fixEncoding(request.getParameter("name"));
你始终可以这样做。具有正确编码的字符串不会被更改。 代码已附上。祝好运!
 public static String fixEncoding(String latin1) {
  try {
   byte[] bytes = latin1.getBytes("ISO-8859-1");
   if (!validUTF8(bytes))
    return latin1;   
   return new String(bytes, "UTF-8");  
  } catch (UnsupportedEncodingException e) {
   // Impossible, throw unchecked
   throw new IllegalStateException("No Latin1 or UTF-8: " + e.getMessage());
  }

 }

 public static boolean validUTF8(byte[] input) {
  int i = 0;
  // Check for BOM
  if (input.length >= 3 && (input[0] & 0xFF) == 0xEF
    && (input[1] & 0xFF) == 0xBB & (input[2] & 0xFF) == 0xBF) {
   i = 3;
  }

  int end;
  for (int j = input.length; i < j; ++i) {
   int octet = input[i];
   if ((octet & 0x80) == 0) {
    continue; // ASCII
   }

   // Check for UTF-8 leading byte
   if ((octet & 0xE0) == 0xC0) {
    end = i + 1;
   } else if ((octet & 0xF0) == 0xE0) {
    end = i + 2;
   } else if ((octet & 0xF8) == 0xF0) {
    end = i + 3;
   } else {
    // Java only supports BMP so 3 is max
    return false;
   }

   while (i < end) {
    i++;
    octet = input[i];
    if ((octet & 0xC0) != 0x80) {
     // Not a valid trailing byte
     return false;
    }
   }
  }
  return true;
 }

编辑:你的方法因为各种原因不可行。当出现编码错误时,你不能依靠从Tomcat获取的内容。有时你会得到�或者?。其他时候,你将得不到任何东西,getParameter()将返回null。假设你可以检查“?”的存在,如果你的查询字符串包含有效的“?”会发生什么?

另外,你不应该拒绝任何请求。这不是你用户的错。正如我在最初的问题中提到的那样,浏览器可能会使用UTF-8或Latin-1对URL进行编码。用户无法控制。你需要接受两者。将你的servlet更改为Latin-1将保留所有字符,即使它们是错误的,也给我们一个机会来修复它或将其丢弃。

我在这里发布的解决方案并不完美,但目前为止是我们找到的最好的办法。


不错!但我必须反对你的评论“Java仅支持BMP”。UTF-8字节序列上的四字节限制是由Unicode联盟强制实施的,它足以处理完整的字符范围(U+0000..U+10FFFF),而不仅仅是BMP。 - Alan Moore
正确的注释可能应该是“我们只关心BMP”。我的印象是代理对在Java中不太好用。 - ZZ Coder
好的,我五月份就问过了;-) 无论如何,上面的代码是做什么的?它将ISO转换为UTF-8吗?我不想转换代码,只想检查编码是否正确,如果不正确则抛出错误。请再次查看我的解决方案,并检查它是否正确,好吗? - Daniel Hiller
你的解决方案行不通。如果使用错误的编码,你会得到问号,而不是异常。只需使用我的函数validUTF8()。如果它返回true,则最有可能是UTF8。否则,它就是Latin-1。你必须在服务器的所有地方使用Latin-1编码才能使此检查起作用。 - ZZ Coder
@ZZ Coder:你能否在你的代码中添加一些注释,以帮助我理解你正在做什么? - Daniel Hiller
显示剩余3条评论

15

您可以使用配置为在发现无效字符时抛出异常的CharsetDecoder:

 CharsetDecoder UTF8Decoder =
      Charset.forName("UTF8").newDecoder().onMalformedInput(CodingErrorAction.REPORT);

请参见CodingErrorAction.REPORT


1
我尝试了从UTF-8到ISO-8859-1再到JISAutoDetect的转换方法,但遗憾的是似乎没有抛出异常。(虽然对于UTF-8失败,我只是测试了mString.indexOf('\ufffd') != -1) - Houtman
2
我也添加了 .onUnmappableCharacter(CodingErrorAction.REPORT),现在似乎会为无效编码抛出异常。 - Kevin King
我本来希望能够得到CharacterCodingException,但在我的情况下(CSV文件,将UCS-2 BE BOM读取为UTF8),没有出现任何错误,但文件被读取为一个字符文件。而将UTF-8-BOM作为UTF-8读取也没有被标记为错误,但内容仍然是乱码的。因此,这种技术很好并且可以捕获一些编码错误,但它并不是万无一失的。 - Bampfer

6

这是我用来检查编码的方法:

CharsetDecoder ebcdicDecoder = Charset.forName("IBM1047").newDecoder();
ebcdicDecoder.onMalformedInput(CodingErrorAction.REPORT);
ebcdicDecoder.onUnmappableCharacter(CodingErrorAction.REPORT);

CharBuffer out = CharBuffer.wrap(new char[3200]);
CoderResult result = ebcdicDecoder.decode(ByteBuffer.wrap(bytes), out, true);
if (result.isError() || result.isOverflow() ||
    result.isUnderflow() || result.isMalformed() ||
    result.isUnmappable())
{
    System.out.println("Cannot decode EBCDIC");
}
else
{
    CoderResult result = ebcdicDecoder.flush(out);
    if (result.isOverflow())
       System.out.println("Cannot decode EBCDIC");
    if (result.isUnderflow())
        System.out.println("Ebcdic decoded succefully ");
}

编辑:已根据Vouze的建议更新


有趣,我会看一下的! - Daniel Hiller
如果您的数据是以字节数组而不是字符串的形式存在,则此方法可行。当然,这也是您测试有效性的唯一方式,所以它非常完美。 - james.garriss
警告:当没有发现错误时,您应该调用decoder.flush()和result.isUnderflow()为true。 - Vouze

5

将所有控制字符替换为空字符串

value = value.replaceAll("\\p{Cntrl}", "");

你帮了我大忙。新版的Android Studio用Ctrl键不能显示一些日志。 - djdance

3
我一直在研究类似的“猜测编码”问题。最好的解决方法是知道编码。如果没有,您可以做出有根据的猜测来区分UTF-8和ISO-8859-1。
要回答如何检测字符串是否正确编码为UTF-8的一般问题,您可以验证以下内容:
  1. 没有字节为0x00、0xC0、0xC1或范围在0xF5-0xFF之间。
  2. 尾字节(0x80-0xBF)前面总是有头字节0xC2-0xF4或另一个尾字节。
  3. 头字节应正确预测尾字节数量(例如,0xC2-0xDF中的任何字节后面都应跟随范围在0x80-0xBF之间的正好一个字节)。
如果字符串通过所有这些测试,则可以将其解释为有效的UTF-8格式。这并不能保证它确实是UTF-8,但它是一个很好的预测指标。
在ISO-8859-1中的合法输入可能不会有控制字符(0x00-0x1F和0x80-0x9F),除了行分隔符外。看起来0x7F在ISO-8859-1中也没有定义。(我基于维基百科对UTF-8和ISO-8859-1的页面得出这个结论。)

3
URLDecoder 可以将给定编码解码。这应该会正确地标记错误。然而,文档说明如下:
有两种可能的方式可以处理非法字符串。它可以直接保留非法字符或者抛出 IllegalArgumentException 异常。实现决定采用哪种方法。
所以你应该尝试一下。还请注意(从 decode() 方法文档): World Wide Web Consortium Recommendation 建议使用UTF-8。如果不这样做可能会导致不兼容问题,因此需要考虑其他方面!
编辑:Apache Commons URLDecode 宣称会为错误的编码抛出适当的异常。

我知道推荐规范,但是浏览器(例如 Firefox 3.0.10)是否违反了这个规范呢?只要它是推荐而不是必需的,你必须确保没有非法实体存在,对吧? - Daniel Hiller
所以我会尝试使用URLDecoder进行解码,并选择适当的编码方式。我很感兴趣(!)看看URLDecoder是否会在非法编码字符上抛出异常(可以在浏览器/服务器环境之外轻松测试)。 - Brian Agnew
Apache Commons的链接已经失效了。看起来URLCodec替代了URLDecoder:https://commons.apache.org/proper/commons-codec/apidocs/org/apache/commons/codec/net/URLCodec.html - Bampfer

2

您可能希望在请求中包含一个已知的参数,例如"...&encTest=ä€",以安全地区分不同的编码。


1

您需要从一开始就设置字符编码。尝试发送正确的Content-Type头,例如Content-Type: text/html; charset=utf-8以修复正确的编码。Web服务的标准符合 utf-8和utf-16作为适当的编码。检查您的响应头。

此外,在服务器端 - 如果浏览器未正确处理服务器发送的编码 - 通过分配新字符串来强制进行编码。还可以通过执行单个each_byte & 0x80并验证结果是否为非零来检查编码的utf-8字符串中的每个字节。


boolean utfEncoded = true;
byte[] strBytes = queryString.getBytes();
for (int i = 0; i < strBytes.length(); i++) {
    if ((strBytes[i] & 0x80) != 0) {
        continue;
    } else {
        /* treat the string as non utf encoded */
        utfEncoded = false;
        break;
    }
}

String realQueryString = utfEncoded ?
    queryString : new String(queryString.getBytes(), "iso-8859-1");

此外,请查看这篇文章,希望它能对您有所帮助。

string.getBytes() 与 new String() 结合使用是一个经典的错误,应该避免。 - Dennis C

1

下面的正则表达式可能会对你有所帮助:

http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/185624

我在Ruby中使用它如下:
module Encoding
    UTF8RGX = /\A(
        [\x09\x0A\x0D\x20-\x7E]            # ASCII
      | [\xC2-\xDF][\x80-\xBF]             # non-overlong 2-byte
      |  \xE0[\xA0-\xBF][\x80-\xBF]        # excluding overlongs
      | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}  # straight 3-byte
      |  \xED[\x80-\x9F][\x80-\xBF]        # excluding surrogates
      |  \xF0[\x90-\xBF][\x80-\xBF]{2}     # planes 1-3
      | [\xF1-\xF3][\x80-\xBF]{3}          # planes 4-15
      |  \xF4[\x80-\x8F][\x80-\xBF]{2}     # plane 16
    )*\z/x unless defined? UTF8RGX

    def self.utf8_file?(fileName)
      count = 0
      File.open("#{fileName}").each do |l|
        count += 1
        unless utf8_string?(l)
          puts count.to_s + ": " + l
        end
      end
      return true
    end

    def self.utf8_string?(a_string)
      UTF8RGX === a_string
    end

end

0

尽可能在任何可以接触的地方都使用UTF-8作为默认编码方式。(数据库、内存和UI)

一个单一的字符集编码可以减少很多问题,实际上它可以加快您的Web服务器性能。有很多处理能力和内存浪费在编码/解码上。


虽然这是好的建议,但它并没有回答问题。考虑将其作为评论。 - james.garriss

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