新闻动态

你的位置:3d组6怎么买最划算 > 新闻动态 > 让数据传输更优雅:SpringBoot前后端加密技巧全攻略

让数据传输更优雅:SpringBoot前后端加密技巧全攻略

发布日期:2025-09-07 10:36    点击次数:111
点击“IT码徒”,关注,置顶公众号 每日技术干货,第一时间送达! 在Web应用中,确保前后端之间的数据传输安全是非常重要的。这通常涉及到使用HTTPS协议、数据加密、令牌验证等安全措施。本文通过将前后端之间的传输数据进行加密,用于在Spring Boot应用中实现前后端传输加密设计。 一、数据加密方案 即使使用了HTTPS,也可能需要在应用层对数据进行额外的加密。这可以通过以下方式实现: 对称加密:加密解密是同一个密钥,速度快,数据接收方需要公布其私钥给数据传输方进行数据加密,安全性完全依赖于...

点击“IT码徒”,关注,置顶公众号

每日技术干货,第一时间送达!

在Web应用中,确保前后端之间的数据传输安全是非常重要的。这通常涉及到使用HTTPS协议、数据加密、令牌验证等安全措施。本文通过将前后端之间的传输数据进行加密,用于在Spring Boot应用中实现前后端传输加密设计。

一、数据加密方案

即使使用了HTTPS,也可能需要在应用层对数据进行额外的加密。这可以通过以下方式实现:

对称加密: 加密解密是同一个密钥,速度快,数据接收方需要公布其私钥给数据传输方进行数据加密,安全性完全依赖于该密钥。适合做大量数据或数据文件的加解密。

使用AES、DES等对称加密算法对敏感数据进行加密和解密。前后端需要共享一个密钥(key)用于加密和解密。密钥的管理和传输需要特别注意安全性。

非对称加密: 加密用公钥,解密用私钥。公钥和私钥是成对的(可借助工具生成,如openssl等),即用公钥加密的数据,一定能用其对应的私钥解密,能用私钥解密的数据,一定是其对应的公钥加密。对大量数据或数据文件加解密时,效率较低。数据接收方需公布其公钥给数据传输方,私钥自己保留,安全性更高。

使用RSA、ECC等非对称加密算法。私钥用于加密数据,公钥用于解密数据。公钥可以公开,而私钥需要安全存储。

混合加密

结合使用对称加密和非对称加密。使用非对称加密算法交换对称加密的密钥(会话密钥),然后使用会话密钥进行实际的数据加密和解密。

这里就赘述介绍每种加密的实现方式和原理。

1.1 数据加密实现方式如果数据传输较大,密钥不需要进行网络传输,数据不需要很高的安全级别,则采用对称加密,只要能保证密钥没有人为外泄即可;如果数据传输小,而且对安全级别要求高,或者密钥需要通过internet交换,则采用非对称加密;

本文采用了两者结合的方式(混合加密模式),这样是大多数场景下采用的加密方式。加密时序图如下所示:

图片

通过使用对称加密(AES) 和 非对称加密(RSA) 的方式来实现对数据的加密;即通过对称加密进行业务数据体的加密,通过非对称加密进行对称加密密钥的加密; 它结合了对称加密的高效性 和 非对称加密的安全性。

注意事项:

确保RSA公钥在传输过程中是安全的,因为任何拥有这个公钥的人都可以用它来加密AES密钥,但只有拥有私钥的人才能解密它。确保在加密和解密过程中使用安全的加密库和最新的加密算法标准。定期更换密钥对和对称密钥,以降低密钥泄露的风险。

这种混合加密模式提供了安全性和效率之间的平衡。对称加密(如AES)用于加密大量数据,因为它通常比非对称加密更快。而非对称加密(如RSA)用于加密密钥,因为它提供了更强的安全性,特别是当密钥需要在不安全的通道上传输时。

1.2 AES加密工具类创建

封装AESUtil工具类时 pom.xml 中运用到的依赖:

<!--   hutool-all工具类依赖   --><dependency>  <groupId>cn.hutool</groupId>  <artifactId>hutool-all</artifactId>  <version>5.8.18</version></dependency>

AES加解密工具类 AESUtil 代码:

package com.example.api_security_demo.utils;import cn.hutool.core.codec.Base64;import javax.crypto.Cipher;import javax.crypto.KeyGenerator;import javax.crypto.spec.SecretKeySpec;import java.io.UnsupportedEncodingException;import java.security.Key;import java.security.NoSuchAlgorithmException;import java.security.SecureRandom;import java.util.Random;/** * @ClassName : AESUtil * @Description : AES加密工具类 * @Author : AD */public class AESUtil {    public static final String CHAR_ENCODING = "UTF-8";    /**     * [常见算法]AES、DES、RSA、Blowfish、RC4 等等     * [常见的模式] ECB (电子密码本模式)、CBC (密码分组链接模式)、CTR (计数模式) 等等     * [常见的填充] NoPadding、PKCS5Padding、PKCS7Padding 等等     *     * [AES算法]可以有以下几种常见的值:     * AES:标准的AES算法。     * AES/CBC/PKCS5Padding:使用CBC模式和PKCS5填充的AES算法。     * AES/ECB/PKCS5Padding:使用ECB模式和PKCS5填充的AES算法。     * AES/GCM/NoPadding:使用GCM模式的AES算法,不需要填充。     * AES/CCM/NoPadding:使用CCM模式的AES算法,不需要填充。     * AES/CFB/NoPadding:使用CFB模式的AES算法,不需要填充。     * */    public static final String AES_ALGORITHM = "AES";    public static char[] HEXCHAR = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};    /**     * Description: 随机生成 AESKey密钥     *     * @param length 随机生成密钥长度     * @return java.lang.String    */    public static String getAESKey(int length) throws Exception    {        /*        * Random类用于生成伪随机数。        * */        Random random = new Random();        StringBuilder ret = new StringBuilder();        for(int i = 0; i < length; i++)        {            // 选择生成数字还是字符            boolean isChar = (random.nextInt(2) % 2 == 0);            /* 0随机生成一个字符*/            if (isChar)            {                // 选择生成大写字母 / 小写字母                int choice = (random.nextInt(2) % 2 == 0) ? 65 : 97;                ret.append((char) (choice+random.nextInt(26)));                /* 1随机生成一个数字 */            }else            {                ret.append( random.nextInt(10));            }        }        return ret.toString();    }    /**     * Description: 加密     *     * @param data 待加密数据内容     * @param aesKey 加密密钥     * @return byte[]    */    public static byte[] encrypt(byte[] data,byte[] aesKey)    {        if (aesKey.length != 16)        {            throw new RuntimeException("Invalid AES key length (must be 16 bytes) !");        }        try{            /*            * 创建一个SecretKeySpec对象来包装AES密钥。            * 它使用了aesKey字节数组作为密钥,并指定算法为"AES"。            * 这个对象用来提供对称加密算法的密钥。            * */            SecretKeySpec secretKey = new SecretKeySpec(aesKey, "AES");            /*            * 获取SecretKeySpec对象中的编码形式,将其存储在encodedFormat字节数组中。            * 这个编码形式可以被用来重新构造密钥。            * */            byte[] encodedFormat = secretKey.getEncoded();            /*            * 使用encodedFormat字节数组创建了另一个SecretKeySpec对象secKey。            * 这个对象也用来提供对称加密算法的密钥。            * */            SecretKeySpec secKey = new SecretKeySpec(encodedFormat, "AES");            /*            * 使用Cipher类的getInstance()方法获取了一个Cipher对象(创建密码器)。            * 这个对象用来完成加密或解密的工作。            * */            Cipher cipher = Cipher.getInstance(AES_ALGORITHM);            /*            * 码调用init()方法来初始化Cipher对象(初始化)。            * 它要求传入操作模式和提供密钥的对象,这里使用Cipher.ENCRYPT_MODE代表加密模式,以及之前创建的secKey对象作为密钥。            * */            cipher.init(Cipher.ENCRYPT_MODE,secKey);            /*            * 用Cipher对象对data进行加密操作,得到加密后的结果存储在result字节数组中。            * */            byte[] result = cipher.doFinal(data);            return result;        }catch (Exception e){            throw new RuntimeException(" encrypt fail! ",e);        }    }    /**     * Description: 解密     *     * @param data 解密数据     * @param aesKey 解密密钥     * @return byte[]     */    public static byte[] decrypt(byte[] data,byte[] aesKey)    {        if (aesKey.length != 16)        {            throw new RuntimeException(" Invalid AES Key length ( must be 16 bytes)");        }        try {            SecretKeySpec secretKeySpec = new SecretKeySpec(aesKey, "AES");            byte[] encodedFormat = secretKeySpec.getEncoded();            SecretKeySpec secKey = new SecretKeySpec(encodedFormat, "AES");            /* 创建密码器 */            Cipher cipher = Cipher.getInstance(AES_ALGORITHM);            /* 初始化密码器 */            cipher.init(Cipher.DECRYPT_MODE,secKey);            byte[] result = cipher.doFinal(data);            return result;        }catch (Exception e){            throw new RuntimeException(" Decrypt Fail !",e);        }    }    /**     * Description:加密数据,并转换为Base64编码格式!     *     * @param data 待加密数据     * @param aeskey 加密密钥     * @return java.lang.String    */    public static String encryptToBase64(String data,String aeskey)    {        try {            byte[] valueByte = encrypt(data.getBytes(CHAR_ENCODING), aeskey.getBytes(CHAR_ENCODING));            /* 加密数据转 Byte[]--> 换为Base64 --> String */            return Base64.encode(valueByte);        }catch (UnsupportedEncodingException e){            throw new RuntimeException(" Encrypt Fail !",e);        }    }    /**     * Description: 解密数据,将Basse64格式的加密数据进行解密操作     *     * @param data     * @param aeskey     * @return java.lang.String    */    public static String  decryptFromBase64(String data,String aeskey)    {        try {            byte[] originalData = Base64.decode(data.getBytes());            byte[] valueByte = decrypt(originalData,aeskey.getBytes(CHAR_ENCODING));            return new String(valueByte,CHAR_ENCODING);        }catch (UnsupportedEncodingException e){            throw new RuntimeException("Decrypt Fail !",e);        }    }    /**     * Description:加密数据,aesKey为Base64格式时,并将加密后的数据转换为Base64编码格式     *     * @param data     * @param aesKey     * @return java.lang.String    */    public static String encryptWithKeyBase64(String data,String aesKey)    {        try{            byte[] valueByte = encrypt(data.getBytes(CHAR_ENCODING), Base64.decode(aesKey.getBytes()));            return Base64.encode(valueByte);        }catch (UnsupportedEncodingException e){            throw new RuntimeException("Encrypt Fail!",e);        }    }    /**     * Description: 解密数据,数据源为Base64格式,且 aesKey为Base64编码格式     *     * @param data     * @param aesKey     * @return java.lang.String    */    public static String decryptWithKeyBase64(String data,String aesKey)    {        try {            byte[] originalDate = Base64.decode(data.getBytes());            byte[] valueByte = decrypt(originalDate,Base64.decode(aesKey.getBytes()));            return new String(valueByte,CHAR_ENCODING);        }catch (UnsupportedEncodingException e){            throw new RuntimeException("Decrypt Fail !",e);        }    }    /**     * Description:通过密钥生成器生成一个随机的 AES 密钥,并将其以字节数组的形式返回。     * 主要功能是生成并返回一组随机的密钥字节数组,这些字节数组可用于加密和解密数据。     *     * @param     * @return byte[]    */    public static byte[] generateRandomAesKey()    {        KeyGenerator keyGenerator = null;        try{            /*            * KeyGenerator是Java Cryptography Architecture(JCA)提供的主要密钥生成器类之一,用于生成对称加密算法的密钥。            * 获取一个用于生成AES算法密钥的KeyGenerator实例,以便在加密和解密操作中使用该密钥。            * */            keyGenerator = KeyGenerator.getInstance(AES_ALGORITHM);        }catch (NoSuchAlgorithmException e){            throw new RuntimeException("GenerateRandomKey Fail !",e);        }        /*        * SecureRandom 类提供了一种用于生成加密强随机数的实现。        * */        SecureRandom secureRandom = new SecureRandom();        /*        * 初始化密钥生成器 keyGenerator。        * 初始化密钥生成器时使用了 SecureRandom 实例,以确保生成的密钥具有足够的随机性。        * */        keyGenerator.init(secureRandom);        /*        * 调用 generateKey() 方法,使用初始化后的 keyGenerator 生成密钥对象 key。        * */        Key key = keyGenerator.generateKey();        //返回生成的密钥的字节数组表示。        return key.getEncoded();    }    /**     * Description: 通过密钥生成器生成一个随机的 AES 密钥,并转化为Base64格式     *     * @param     * @return java.lang.String    */    public static String generateRandomAesKeyWithBase64()    {        return Base64.encode(generateRandomAesKey());    }/* !!当GET请求进行加密时,地址上的加密参数就以16进制字符串的方式进行传输,否则特殊符号路径无法解析[ +、/、=]等Base64编码格式 */    /**     * Description: 从Byte[] 数组转 16进制字符串     *     * @param b     * @return java.lang.String     */    public static String toHexString(byte[] b)    {        /*         * 每个字节都可以用两个十六进制字符来表示,因此初始化的容量是字节数组长度的两倍。         * */        StringBuilder sb = new StringBuilder(b.length * 2);        for (int i = 0; i<b.length ;i++)        {            /*             * 首先取字节的高四位,然后查找对应的十六进制字符,并将其追加到StringBuilder中             * */            sb.append(HEXCHAR[(b[i] & 0xf0) >>> 4]);            /*             * 取字节的低四位,找到对应的十六进制字符,并追加到StringBuilder中。             * */            sb.append(HEXCHAR[b[i] & 0x0f]);        }        return sb.toString();    }    /**     * Description: 从16进制字符串转 byte[] 数组     *     * @param s     * @return byte[]     */    public static final byte[] toBytes(String s)    {        byte[] bytes;        bytes = new byte[s.length() / 2];        for (int i = 0; i < bytes.length ; i++)        {            bytes[i] = (byte) Integer.parseInt(s.substring(2*i,2*i+2),16);        }        return bytes;    }}

AES加解密工具类方法代码解析(为了方便自己理解和使用,有必要简单分类记录一下工具类中的方法接口):

AESUtil工具类中的方法封装的比较杂乱,通过梳理之后更加能理清每个方法的具体用法和功能!

生成AES密钥的方法:

该工具类中总共封装了两种 生成 AES密钥的方法 String getAESKey(int length) 和 String generateRandomAesKeyWithBase64()

getAESKey 方法生成的密钥是 由数字(0-9)、小写字母、大写字母随机组成的普通字符串;

generateRandomAesKeyWithBase64() 方法生成的密钥是 通过javax.crypto.KeyGenerator 密钥生成器生成Byte[] 类型的数据 在将该 byte[] 转换为 Base64编码格式。

图片

AES加密数据方法:

1.AES工具类封装的加密数据方法有以下几种byte[] encrypt(byte[] data,byte[] aesKey)、 String encryptToBase64(String data,String aeskey) 和  String encryptWithKeyBase64(String data,String aesKey) 共三种加密方式:

注:其实其余加密方法都是基于该方法进行封装的。也可以根据自己需求来调整,注意区别在于传入的数据格式有所区别!!

2.String encryptToBase64(String data,String aeskey) 该AES加密方法是通过传入加密数据的字符串,同时传入字符格式的AES密钥Key(通过getAESKey生成的密钥);方法内部会将传入进来的 待加密数据 data 和 aesKey密钥转换为 byte[] 格式,然后在调用第一种加密方法;最后生成的加密数据byte[] 也会在内部自动转换为Base64编码格式 然后返回。

3.String encryptWithKeyBase64(String data,String aesKey) 该AES加密方法,需要传入 字符串形式的加密数据,以及Base64编码格式的AES密钥 (该密钥主要是通过generateRandomAesKeyWithBase64() 方法生成的密钥数据 为Base64编码格式)。加密方法内部在接收到 待加密数据后会自动转换为byte[]格式;在接收到Base64编码格式的AES密钥后,通过Base64.decode() 将其解码为 byte[]。然后在调用原始的加密方法对待加密数据进行加密操作。最终加密后的数据byte[] 通过 new String(valueByte,“UTF-8”) 的方式转换为字符串返回。

图片

AES解密数据方法:

AES工具类中封装的解密方法,对应于加密方法:byte[] decrypt(byte[] data,byte[] aesKey)、 String decryptFromBase64(String data,String aeskey) 和 String decryptFromBase64(String data,String aeskey) 三种方式。

该三种方式分别与上面三种加密方式是对应的。需要注意传入的数据封装格式就行。

第一种解密方法就需要传入 加密后数据格式 byte[],密钥格式 byte[]

第二种解密方法对应于上面的第二种加密方法。需要传入的加密数据为Base64编码格式( 通过加密方法生成byte[] 后 在转换为 Base64格式 ),需要传入AES密钥格式就为普通字符串格式(通过 String getAESKey(int length) 方法生成的密钥)。

第三种解密方法,需要传入的待解密数据 为Base64编码格式,需要传入的AES密钥也为Base64编码格式

图片

String toHexString(byte[] b) 方法

该方法是将byte[] 字节数据转换为16进制的字符串数据,后续会利用到。 比如在Get请求种传输加密数据,如果前端加密后的数据需要放入地址中进行传输到后端;若采用Base64编码格式数加密数据进行传输时,加密内容会包含 +、\、= 三个符号,无法在地址中进行传输了。

所以这里封装了该方法,通过调用该方法,将加密后的byte[] 字节数据数据转换为 HexString 16进制字符串格式(只包含了 0~9、a、b、c、d、e、f)。这样Get请求中的加密数据就可以通过地址进行传输了

byte[] toBytes(String s) 方法

该方法于 toHexString 方法相对应,将转换为HexString十六进制的字符串 还原为字节数据Byte[]。

1.3 RSA加密工具类创建

RSA加密工具类,同样引用了 hutool-all 依赖工具类。

package com.example.api_security_demo.utils;import cn.hutool.core.codec.Base64Encoder;import javax.crypto.Cipher;import java.io.ByteArrayOutputStream;import java.security.*;import java.security.interfaces.RSAPrivateKey;import java.security.interfaces.RSAPublicKey;import java.security.spec.PKCS8EncodedKeySpec;import java.security.spec.X509EncodedKeySpec;import java.util.*;/** * @ClassName : RSAUtil * @Description : RSA加密工具类 * @Author : AD */public class RSAUtil {    /**     * "SHA256withRSA" 是一种使用 SHA-256 哈希算法和 RSA 加密算法结合的数字签名算法。     * 在这种算法中,数据首先会通过 SHA-256 进行哈希处理,得到一个固定长度的摘要,然后使用 RSA 私钥对这个摘要进行加密,从而生成数字签名。     * */    public static final String  ALGORITHM_SHA256WITHRSA = "SHA256withRSA";    public static final String KEY_ALGORITHM ="RSA";    //RSA最大加密明文大小    public static final int MAX_ENCRYPT_BLOCK = 117;    //RSA最大解密密文大小    public static final int MAX_DECRYPT_BLOCK = 128;    private static char[] HEXCHAR = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };    /**     * Description: 公钥分段加密     *     * @param data 待加密源数据     * @param publicKey 公钥(BASE64编码)     * @param length 段长 1024长度的公钥最大取117     * @return byte[]    */    public static byte[] encryptByPublicKey(byte[] data,String publicKey,int length) throws Exception    {        /*        * 将BASE64编码格式 publicKey进行解码        * */        byte[] publicKeyByte = decryptBASE64(publicKey);        /*        * 使用X509EncodedKeySpec类创建了一个X.509编码的KeySpec对象,并将publicKeyByte作为参数传入。        * 将公钥 [字符串] 解码成 [公钥对象] ,以便用于加密数据。         * */        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyByte);        /*        * 通过KeyFactory获取了RSA的实例        * */        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);        /*        * 调用generatePublic方法使用之前创建的X509EncodedKeySpec对象来生成公钥。        * */        Key generatePublicKey = keyFactory.generatePublic(x509EncodedKeySpec);        /*        * 创建一个Cipher实例,它是用于加密或解密数据的对象。Cipher类提供了加密和解密功能,并支持许多不同的加密算法。        * 在这里,getInstance 方法中传入了keyFactory.getAlgorithm()[获取与指定密钥工厂相关联的算法名称。],它用于获取与指定算法关联的 Cipher 实例。        * */        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());        /*        * 初始化 Cipher 对象。        * 在初始化过程中,指定加密模式为 ENCRYPT_MODE,并传入了之前生成的公钥 generatePublicKey。        * */        cipher.init(Cipher.ENCRYPT_MODE,generatePublicKey);        int inputLen = data.length;        ByteArrayOutputStream out = new ByteArrayOutputStream();        //段落起始位置        int offSet = 0;        byte[] cache;        int i = 0;        //对数据进行分段加密        while (inputLen - offSet > 0)        {            if (inputLen - offSet > length) {                cache = cipher.doFinal(data,offSet,length);            } else {                cache = cipher.doFinal(data,offSet,inputLen-offSet);            }            out.write(cache,0,cache.length);            i++;            offSet = i * length;        }        byte[] encryptDate = out.toByteArray();        out.close();        return encryptDate;    }    /**     * Description:     *     * @param data 待解密数据     * @param privateKey 私密(BUSE64编码)     * @param length 分段解密长度 128     * @return byte[]    */    public static byte[] decryptByPrivateKey(byte[] data,String privateKey,int length) throws Exception    {        byte[] privateKeyByte = decryptBASE64(privateKey);        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyByte);        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);        Key generatePrivateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());        cipher.init(Cipher.DECRYPT_MODE,generatePrivateKey);        int inputLen = data.length;        ByteArrayOutputStream out = new ByteArrayOutputStream();        int offSet = 0;        byte[] cache;        int i = 0;        //对数据进行分段解密        while (inputLen - offSet > 0)        {            if (inputLen - offSet > length)            {                cache = cipher.doFinal(data,offSet,length);            } else {                cache = cipher.doFinal(data,offSet,inputLen - offSet);            }            out.write(cache,0,cache.length);            i++;            offSet = i * length;        }        byte[] decryptData = out.toByteArray();        out.close();        return decryptData;    }    /**     * Description: BASE64解码     *     * @param src     * @return byte[]    */    public static byte[] decryptBASE64(String src)    {        sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();        try{            return decoder.decodeBuffer(src);        }catch (Exception ex){            return null;        }    }    /**     * Description: BASE64编码     *     * @param src     * @return java.lang.String    */    public static String encryptBASE64(byte[] src)    {       sun.misc.BASE64Encoder encoder = new sun.misc.BASE64Encoder();        return encoder.encode(src);    }    /**     * Description: 从Byte[] 数组转 16进制字符串     *     * @param b     * @return java.lang.String    */    public static String toHexString(byte[] b)    {        /*        * 每个字节都可以用两个十六进制字符来表示,因此初始化的容量是字节数组长度的两倍。        * */        StringBuilder sb = new StringBuilder(b.length * 2);        for (int i = 0; i<b.length ;i++)        {            /*            * 首先取字节的高四位,然后查找对应的十六进制字符,并将其追加到StringBuilder中            * */            sb.append(HEXCHAR[(b[i] & 0xf0) >>> 4]);            /*            * 取字节的低四位,找到对应的十六进制字符,并追加到StringBuilder中。            * */            sb.append(HEXCHAR[b[i] & 0x0f]);        }        return sb.toString();    }    /**     * Description: 从16进制字符串转 byte[] 数组     *     * @param s     * @return byte[]    */    public static final byte[] toBytes(String s)    {        byte[] bytes;        bytes = new byte[s.length() / 2];        for (int i = 0; i < bytes.length ; i++)        {            bytes[i] = (byte) Integer.parseInt(s.substring(2*i,2*i+2),16);        }        return bytes;    }    /**     * Description: 判断对象是否为null     */    public static boolean isEmpty(Object str) {        return (str == null 
                        

上一篇:血汗钱蒸发?揭秘股市7大隐形风险及保命法则
下一篇:萍乡白喜事谢孝词(谱名金鑫)
TOP