写点什么

java 安全编码指南之: 字符串和编码

发布于: 2020 年 09 月 16 日
java安全编码指南之:字符串和编码

简介

字符串是我们日常编码过程中使用到最多的 java 类型了。全球各个地区的语言不同,即使使用了 Unicode 也会因为编码格式的不同采用不同的编码方式,如 UTF-8,UTF-16,UTF-32 等。


我们在使用字符和字符串编码的过程中会遇到哪些问题呢?一起来看看吧。


使用变长编码的不完全字符来创建字符串

在 java 中 String 的底层存储 char[]是以 UTF-16 进行编码的。


注意,在 JDK9 之后,String 的底层存储已经变成了 byte[]。


StringBuilder 和 StringBuffer 还是使用的是 char[]。


那么当我们在使用 InputStreamReader,OutputStreamWriter 和 String 类进行 String 读写和构建的时候,就需要涉及到 UTF-16 和其他编码的转换。


我们来看一下从 UTF-8 转换到 UTF-16 可能会遇到的问题。


先看一下 UTF-8 的编码:



UTF-8 使用 1 到 4 个字节表示对应的字符,而 UTF-16 使用 2 个或者 4 个字节来表示对应的字符。


转换起来可能会出现什么问题呢?


    public String readByteWrong(InputStream inputStream) throws IOException {        byte[] data = new byte[1024];        int offset = 0;        int bytesRead = 0;        String str="";
while ((bytesRead = inputStream.read(data, offset, data.length - offset)) != -1) { str += new String(data, offset, bytesRead, "UTF-8"); offset += bytesRead; if (offset >= data.length) { throw new IOException("Too much input"); } } return str; }
复制代码

上面的代码中,我们从 Stream 中读取 byte,每读一次 byte 就将其转换成为 String。很明显,UTF-8 是变长的编码,如果读取 byte 的过程中,恰好读取了部分 UTF-8 的代码,那么构建出来的 String 将是错误的。


我们需要下面这样操作:


    public String readByteCorrect(InputStream inputStream) throws IOException {        Reader r = new InputStreamReader(inputStream, "UTF-8");        char[] data = new char[1024];        int offset = 0;        int charRead = 0;        String str="";
while ((charRead = r.read(data, offset, data.length - offset)) != -1) { str += new String(data, offset, charRead); offset += charRead; if (offset >= data.length) { throw new IOException("Too much input"); } } return str; }
复制代码

我们使用了 InputStreamReader,reader 将会自动把读取的数据转换成为 char,也就是说自动进行 UTF-8 到 UTF-16 的转换。


所以不会出现问题。


char 不能表示所有的 Unicode

因为 char 是使用 UTF-16 来进行编码的,对于 UTF-16 来说,U+0000 to U+D7FF 和 U+E000 to U+FFFF,这个范围的字符,可以直接用一个 char 来表示。


但是对于 U+010000 to U+10FFFF 是使用两个 0xD800–0xDBFF 和 0xDC00–0xDFFF 范围的 char 来表示的。


这种情况下,两个 char 合并起来才有意思,单独一个 char 是没有任何意义的。


考虑下面的我们的的一个 subString 的方法,该方法的本意是从输入的字符串中找到第一个非字母的位置,然后进行字符串截取。


public static String subStringWrong(String string) {        char ch;        int i;        for (i = 0; i < string.length(); i += 1) {            ch = string.charAt(i);            if (!Character.isLetter(ch)) {                break;            }        }        return string.substring(i);    }
复制代码

上面的例子中,我们一个一个的取出 string 中的 char 字符进行比较。如果遇到 U+010000 to U+10FFFF 范围的字符,就可能报错,误以为该字符不是 letter。


我们可以这样修改:


public static String subStringCorrect(String string) {        int ch;        int i;        for (i = 0; i < string.length(); i += Character.charCount(ch)) {            ch = string.codePointAt(i);            if (!Character.isLetter(ch)) {                break;            }        }        return string.substring(i);    }
复制代码

我们使用 string 的 codePointAt 方法,来返回字符串的 Unicode code point,然后使用该 code point 来进行 isLetter 的判断就好了。


注意 Locale 的使用

为了实现国际化支持,java 引入了 Locale 的概念,而因为有了 Locale,所以会导致字符串在进行转换的过程中,产生意想不到变化。


考虑下面的例子:


    public void toUpperCaseWrong(String input){        if(input.toUpperCase().equals("JOKER")){            System.out.println("match!");        }    }
复制代码

我们期望的是英语,如果系统设置了 Locale 是其他语种的话,input.toUpperCase()可能得到完全不一样的结果。


幸好,toUpperCase 提供了一个 locale 的参数,我们可以这样修改:


    public void toUpperCaseRight(String input){        if(input.toUpperCase(Locale.ENGLISH).equals("JOKER")){            System.out.println("match!");        }    }
复制代码

同样的, DateFormat 也存在着问题:


    public void getDateInstanceWrong(Date date){        String myString = DateFormat.getDateInstance().format(date);    }
public void getDateInstanceRight(Date date){ String myString = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.US).format(date); }
复制代码

我们在进行字符串比较的时候,一定要考虑到 Locale 影响。


文件读写中的编码格式

我们在使用 InputStream 和 OutputStream 进行文件对写的时候,因为是二进制,所以不存在编码转换的问题。


但是如果我们使用 Reader 和 Writer 来进行文件的对象,就需要考虑到文件编码的问题。


如果文件是 UTF-8 编码的,我们是用 UTF-16 来读取,肯定会出问题。


考虑下面的例子:


    public void fileOperationWrong(String inputFile,String outputFile) throws IOException {        BufferedReader reader = new BufferedReader(new FileReader(inputFile));        PrintWriter writer = new PrintWriter(new FileWriter(outputFile));        int line = 0;        while (reader.ready()) {            line++;            writer.println(line + ": " + reader.readLine());        }        reader.close();        writer.close();    }
复制代码

我们希望读取源文件,然后插入行号到新的文件中,但是我们并没有考虑到编码的问题,所以可能会失败。


上面的代码我们可以修改成这样:


BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(inputFile), Charset.forName("UTF8")));PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(outputFile), Charset.forName("UTF8")));
复制代码

通过强制指定编码格式,从而保证了操作的正确性。


不要将非字符数据编码为字符串

我们经常会有这样的需求,就是将二进制数据编码成为字符串 String,然后存储在数据库中。


二进制是以 Byte 来表示的,但是从我们上面的介绍可以得知不是所有的 Byte 都可以表示成为字符。如果将不能表示为字符的 Byte 进行字符的转化,就有可能出现问题。


看下面的例子:


    public void convertBigIntegerWrong(){        BigInteger x = new BigInteger("1234567891011");        System.out.println(x);        byte[] byteArray = x.toByteArray();        String s = new String(byteArray);        byteArray = s.getBytes();        x = new BigInteger(byteArray);        System.out.println(x);    }
复制代码

上面的例子中,我们将 BigInteger 转换为 byte 数字(大端序列),然后再将 byte 数字转换成为 String。最后再将 String 转换成为 BigInteger。


先看下结果:


123456789101180908592843917379
复制代码

发现没有转换成功。


虽然 String 可以接收第二个参数,传入字符编码,目前 java 支持的字符编码是:ASCII,ISO-8859-1,UTF-8,UTF-8BE, UTF-8LE,UTF-16,这几种。默认情况下 String 也是大端序列的。


上面的例子怎么修改呢?


    public void convertBigIntegerRight(){        BigInteger x = new BigInteger("1234567891011");        String s = x.toString();  //转换成为可以存储的字符串        byte[] byteArray = s.getBytes();        String ns = new String(byteArray);        x = new BigInteger(ns);        System.out.println(x);    }
复制代码

我们可以先将 BigInteger 用 toString 方法转换成为可以表示的字符串,然后再进行转换即可。


我们还可以使用 Base64 来对 Byte 数组进行编码,从而不丢失任何字符,如下所示:


    public void convertBigIntegerWithBase64(){        BigInteger x = new BigInteger("1234567891011");        byte[] byteArray = x.toByteArray();        String s = Base64.getEncoder().encodeToString(byteArray);        byteArray = Base64.getDecoder().decode(s);        x = new BigInteger(byteArray);        System.out.println(x);
}
复制代码

本文的代码:


learn-java-base-9-to-20/tree/master/security


本文已收录于 http://www.flydean.com/java-security-code-line-string/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!


发布于: 2020 年 09 月 16 日阅读数: 70
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
java安全编码指南之:字符串和编码