当前位置:   article > 正文

Android 进阶——常用的Base64编码算法和MD5 信息摘要算法详解_android md5

android md5

引言

无论是任何数据,在计算机的世界里本质上都是0101010 二进制字符流,而无论是ASCII、Unicode、UTF-8、Base64等编码方法本质上就是约定了对于二进制字符流的解释规则,解码程序/解释程序都遵守同一种解释方法,因而能洞悉二进制字符流的背后真正含义。

一、Base64

一个字节(8位)可表示的范围是0——255(即十六进制:0x00——0xFF),其中ASCII 码的值范围为0——127(即十六进制:0x00——0x7F),超过ASCII 码范围的(即十六进制:0x80——0xFF)之间的值是不可见的。

一个0或者1 二进制字符为一位;一个字节 8位;6位为一个Base64 单元;一个Base64单元对应一个可打印的字符

在这里插入图片描述

1、ASCII

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nEHYuYUC-1604140903735)(D:\Doc\个人文档\TODO\assets\image-20200930095810077.png)]

ASCII(American Standard Code for Information Interchange,美国信息互换标准代码)是一套基于拉丁字母的字符编码,共收录了 128 个字符,用一个字节就可以存储,它等同于国际标准 ISO/IEC 646。

在ASCII码中规定,0--31127共33个索引对应的字符属于控制字符(具有某些特殊功能但是无法显示的字符,不可打印字符,如下表所示部分常见的控制字符)
在这里插入图片描述
32--126共95个索引对应的字符属于可打印字符(可以显示,也可以从键盘上输入的字符),网络传输只能传输这95个字符,不在这个范围内的字符无法传输,比如一个纯文本协议,二进制中可能会出现被当做控制字符处理的数据,导致传输失败或者解析失败。

因为在设备之间通过网络通信时,往往要经过多个路由设备,由于不同的设备对字符的处理方式有一些不同(经ASCII/UTF-8编码后在大多数机器处理相同),这样那些不可见字符就有可能被处理错误,这是不利于传输的,而Base64 就是一种简洁统一的解释方案。

在这里插入图片描述

2、Base64 概述

Base64是一种基于64个可打印字符来表示二进制数据的编码方法常用于表示、传输、存储二进制数据,也可以用于将一些含有特殊字符的文本内容编码,以便传输。大部分网络协议(比如HTTP协议、SMTP、MQTT等)是纯文本协议,而在网络协议下二进制格式的数据(图片,音频,视频,语音等非文本字符)无法直接传输,但直接转换是不行的,因为网络传输只能传输可打印字符,故可以先将原始的二进制字节数组使用Base64 重新进行编码,使所有的字符都是可见的,再行传输。

Base64 里的64个字符在ASCII 码里都是可见的,64个字符中不包括特殊字符以免被一些应用认为是控制字符,如ftp,ssh等,Base64 不是加密算法,也不应该作为校验之用

3、Base64 编码原理

标准 Base64 里的有 65 个可打印字符—— A--Z a--z 0--9 + / = 或者A--Z a--z 0--9 - _ =(即小写字母a-z、大写字母A-Z、数字0-9、符号"+"("-")、"/"("_") 再加上作为垫字的"=")作为基本的编码字符集,分别依次对应索引值 0-63,Base64 编码就是使用这64个字符来解释二进制字节流,其他所有符号都转换成这个字符集中的字符。简而言之,Base64编码就是把所有字符变为可见的字符

3.1、选出Base64 的64个字符集

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Im1ZbYRZ-1604140903747)(D:\Doc\个人文档\TODO\assets\image-20200930100815786.png)]

为了兼容不同的场景下Base64的应用,国际上有了两种字符集:

  • RFC 4648定义Base64的字符集

    A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9 + /

  • RFC 4686提议使用另外一个字符集

    A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9 - _

    因为RFC 4648 字符集中包括了 ‘+/’ ,而一般的URL和文件名中,不能使用这两个字符,故RFC 4686 字符集诞生。

而具体用哪个字符集,要结合具体的场景。若只是在项目内部使用,编解码都可控,则就可以使用第二个,甚至可以使用一个自定义的字符集;但如果是将数据给第三方,那么使用第一个字符集更好,因为大部分库的实现都使用了第一个字符集,Base64相应的索引表:

引入 https://segmentfault.com/a/1190000021624542

3.2、得到原始字符串对应的ASCII码

3.3、把ASCII码值转为二进制

如果不是ASCII 码值,需要在Base64编码前先转码,如可以直接转为二进制流。

3.4、把原始的二进制位全部合并起来,从左到右每6位为一个Base64 单元分组,再把Base64 单元二进制转为十进制

转换过程针对不同的情况,具体步骤略有差异:

3.4.1、当编码字节流长度刚好是6的倍数时,合并之后直接重组

在这里插入图片描述
首先从ASCII码表得知M 对应的ASCII码十进制值为77,二进制表示为01001101,继续查阅a 、n 得到各自的二进制字节流,然后合并到一起,再从左到右按照每6位为一组(即一个Base64单元)得到全新的二进制字节流,再转为十进制值得到在Base64字节表的索引,于是乎Man 的Base64 编码变为TWFu,不过也由原来的3个字节变成了4个可打印字符,经Base64 编码后体积增加了1/3,即编码前后长度比4/3

在这里插入图片描述

上面Man 编码长度3个字节24位,正好24/6=4个Base64,字节流长度刚好是6的倍数时。

3.4.2、当编码字节流长度不能被6整除时,在末尾补0字节,能够整除后再重组

当编码字节流长度不能被6整除时,在末尾会多出1个字节或者2个字节,所以需要使用0 来补足1 字节或者 2字节后,再重组为Base64单元。

在这里插入图片描述

再以以 Hello!! 为例,其转换过程为:http://blog.xiayf.cn/2016/01/24/base64-encoding/

在这里插入图片描述

Hello!! 经Base64编码后计算得到的为 SGVsbG8hIQAA ,但最后2个零值只是为了Base64编码而补充的,在原始字符中并没有对应的字符,即Base64编码结果中的最后两个字符 AA 实际上不带有效信息,所以需要特殊处理——标准Base64编码通常= 字符来替换最后的 A,即最终编码结果为 SGVsbG8hIQ==

因为 = 字符并不在Base64编码索引表中,其意义在于结束符号,在Base64解码时遇到 = 时即可知道一个Base64编码字符串结束。

但如果确认Base64编码字符串不会相互拼接再传输,那么最后的 = 也可以省略,解码时如果发现Base64编码字符串长度不能被4整除,则先补充 = 字符,再解码即可。

解码是对编码的逆向操作,对于最后的两个 = 字符,转换成两个 A 字符,再转成对应的两个6比特二进制0值,接着转成原始字符之前,需要将最后的两个6比特二进制0值丢弃,因为它们实际上不携带有效信息

Base64部分图文摘自一文读懂Base64编码

二、MD5 概述

MD5信息摘要算法(MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。MD5码以512位分组来处理输入的信息,且每一分组又被划分为16个32位子分组,经过了一系列的处理后,算法的输出由四个32位分组组成,将这四个32位分组级联后将生成一个128位散列值,每次的运算都由前一轮的128位结果值和当前的512bit值进行运算。简而言之,就是通过MD5算法可以从字符串或者文件对象中提取唯一的一串固定长度为32的散列字符串

        Log.e("CRAZYMO","获取图片路径对应文件的MD5="+BitmapUtil.MD5.getFileMD5(BitmapUtil.getSDPath()+"/cmo.PNG"));
        Log.e("CRAZYMO","获取图片的Base64的MD5="+BitmapUtil.MD5.getStrMD5(BitmapUtil.bitmap2Base64(BitmapUtil.getSDPath()+"/cmo.PNG")));
        Bitmap bitmap=BitmapFactory.decodeFile(BitmapUtil.getSDPath()+"/cmo.PNG");
        Log.e("CRAZYMO","获取图片的bytes数组的MD5="+BitmapUtil.MD5.getBytesMD5(BitmapUtil.bitmap2Bytes(bitmap)));

  • 1
  • 2
  • 3
  • 4
  • 5

在这里插入图片描述

三、Base64 和MD5 的简单应用工具类

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.annotation.StringDef;
import android.text.TextUtils;
import android.util.Base64;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class BitmapUtil {

    public static String getSDPath() {
        boolean sdCardExist = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
        if (sdCardExist) {
            return Environment.getExternalStorageDirectory().getPath();
        } else {
            return "";
        }
    }

    /**
     * 将图片转换成Base64编码的字符串
     *
     * @param path Bitmap的全路径
     */
    public static String bitmap2Base64(String path) {
        if (TextUtils.isEmpty(path)) {
            return null;
        }
        InputStream is = null;
        byte[] data = null;
        String result = null;
        try {
            is = new FileInputStream(path);
            //创建一个字符流大小的数组。
            data = new byte[is.available()];
            //写入数组
            is.read(data);
            //用默认的编码格式进行编码
            result = Base64.encodeToString(data, Base64.NO_CLOSE);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != is) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
        return result;
    }

    /**
     * 将Base64编码字符串转为图片
     *
     * @param base64Str 源编码的Base64字符串
     * @return bitmap
     */
    public static Bitmap base64ToBitmap(String base64Str) {
        byte[] data = Base64.decode(base64Str, Base64.NO_WRAP);
        if (data != null && data.length > 0) {
            for (int i = 0; i < data.length; i++) {
                if (data[i] < 0) {
                    //调整异常数据
                    data[i] += 256;
                }
            }
            return BitmapFactory.decodeByteArray(data, 0, data.length);
        }
        return null;
    }

    /**
     * 将Base64编码转换为Bitmap 并保存
     *
     * @param base64Str 源编码的Base64字符串
     * @param parentDir Bitmap保存的父目录路径
     * @param name      Bitmap 的名称,不需要带上后缀
     * @return Bitmap的全路径名称
     */
    public static String base64ToBitmap(String base64Str, String parentDir, String name, @ImageType String type) {
        String path = "";
        byte[] data = Base64.decode(base64Str, Base64.NO_WRAP);
        for (int i = 0; i < data.length; i++) {
            if (data[i] < 0) {
                //调整异常数据
                data[i] += 256;
            }
        }
        OutputStream os = null;
        try {
            File parentFile = new File(parentDir);
            if (!parentFile.exists()) {
                parentFile.mkdirs();
            } else {
                if (getFileSize(parentFile) > 1024 * 1024 * 50) {
                    deleDir(parentDir);
                }
            }
            File jpgFile = new File(parentDir, name + type);
            os = new FileOutputStream(jpgFile);
            os.write(data);
            os.flush();
            path = jpgFile.getAbsolutePath();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return path;
    }

    public static byte[] bitmap2Bytes(Bitmap bitmap) {
        ByteArrayOutputStream byteArrOutStream = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrOutStream);
        return byteArrOutStream.toByteArray();
    }

    public static Bitmap bytes2Bitmap(@NonNull byte[] bytes) {
        if (bytes.length != 0) {
            return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
        }
        return null;
    }

    public static Bitmap compressionBitmap(@NonNull Bitmap bitmap) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        if (baos.toByteArray().length / 1024 > 2048) {
            baos.reset();
            bitmap.compress(Bitmap.CompressFormat.JPEG, 60, baos);
        }
        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());
        BitmapFactory.Options newOpts = new BitmapFactory.Options();
        newOpts.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(isBm, null, newOpts);
        newOpts.inJustDecodeBounds = false;
        int w = newOpts.outWidth;
        int h = newOpts.outHeight;
        int be = 1;
        if (w > h && ((float) w) > 1280.0f) {
            be = (int) (((float) newOpts.outWidth) / 1280.0f);
        } else if (w < h && ((float) h) > 720.0f) {
            be = (int) (((float) newOpts.outHeight) / 720.0f);
        }
        if (be <= 0) {
            be = 1;
        }
        newOpts.inSampleSize = be;
        newOpts.inPreferredConfig = Bitmap.Config.RGB_565;
        return BitmapFactory.decodeStream(new ByteArrayInputStream(baos.toByteArray()), null, newOpts);
    }

    /**
     * 将原始的位图数据的bytes 保存到指定路径下
     *
     * @param data
     * @param parentDir Bitmap保存的父目录路径
     * @param name      Bitmap 的名称,不需要带上后缀
     * @return Bitmap的全路径名称
     */
    public static void save2SDCard(byte[] data, String parentDir, String name, @ImageType String type) {
        FileOutputStream outputStream = null;
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA);
        String dateStr = df.format(new Date());
        String photoName = "qr_" + dateStr;
        try {
            File parentFile = new File(parentDir);
            if (!parentFile.exists()) {
                parentFile.mkdirs();
            }
            File jpgFile = new File(parentDir, photoName + type);
            outputStream = new FileOutputStream(jpgFile); // 文件输出流

//            BASE64Decoder decoder = new BASE64Decoder();
//            try {
//                // Base64解码
//                byte[] bytes = decoder.decodeBuffer(imgStr.substring(22));
//
//                System.out.println("bytes的长度:"+bytes.length);
//
//                for (int i = 0; i < bytes.length; ++i) {
//                    if (bytes[i] < 0) {// 调整异常数据
//                        bytes[i] += 256;
//                    }
//                }
//                // 生成jpg图片
//                OutputStream out = new FileOutputStream(imgFilePath+imgFileName);
//                out.write(bytes);
//                out.flush();
//                out.close();
//            } catch (Exception e) {
//
//            }

            outputStream.write(data); // 写入sd卡中
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (outputStream != null) {
                    outputStream.close(); // 关闭输出流
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static boolean fileExits(String fileName) {
        return (fileName == null || "".equals(fileName) || !new File(fileName).exists()) ? false : true;
    }

    public static boolean deleteFile(String fileName) {
        File file = new File(fileName);
        if (fileExits(fileName)) {
            return file.delete();
        }
        return false;
    }

    public static boolean deleDir(String dirName) {
        if (!fileExits(dirName)) {
            return false;
        }
        File dir = new File(dirName);
        File[] tmp = dir.listFiles();
        for (int i = 0; i < tmp.length; i++) {
            if (tmp[i].isDirectory()) {
                deleDir(dirName + "/" + tmp[i].getName());
            } else {
                tmp[i].delete();
            }
        }
        return dir.delete();
    }

    public static long getFileSize(File f) {
        long size = 0;
        File flist[] = f.listFiles();
        for (int i = 0; i < flist.length; i++) {
            if (flist[i].isDirectory()) {
                size = size + getFileSize(flist[i]);
            } else {
                size = size + flist[i].length();
            }
        }
        return size;
    }

    public static class MD5{
        /**
         * 根据图片路径获取MD5
         * @param imagePath
         * @return
         */
        public static String getFileMD5(String imagePath) {
            String ret="";
            if (!TextUtils.isEmpty(imagePath)) {
                File file = new File(imagePath);
                if(file.exists()) {
                    InputStream in=null;
                    StringBuilder md5 = new StringBuilder();
                    try {
                        MessageDigest md = MessageDigest.getInstance("MD5");
                        in= new FileInputStream(file);
                        int n = 0;
                        byte[] dataBytes = new byte[1024];
                        while ((n = in.read(dataBytes)) != -1) {
                            md.update(dataBytes, 0, n);
                        }
                        byte[] bytes = md.digest();
                        for (int i = 0; i < bytes.length; i++) {
                            md5.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
                        }
                        ret=md5.toString().toUpperCase();
                    } catch (NoSuchAlgorithmException | IOException e) {
                        e.printStackTrace();
                    } finally {
                        if(in!=null){
                            try {
                                in.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
            return ret;
        }

        /**
         * 获取字符串对应的MD5
         * @param str
         * @return
         */
        public static String getStrMD5(String str) {
            String ret="";
            if (!TextUtils.isEmpty(str)) {
                try {
                    MessageDigest md5 = MessageDigest.getInstance("MD5");
                    char[] charArray = str.toCharArray();
                    byte[] byteArray = new byte[charArray.length];
                    for (int i = 0; i < charArray.length; i++) {
                        byteArray[i] = (byte) charArray[i];
                    }
                    byte[] md5Bytes = md5.digest(byteArray);
                    StringBuilder hexValue = new StringBuilder();
                    for (int i = 0; i < md5Bytes.length; i++) {
                        int val = ((int) md5Bytes[i]) & 0xff;
                        if (val < 16) {
                            hexValue.append("0");
                        }
                        hexValue.append(Integer.toHexString(val));
                    }
                    ret= hexValue.toString().toUpperCase();
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                }
            }
            return ret;
        }
    }

    /**
     * 获取Bytes 数组的MD5
     * @param charArray
     * @return
     */
    public static String getBytesMD5(byte[] charArray){
        String ret="";
        if(charArray!=null && charArray.length>0){
            try {
                MessageDigest md5 = MessageDigest.getInstance("MD5");
                byte[] byteArray = new byte[charArray.length];
                for (int i = 0; i < charArray.length; i++) {
                    byteArray[i] = charArray[i];
                }
                byte[] md5Bytes = md5.digest(byteArray);
                StringBuilder hexValue = new StringBuilder();
                for (int i = 0; i < md5Bytes.length; i++) {
                    int val = ((int) md5Bytes[i]) & 0xff;
                    if (val < 16) {
                        hexValue.append("0");
                    }
                    hexValue.append(Integer.toHexString(val));
                }
                ret = hexValue.toString().toUpperCase();
            }catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
        }
        return ret;
    }

    @StringDef({ImageType.JPG, ImageType.PNG, ImageType.BITMAP})
    @Retention(RetentionPolicy.SOURCE)
    public @interface ImageType {
        String PNG = ".png";
        String JPG = ".jpg";
        String BITMAP = ".bmp";
    }
}

  • 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
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/730839
推荐阅读
相关标签
  

闽ICP备14008679号