2021-11-09

哈希加密详解和md5、sha1、sha256、Java 工具类

前言
在所有的加密算法中使用最多的就是哈希加密了,很多人第一次接触的加密算法如MD5、SHA1都是典型的哈希加密算法,而哈希加密除了用在密码加密上,它还有很多的用途,如提取内容摘要、生成签名、文件对比、区块链等等。这篇文章就是想详细的讲解一下哈希加密,并分享一个哈希加密的工具类。

概述
哈希函数(Hash Function),也称为散列函数或杂凑函数。哈希函数是一个公开函数,可以将任意长度的消息M映射成为一个长度较短且长度固定的值H(M),称H(M)为哈希值、散列值(Hash Value)、杂凑值或者消息摘要(Message Digest)。它是一种单向密码体制,即一个从明文到密文的不可逆映射,只有加密过程,没有解密过程。

它的函数表达式为:h=H(m)

无论输入是什么数字格式、文件有多大,输出都是固定长度的比特串。以比特币使用的Sh256算法为例,无论输入是什么数据文件,输出就是256bit。

哈希算法

把网址A,转换成数字1。网址B,转换成数字2。

一个网址X,转换成数字N,根据数字N作为下标,就可以快速地查找出网址X的信息。这个转换的过程就是哈希算法。

比如这里有一万首歌,给你一首新的歌X,要求你确认这首歌是否在那一万首歌之内。

无疑,将一万首歌一个一个比对非常慢。但如果存在一种方式,能将一万首歌的每首数据浓缩到一个数字(称为哈希码)中,于是得到一万个数字,那么用同样的算法计算新的歌X的编码,看看歌X的编码是否在之前那一万个数字中,就能知道歌X是否在那一万首歌中。

作为例子,如果要你组织那一万首歌,一个简单的哈希算法就是让歌曲所占硬盘的字节数作为哈希码。这样的话,你可以让一万首歌“按照大小排序”,然后遇到一首新的歌,只要看看新的歌的字节数是否和已有的一万首歌中的某一首的字节数相同,就知道新的歌是否在那一万首歌之内了。

一个可靠的哈希算法,应该满足:

对于给定的数据M,很容易算出哈希值X=F(M);

根据X很难反算出M;

很难找到M和N使得F(N)=F(M)

总结哈希加密的特点:

易压缩:对于任意大小的输入x,Hash值的长度很小,在实际应用中,函数H产生的Hash值其长度是固定的。

易计算:对于任意给定的消息,计算其Hash值比较容易。

不可逆:对于给定的Hash值,要找到使得在计算上是不可行的,即求Hash的逆很困难。在给定某个哈希函数H和哈希值H(M)的情况下,得出M在计算上是不可行的。即从哈希输出无法倒推输入的原始数值。这是哈希函数安全性的基础。

抗碰撞性:理想的Hash函数是无碰撞的,但在实际算法的设计中很难做到这一点。
有两种抗碰撞性:一种是弱抗碰撞性,即对于给定的消息,要发现另一个消息,满足在计算上是不可行的;另一种是强抗碰撞性,即对于任意一对不同的消息,使得在计算上也是不可行的。

高灵敏性:这是从比特位角度出发的,指的是1比特位的输入变化会造成1/2的比特位发生变化。消息M的任何改变都会导致哈希值H(M)发生改变。即如果输入有微小不同,哈希运算后的输出一定不同。

哈希加密并非不可破解,2004年,王小云教授在国际密码学大会上公布了破解Hash函数的关键技术。

哈希加密和对称/非对称加密对比

主要有这些区别:

哈希密码是不可逆的,因此无法从密文中获取到原文,而对称/非对称加密可以;
哈希密码加密大部分不需要密钥(除了HMAC),而对称/非对称加密需要;
哈希加密不管是短数据还是长数据,加密后得到的密文长度是固定的,而对称/非对称通常和原文的长度成正比;
哈希加密有可能碰撞,虽然理论的哈希加密是不可能碰撞的,但是只是理论,王小云教授之前就提出碰撞的方法。而对于对称/非对称,一个密文用密钥解密后的结果一定是唯一的;
常见的加密算法
MD5信息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。MD5由美国密码学家罗纳德·李维斯特(Ronald Linn Rivest)设计,于1992年公开,用以取代MD4算法。这套算法的程序在 RFC 1321 标准中被加以规范。1996年后该算法被证实存在弱点,可以被加以破解,对于需要高度安全性的数据,专家一般建议改用其他算法,如SHA-2。2004年,证实MD5算法无法防止碰撞(collision),因此不适用于安全性认证,如SSL公开密钥认证或是数字签名等用途。

SHA-1(英语:Secure Hash Algorithm 1,中文名:安全散列算法1)是一种密码散列函数,美国国家安全局设计,并由美国国家标准技术研究所(NIST)发布为联邦数据处理标准(FIPS)。SHA-1可以生成一个被称为消息摘要的160位(20字节)散列值,散列值通常的呈现形式为40个十六进制数。
SHA-1已经不再视为可抵御有充足资金、充足计算资源的攻击者。2005年,密码分析人员发现了对SHA-1的有效攻击方法,这表明该算法可能不够安全,不能继续使用,自2010年以来,许多组织建议用SHA-2或SHA-3来替换SHA-1。Microsoft、Google以及Mozilla都宣布,它们旗下的浏览器将在2017年前停止接受使用SHA-1算法签名的SSL证书。
2017年2月23日,CWI Amsterdam与Google宣布了一个成功的SHA-1碰撞攻击,发布了两份内容不同但SHA-1散列值相同的PDF文件作为概念证明。

SHA-2/SHA-256
SHA-2有多种不同的位数,导致这个名词有一些混乱。但是无论是“SHA-2”,“SHA-256”或“SHA-256位”,其实都是指同一种加密算法。但是SHA-224”,“SHA-384”或“SHA-512”,表示SHA-2的二进制长度。还要另一种就是会把算法和二进制长度都写上,如“SHA-2 384”。
SSL行业选择SHA作为数字签名的散列算法,从2011到2015,一直以SHA-1位主导算法。但随着互联网技术的提升,SHA-1的缺点越来越突显。从去年起,SHA-2成为了新的标准,所以现在签发的SSL证书,必须使用该算法签名。
也许有人偶尔会看到SHA-2 384位的证书,很少会看到224位,因为224位不允许用于公共信任的证书,512位,不被软件支持。
初步预计,SHA-2的使用年限为五年,但也许会被提前淘汰。这需要时间来验证。


HMAC是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code)的缩写,由H.Krawezyk,M.Bellare,R.Canetti于1996年提出的一种基于Hash函数和密钥进行消息认证的方法,并于1997年作为RFC2104被公布,并在IPSec和其他网络协议(如SSL)中得以广泛应用,现在已经成为事实上的Internet安全标准。它可以与任何迭代散列函数捆绑使用。
HMAC运算利用hash算法,以一个消息M和一个密钥K作为输入,生成一个定长的消息摘要作为输出。HMAC算法利用已有的Hash函数,关键问题是如何使用密钥。

HMAC的密钥长度可以是任意大小,如果小于n(hash输出值的大小),那么将会消弱算法安全的强度。建议使用长度大于n的密钥,但是采用长度大的密钥并不意味着增强了函数的安全性。密钥应该是随机选取的,可以采用一种强伪随机发生器,并且密钥需要周期性更新,这样可以减少散列函数弱密钥的危险性以及已经暴露密钥所带来的破坏
使用SHA-1、SHA-224、SHA-256、SHA-384、SHA-512所构造的HMAC,分别称为HMAC-SHA1、HMAC-SHA-224、HMAC-SHA-384、HMAC-SHA-512。
HMAC算法更象是一种加密算法,它引入了密钥,其安全性已经不完全依赖于所使用的Hash算法,安全性主要有以下几点保证。
(1)使用的密钥是双方事先约定的,第三方不可能知道。作为非法截获信息的第三方,能够得到的信息只有作为“挑战”的随机数和作为“响应”的HMAC 结果,无法根据这两个数据推算出密钥。由于不知道密钥,所以无法仿造出一致的响应。
(2)HMAC与一般的加密重要的区别在于它具有“瞬时"性,即认证只在当时有效,而加密算法被破解后,以前的加密结果就可能被解密。
HMAC的安全性依赖于散列函数H的密码学属性:①抗碰撞属性;②当应用于一个单独的消息分组时H的压缩函数的消息认证属性 。

四种算法的用途:MD5、SHA-1可以在一些数据量较小的情况下用来生成信息摘要,他们生成的摘要长度较短,加密速度快,但是摘要长度较短,有碰撞的可能;SHA-256可以用来对一些大数据量的数据来进行加密、或者生成信息摘要,基于它的长摘要长度,碰撞的可能性较低;HMAC引入了密钥,因此其安全性最高,可以很好的用在对密码的加密上
 

JAVA工具类

package com.gxsoft.common.utils;

/**
 * @program: simple_tools
 * @description: hash 相关的加密工具类
 * @author: sunli
 * @create: 2021/11/9
 **/

public class HashUtil {



        private static final int R1_32 = 15;
        private static final int R2_32 = 13;
        private static final int M_32 = 5;
        private static final int N_32 = 0xe6546b64;
        private static final int C1_32 = 0xcc9e2d51;
        private static final int C2_32 = 0x1b873593;
        private static final int DEFAULT_SEED = 0;

        /**
         * 加法hash
         *
         * @param key 字符串
         * @param prime 一个质数
         * @return hash结果
         */
        public static int additiveHash(String key, int prime) {
            int hash, i;
            for (hash = key.length(), i = 0; i < key.length(); i++) {
                hash += key.charAt(i);
            }
            return hash % prime;
        }

        /**
         * 旋转hash
         *
         * @param key 输入字符串
         * @param prime 质数
         * @return hash值
         */
        public static int rotatingHash(String key, int prime) {
            int hash, i;
            for (hash = key.length(), i = 0; i < key.length(); ++i) {
                hash = (hash << 4) ^ (hash >> 28) ^ key.charAt(i);
            }
            return hash % prime;
        }

        /**
         * 一次一个hash
         *
         * @param key 输入字符串
         * @return 输出hash值
         */
        public static int oneByOneHash(String key) {
            int hash, i;
            for (hash = 0, i = 0; i < key.length(); ++i) {
                hash += key.charAt(i);
                hash += (hash << 10);
                hash ^= (hash >> 6);
            }
            hash += (hash << 3);
            hash ^= (hash >> 11);
            hash += (hash << 15);
            return hash;
        }

        /**
         * Bernstein's hash
         *
         * @param key 输入字节数组
         * @return 结果hash
         */
        public static int bernstein(String key) {
            int hash = 0;
            int i;
            for (i = 0; i < key.length(); ++i) {
                hash = 33 * hash + key.charAt(i);
            }
            return hash;
        }

        /**
         * Universal Hashing
         *
         * @param key 字节数组
         * @param mask 掩码
         * @param tab tab
         * @return hash值
         */
        public static int universal(char[] key, int mask, int[] tab) {
            int hash = key.length, i, len = key.length;
            for (i = 0; i < (len << 3); i += 8) {
                char k = key[i >> 3];
                if ((k & 0x01) == 0) {
                    hash ^= tab[i + 0];
                }
                if ((k & 0x02) == 0) {
                    hash ^= tab[i + 1];
                }
                if ((k & 0x04) == 0) {
                    hash ^= tab[i + 2];
                }
                if ((k & 0x08) == 0) {
                    hash ^= tab[i + 3];
                }
                if ((k & 0x10) == 0) {
                    hash ^= tab[i + 4];
                }
                if ((k & 0x20) == 0) {
                    hash ^= tab[i + 5];
                }
                if ((k & 0x40) == 0) {
                    hash ^= tab[i + 6];
                }
                if ((k & 0x80) == 0) {
                    hash ^= tab[i + 7];
                }
            }
            return (hash & mask);
        }

        /**
         * Zobrist Hashing
         *
         * @param key 字节数组
         * @param mask 掩码
         * @param tab tab
         * @return hash值
         */
        public static int zobrist(char[] key, int mask, int[][] tab) {
            int hash, i;
            for (hash = key.length, i = 0; i < key.length; ++i) {
                hash ^= tab[i][key[i]];
            }
            return (hash & mask);
        }

        /**
         * 改进的32位FNV算法1
         *
         * @param data 数组
         * @return hash结果
         */
        public static int fnvHash(byte[] data) {
            final int p = 16777619;
            int hash = (int) 2166136261L;
            for (byte b : data) {
                hash = (hash ^ b) * p;
            }
            hash += hash << 13;
            hash ^= hash >> 7;
            hash += hash << 3;
            hash ^= hash >> 17;
            hash += hash << 5;
            return hash;
        }

        /**
         * 改进的32位FNV算法1
         *
         * @param data 字符串
         * @return hash结果
         */
        public static int fnvHash(String data) {
            final int p = 16777619;
            int hash = (int) 2166136261L;
            for (int i = 0; i < data.length(); i++) {
                hash = (hash ^ data.charAt(i)) * p;
            }
            hash += hash << 13;
            hash ^= hash >> 7;
            hash += hash << 3;
            hash ^= hash >> 17;
            hash += hash << 5;
            return hash;
        }

        /**
         * Thomas Wang的算法,整数hash
         *
         * @param key 整数
         * @return hash值
         */
        public static int intHash(int key) {
            key += ~(key << 15);
            key ^= (key >>> 10);
            key += (key << 3);
            key ^= (key >>> 6);
            key += ~(key << 11);
            key ^= (key >>> 16);
            return key;
        }

        /**
         * RS算法hash
         *
         * @param str 字符串
         * @return hash值
         */
        public static int rsHash(String str) {
            int b = 378551;
            int a = 63689;
            int hash = 0;

            for (int i = 0; i < str.length(); i++) {
                hash = hash * a + str.charAt(i);
                a = a * b;
            }

            return hash & 0x7FFFFFFF;
        }

        /**
         * JS算法
         *
         * @param str 字符串
         * @return hash值
         */
        public static int jsHash(String str) {
            int hash = 1315423911;

            for (int i = 0; i < str.length(); i++) {
                hash ^= ((hash << 5) + str.charAt(i) + (hash >> 2));
            }

            return hash & 0x7FFFFFFF;
        }

        /**
         * PJW算法
         *
         * @param str 字符串
         * @return hash值
         */
        public static int pjwHash(String str) {
            int bitsInUnsignedInt = 32;
            int threeQuarters = (bitsInUnsignedInt * 3) / 4;
            int oneEighth = bitsInUnsignedInt / 8;
            int highBits = 0xFFFFFFFF << (bitsInUnsignedInt - oneEighth);
            int hash = 0;
            int test = 0;

            for (int i = 0; i < str.length(); i++) {
                hash = (hash << oneEighth) + str.charAt(i);

                if ((test = hash & highBits) != 0) {
                    hash = ((hash ^ (test >> threeQuarters)) & (~highBits));
                }
            }

            return hash & 0x7FFFFFFF;
        }

        /**
         * ELF算法
         *
         * @param str 字符串
         * @return hash值
         */
        public static int elfHash(String str) {
            int hash = 0;
            int x = 0;

            for (int i = 0; i < str.length(); i++) {
                hash = (hash << 4) + str.charAt(i);
                if ((x = (int) (hash & 0xF0000000L)) != 0) {
                    hash ^= (x >> 24);
                    hash &= ~x;
                }
            }

            return hash & 0x7FFFFFFF;
        }

        /**
         * BKDR算法
         *
         * @param str 字符串
         * @return hash值
         */
        public static int bkdrHash(String str) {
            int seed = 131; // 31 131 1313 13131 131313 etc..
            int hash = 0;

            for (int i = 0; i < str.length(); i++) {
                hash = (hash * seed) + str.charAt(i);
            }

            return hash & 0x7FFFFFFF;
        }

        /**
         * SDBM算法
         *
         * @param str 字符串
         * @return hash值
         */
        public static int sdbmHash(String str) {
            int hash = 0;

            for (int i = 0; i < str.length(); i++) {
                hash = str.charAt(i) + (hash << 6) + (hash << 16) - hash;
            }

            return hash & 0x7FFFFFFF;
        }

        /**
         * DJB算法
         *
         * @param str 字符串
         * @return hash值
         */
        public static int djbHash(String str) {
            int hash = 5381;

            for (int i = 0; i < str.length(); i++) {
                hash = ((hash << 5) + hash) + str.charAt(i);
            }

            return hash & 0x7FFFFFFF;
        }

        /**
         * DEK算法
         *
         * @param str 字符串
         * @return hash值
         */
        public static int dekHash(String str) {
            int hash = str.length();

            for (int i = 0; i < str.length(); i++) {
                hash = ((hash << 5) ^ (hash >> 27)) ^ str.charAt(i);
            }

            return hash & 0x7FFFFFFF;
        }

        /**
         * AP算法
         *
         * @param str 字符串
         * @return hash值
         */
        public static int apHash(String str) {
            int hash = 0;

            for (int i = 0; i < str.length(); i++) {
                hash ^= ((i & 1) == 0) ? ((hash << 7) ^ str.charAt(i) ^ (hash >> 3)) : (~((hash << 11) ^ str.charAt(i) ^ (hash >> 5)));
            }

            // return (hash & 0x7FFFFFFF);
            return hash;
        }

        /**
         * TianL Hash算法
         *
         * @param str 字符串
         * @return Hash值
         */
        public static long tianlHash(String str) {
            long hash = 0;

            int iLength = str.length();
            if (iLength == 0) {
                return 0;
            }

            if (iLength <= 256) {
                hash = 16777216L * (iLength - 1);
            } else {
                hash = 4278190080L;
            }

            int i;

            char ucChar;

            if (iLength <= 96) {
                for (i = 1; i <= iLength; i++) {
                    ucChar = str.charAt(i - 1);
                    if (ucChar <= 'Z' && ucChar >= 'A') {
                        ucChar = (char) (ucChar + 32);
                    }
                    hash += (3 * i * ucChar * ucChar + 5 * i * ucChar + 7 * i + 11 * ucChar) % 16777216;
                }
            } else {
                for (i = 1; i <= 96; i++) {
                    ucChar = str.charAt(i + iLength - 96 - 1);
                    if (ucChar <= 'Z' && ucChar >= 'A') {
                        ucChar = (char) (ucChar + 32);
                    }
                    hash += (3 * i * ucChar * ucChar + 5 * i * ucChar + 7 * i + 11 * ucChar) % 16777216;
                }
            }
            if (hash < 0) {
                hash *= -1;
            }
            return hash;
        }

        /**
         * JAVA自己带的算法
         *
         * @param str 字符串
         * @return hash值
         */
        public static int javaDefaultHash(String str) {
            int h = 0;
            int off = 0;
            int len = str.length();
            for (int i = 0; i < len; i++) {
                h = 31 * h + str.charAt(off++);
            }
            return h;
        }

        /**
         * 混合hash算法,输出64位的值
         *
         * @param str 字符串
         * @return hash值
         */
        public static long mixHash(String str) {
            long hash = str.hashCode();
            hash <<= 32;
            hash |= fnvHash(str);
            return hash;
        }

        /**
         * 根据对象的内存地址生成相应的Hash值
         *
         * @param obj 对象
         * @return hash值
         * @since 4.2.2
         */
        public static int identityHashCode(Object obj) {
            return System.identityHashCode(obj);
        }

        /**
         * MurmurHash算法32-bit实现
         *
         * @param data 数据
         * @return hash值
         * @since 4.3.3
         */
        public static int murmur32(byte[] data) {
            int length = data.length;
            int hash = DEFAULT_SEED;
            final int nblocks = length >> 2;

            // body
            for (int i = 0; i < nblocks; i++) {
                int i_4 = i << 2;
                int k = (data[i_4] & 0xff) //
                        | ((data[i_4 + 1] & 0xff) << 8) //
                        | ((data[i_4 + 2] & 0xff) << 16) //
                        | ((data[i_4 + 3] & 0xff) << 24);

                // mix functions
                k *= C1_32;
                k = Integer.rotateLeft(k, R1_32);
                k *= C2_32;
                hash ^= k;
                hash = Integer.rotateLeft(hash, R2_32) * M_32 + N_32;
            }

            // tail
            int idx = nblocks << 2;
            int k1 = 0;
            switch (length - idx) {
                case 3:
                    k1 ^= data[idx + 2] << 16;
                case 2:
                    k1 ^= data[idx + 1] << 8;
                case 1:
                    k1 ^= data[idx];

                    // mix functions
                    k1 *= C1_32;
                    k1 = Integer.rotateLeft(k1, R1_32);
                    k1 *= C2_32;
                    hash ^= k1;
            }

            // finalization
            hash ^= length;
            hash ^= (hash >>> 16);
            hash *= 0x85ebca6b;
            hash ^= (hash >>> 13);
            hash *= 0xc2b2ae35;
            hash ^= (hash >>> 16);

            return hash;
        }

        /**
         * MurmurHash算法64-bit实现
         *
         * @param data 数据
         * @return hash值
         * @since 4.3.3
         */
        public static long murmur64(byte[] data) {
            return hash64(data);
        }

        private static long hash64(byte[] data) {
            return 0;
        }

        /**
         * MurmurHash算法128-bit实现
         *
         * @param data 数据
         * @return hash值
         * @since 4.3.3
         */
        public static long[] murmur128(byte[] data) {
            return hash128(data);
        }

        private static long[] hash128(byte[] data) {
            return new long[0];
        }

}

备注

使用需要导入maven或jar包

        <!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.12</version>
        </dependency>

用途探讨
文件、图片等数据的标识码:
对文件进行md5加密,得到一个唯一的文件摘要,把摘要存储,之后再次上传文件时前端先计算摘要,如果文件的摘要在后端发现重复的,那么就不进行上传。这样可以为服务器节省大量的硬盘资源。这主要利用了哈希加密的压缩性、唯一性;
密码加密存储:
密码如果明文存储到数据库是不安全的,这时可以在存储之前先用sha1或者sha256等算法进行加密,加密后进行存储,利用哈希加密的不可逆性,保证了密码存储的安全性,防止密码泄露。
生成数字签名、防止篡改:
所谓的签名,指的是对于一份数据,拥有独一无二的标识=》这标识就是数字签名。生成数字签名的方法就是用md5等哈希算法生成摘要,并将摘要公开,对方获得数据之后,通过再次进行哈希生成摘要后对比公开摘要,就可以确认文件是不是原始的那份,而不是被篡改过的;这主要利用了哈希加密的高灵敏性,消息M的任何改变都会导致哈希值H(M)发生改变。即如果输入有微小不同,哈希运算后的输出一定不同。
防止查表法
所谓查表法针对的就是md5、sha1等哈希加密算法,黑客预先收集一些常用的密码存入表中,预先对表中的密码计算md5、sha1值,之后就可以通过对比表和数据库的密文,来获取到密码的明文;防止查表法的方式有加盐、或者使用HMAC这样的密钥哈希算法,从而使得查表法失效。
参考&引用
通俗易懂的哈希算法讲解
https://blog.csdn.net/zongyue_wang/article/details/81947142
MD5
https://baike.baidu.com/item/MD5/212708?fr=aladdin
SHA-1
https://baike.baidu.com/item/SHA-1/1699692?fromtitle=SHA1&fromid=8812671&fr=aladdin
散列算法:SHA-1,SHA-2和SHA-256之间的区别(下)
https://www.jianshu.com/p/68c664b663f4
hmac
https://baike.baidu.com/item/hmac/7307543?fr=aladdin
HMAC的图解
https://blog.csdn.net/chengqiuming/article/details/82822933
————————————————
版权声明:本文为CSDN博主「衡与墨」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/le_17_4_6/article/details/103022176

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值