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

5次阅读

共计 5033 个字符,预计需要花费 13 分钟才能阅读完成。

简介

字符串是咱们日常编码过程中应用到最多的 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。

先看下后果:

1234567891011
80908592843917379

发现没有转换胜利。

尽管 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/

最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!

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

正文完
 0