Base64编码表

标准的Base64并不适合直接放在URL里传输,因为URL编码器会把标准Base64中的“/”和“+”字符变为形如“%XX”的形式,而这些“%”号在存入数据库时还需要再进行转换,因为ANSI SQL中已将“%”号用作通配符。
为解决此问题,可采用一种用于URL的改进Base64编码,它在末尾填充’=’号,并将标准Base64中的“+”和“/”分别改成了“-”和“_”,这样就免去了在URL编解码和数据库存储时所要作的转换,避免了编码信息长度在此过程中的增加,并统一了数据库、表单等处对象标识符的格式。
另有一种用于正则表达式的改进Base64变种,它将“+”和“/”改成了“!”和“-”,因为“+”,“*”以及前面在IRCu中用到的“[”和“]”在正则表达式中都可能具有特殊含义。
此外还有一些变种,它们将“+/”改为“-”或“.”(用作编程语言中的标识符名称)或“.-”(用于XML中的Nmtoken)甚至“_:”(用于XML中的Name)。
Base64要求把每三个8Bit的字节转换为四个6Bit的字节(3*8 = 4*6 = 24),然后把6Bit再添两位高位0,组成四个8Bit的字节,也就是说,转换后的字符串理论上将要比原来的长1/3。
规则

关于这个编码的规则:

①.把3个字符变成4个字符。
②每76个字符加一个换行符。
③.最后的结束符也要处理。
例子(1)
转换前 11111111, 11111111, 11111111 (二进制)
转换后 00111111, 00111111, 00111111, 00111111 (二进制)
上面的三个字节是原文,下面的四个字节是转换后的Base64编码,其前两位均为0。
转换后,我们用一个码表来得到我们想要的字符串(也就是最终的Base64编码),这个表是这样的:(摘自RFC2045)

转换表

索引对应字符索引对应字符索引对应字符索引对应字符
0A17R34i51z
1B18S35j520
2C19T36k531
3D20U37l542
4E21V38m553
5F22W39n564
6G23X40o575
7H24Y41p586
8I25Z42q597
9J26a43r608
10K27b44s619
11L28c45t62+
12M29d46u63/
13N30e47v
14O31f48w
15P32g49x
16Q33h50y

//自己完成算法实现
        /// <summary>
        ///     Base64加密
        /// </summary>
        /// <param name="Message"></param>
        /// <returns></returns>
        public string Base64Code(string Message)
        {
            char[] Base64Code =
            {
                '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', '+', '/', '='
            };
            byte empty = 0;
            var byteMessage = new ArrayList(Encoding.Default.GetBytes(Message));
            StringBuilder outmessage;
            int messageLen = byteMessage.Count;
            //将字符分成3个字节一组,如果不足,则以0补齐
            int page = messageLen/3;
            int use = 0;
            if ((use = messageLen%3) > 0)
            {
                for (int i = 0; i < 3 - use; i++)
                    byteMessage.Add(empty);
                page++;
            }
            //将3个字节的每组字符转换成4个字节一组的。3个一组,一组一组变成4个字节一组
            //方法是:转换成ASCII码,按顺序排列24位数据,再把这24位数据分成4组,即每组6位。再在每组的的最高位前补两个0凑足一个字节。
            outmessage = new StringBuilder(page*4);
            for (int i = 0; i < page; i++)
            {
                //取一组3个字节的组
                var instr = new byte[3];
                instr[0] = (byte) byteMessage[i*3];
                instr[1] = (byte) byteMessage[i*3 + 1];
                instr[2] = (byte) byteMessage[i*3 + 2];
                //六个位为一组,补0变成4个字节
                var outstr = new int[4];
                //第一个输出字节:取第一输入字节的前6位,并且在高位补0,使其变成8位(一个字节)
                outstr[0] = instr[0] >> 2;
                //第二个输出字节:取第一输入字节的后2位和第二个输入字节的前4位(共6位),并且在高位补0,使其变成8位(一个字节)
                outstr[1] = ((instr[0] & 0x03) << 4) ^ (instr[1] >> 4);
                //第三个输出字节:取第二输入字节的后4位和第三个输入字节的前2位(共6位),并且在高位补0,使其变成8位(一个字节)
                if (!instr[1].Equals(empty))
                    outstr[2] = ((instr[1] & 0x0f) << 2) ^ (instr[2] >> 6);
                else
                    outstr[2] = 64;
                //第四个输出字节:取第三输入字节的后6位,并且在高位补0,使其变成8位(一个字节)
                if (!instr[2].Equals(empty))
                    outstr[3] = (instr[2] & 0x3f);
                else
                    outstr[3] = 64;
                outmessage.Append(Base64Code[outstr[0]]);
                outmessage.Append(Base64Code[outstr[1]]);
                outmessage.Append(Base64Code[outstr[2]]);
                outmessage.Append(Base64Code[outstr[3]]);
            }
            return outmessage.ToString();
        }


        /// <summary>
        ///     Base64解密
        /// </summary>
        /// <param name="Message"></param>
        /// <returns></returns>
        public string Base64Decode(string Message)
        {
            if ((Message.Length%4) != 0)
            {
                throw new ArgumentException("不是正确的BASE64编码,请检查。", "Message");
            }
            if (!Regex.IsMatch(Message, "^[A-Z0-9/+=]*$", RegexOptions.IgnoreCase))
            {
                throw new ArgumentException("包含不正确的BASE64编码,请检查。", "Message");
            }
            string Base64Code = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
            int page = Message.Length/4;
            var outMessage = new ArrayList(page*3);
            char[] message = Message.ToCharArray();
            for (int i = 0; i < page; i++)
            {
                var instr = new byte[4];
                instr[0] = (byte) Base64Code.IndexOf(message[i*4]);
                instr[1] = (byte) Base64Code.IndexOf(message[i*4 + 1]);
                instr[2] = (byte) Base64Code.IndexOf(message[i*4 + 2]);
                instr[3] = (byte) Base64Code.IndexOf(message[i*4 + 3]);
                var outstr = new byte[3];
                outstr[0] = (byte) ((instr[0] << 2) ^ ((instr[1] & 0x30) >> 4));
                if (instr[2] != 64)
                {
                    outstr[1] = (byte) ((instr[1] << 4) ^ ((instr[2] & 0x3c) >> 2));
                }
                else
                {
                    outstr[2] = 0;
                }
                if (instr[3] != 64)
                {
                    outstr[2] = (byte) ((instr[2] << 6) ^ instr[3]);
                }
                else
                {
                    outstr[2] = 0;
                }
                outMessage.Add(outstr[0]);
                if (outstr[1] != 0)
                    outMessage.Add(outstr[1]);
                if (outstr[2] != 0)
                    outMessage.Add(outstr[2]);
            }
            var outbyte = (byte[]) outMessage.ToArray(Type.GetType("System.Byte"));
            return Encoding.Default.GetString(outbyte);
        }

 //直接使用.NET中的的库类函数

        ///<summary>
        ///Base64加密
        ///</summary>
        ///<param name="Message"></param>
        ///<returns></returns>
        public string Base64Code(string Message)
        {
            byte[] bytes = Encoding.Default.GetBytes(Message);
            return Convert.ToBase64String(bytes);
        }
        ///<summary>
        ///Base64解密
        ///</summary>
        ///<param name="Message"></param>
        ///<returns></returns>
        public string Base64Decode(string Message)
        {
            byte[] bytes = Convert.FromBase64String(Message);
            return Encoding.Default.GetString(bytes);
        }
<think>我们已知标准的Base64编码表是"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",但用户希望了解如何自定义Base64编码表。 根据引用[2]中的描述,Base64编码原理是用64个可打印字符表示二进制数据,每6个比特对应一个字符。因此,自定义Base64编码表的关键在于提供一个包含64个可打印字符的字符串,用来替换标准编码表。 自定义Base64编码表的步骤: 1. 确定一个包含64个可打印字符的字符串作为自定义编码表。注意:字符必须是可打印的,并且不能有重复。 2. 编码过程与标准Base64相同,只是将标准编码表替换为自定义的编码表- 将输入数据按每3个字节(24位)分组,每组分为4个6位的单元。 - 如果最后一组不足3个字节,则用0补足至6位的倍数,然后在编码后的字符串末尾添加相应数量的'='(也可以自定义填充字符,但通常保留'='作为填充)。 3. 解码过程则使用相同的自定义编码表,将每个字符反向映射回6位的值,然后重新组合成原始数据(注意忽略填充字符'=')。 注意:自定义编码表必须保证每个字符的唯一性,否则会出现解码错误。 示例:假设我们自定义一个编码表,将标准编码表中的"+/"替换成"-_",则编码表为: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" 这样,我们就实现了自定义编码表。但需要注意的是,如果自定义编码表与标准不同,那么编码后的字符串只能由同样知道该自定义编码表的解码程序才能正确解码。 引用[2]中提到,在编码过程中,当数据不足3字节时,会补0并转换为对应的Base64字符,然后根据补零的字节数(1或2个字节)在末尾添加1个或2个'='。在自定义编码表中,填充字符通常仍然使用'=',但也可以自行定义,不过要注意解码时也要对应。 下面是Java中自定义Base64编码表的实现思路: 1. 定义自己的编码表(一个长度为64的字符串)。 2. 编码- 将输入字节数组按每3个字节分组。 - 对每一组,将3个字节(24位)分成4个6位索引,然后根据这些索引从自定义编码表中取出对应字符。 - 处理最后一组(可能不足3字节)时,补0并转换为对应的索引,然后根据实际输入字节数决定添加1个或2个'='(如果最后一组有3字节,则不需要添加;如果只有1字节,则补两个零字节,得到两个索引,并添加两个'=';如果有2字节,则补一个零字节,得到三个索引,添加一个'=')。 3. 解码: - 先去除末尾的'='(如果有),然后根据自定义编码表将字符转换为6位索引(需要构建一个反向映射的字典,即字符到索引的映射)。 - 将每4个字符(24位)重新组合成3个字节。 - 注意:原始数据末尾可能有补零的字节,需要根据原始数据长度去除(通常编码时会记录原始数据长度,或者根据'='的数量来推算)。 但需要注意,自定义编码表可能会影响兼容性。因为标准的Base64解码器使用固定的编码表,如果使用自定义编码表编码的数据,必须使用同样的自定义编码表解码。 以下是简单的Java代码示例(仅展示编码部分,解码部分类似): ```java public class CustomBase64 { // 自定义编码表,这里使用URL安全的编码表(将"+/"替换为"-_") private static final String CUSTOM_BASE64_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; public static String encode(byte[] data) { StringBuilder result = new StringBuilder(); int length = data.length; int i = 0; // 处理每3个字节(24位)一组 while (i < length) { // 将3个字节组合成一个24位的整数 int block = (data[i++] & 0xFF) << 16; if (i < length) block |= (data[i++] & 0xFF) << 8; if (i < length) block |= (data[i++] & 0xFF); // 将24位分成4个6位的索引 int index1 = (block >> 18) & 0x3F; int index2 = (block >> 12) & 0x3F; int index3 = (block >> 6) & 0x3F; int index4 = block & 0x3F; // 根据索引从自定义编码表中获取字符 result.append(CUSTOM_BASE64_TABLE.charAt(index1)); result.append(CUSTOM_BASE64_TABLE.charAt(index2)); // 处理最后一组可能不足3字节的情况 if (i - 1 < length) { result.append(CUSTOM_BASE64_TABLE.charAt(index3)); } else { result.append('='); // 第三个字符位置用填充 } if (i < length) { result.append(CUSTOM_BASE64_TABLE.charAt(index4)); } else { result.append('='); // 第四个字符位置用填充 } } return result.toString(); } // 解码方法需要自定义实现,这里省略 } ``` 注意:上述代码中,我们根据当前读取的位置判断是否已经到达数据末尾,从而决定是否添加填充字符'='。但上述代码在最后一组处理上可能不够精确,因为当只有1个字节时,我们只需要添加两个填充字符,而上述代码在只有1个字节的情况下,第一个if条件(i<length)在第二次读取时就不满足了,所以会进入else添加填充,但是此时第三个字符位置我们添加了填充,而第四个字符位置也会添加填充(因为i>=length),所以会添加两个填充。实际上,我们需要根据原始数据最后一组的字节数来确定填充字符个数。 更准确的处理方式: - 如果最后一组有1个字节(即剩余1个字节),则只有前6位有效,第二个6位只有2位(剩余4位补0),所以第三个和第四个字符位置都需要填充(两个填充字符)。 - 如果最后一组有2个字节(即剩余2个字节),则前两个6位是完整的,第三个6位只有4位(剩余2位补0),所以只需在第四个字符位置填充一个填充字符。 修正后的编码步骤(针对最后一组): - 计算最后一组实际字节数(1、2或3)。 - 当实际字节数为1时,我们只取第一个6位索引(前6位)和第二个6位索引(后2位+4个0),因为第二个6位只有2位有效,所以第三个和第四个字符位置用填充。 - 当实际字节数为2时,取三个6位索引(第一个6位,第二个6位,第三个6位只有4位有效,剩余2位补0),所以第四个字符位置用填充。 因此,我们可以这样修改: ```java public static String encode(byte[] data) { StringBuilder result = new StringBuilder(); int pad = 0; // 填充字符数,0表示无填充,1表示一个填充,2表示两个填充 for (int i = 0; i < data.length; ) { int block = (data[i++] & 0xFF) << 16; int remaining = data.length - i; if (remaining >= 1) { block |= (data[i++] & 0xFF) << 8; if (remaining >= 2) { block |= (data[i++] & 0xFF); pad = 0; // 完整3字节,无填充 } else { pad = 1; // 只有两个字节,需要填充一个 } } else { pad = 2; // 只有一个字节,需要填充两个 } // 分解成4个6位索引 int index1 = (block >> 18) & 0x3F; int index2 = (block >> 12) & 0x3F; int index3 = (block >> 6) & 0x3F; int index4 = block & 0x3F; result.append(CUSTOM_BASE64_TABLE.charAt(index1)); result.append(CUSTOM_BASE64_TABLE.charAt(index2)); if (pad < 2) { // 填充数小于2,说明第三个字符有效 result.append(CUSTOM_BASE64_TABLE.charAt(index3)); } else { result.append('='); } if (pad < 1) { // 填充数小于1,说明第四个字符有效 result.append(CUSTOM_BASE64_TABLE.charAt(index4)); } else { result.append('='); } } return result.toString(); } ``` 注意:上述代码中,我们根据剩余字节数判断填充数,然后决定是否使用编码表字符还是填充字符。但实际上,在剩余1个字节时,我们只取前两个6位(其中第二个6位只有2位有效,后面4位是补0),第三和第四个字符位置用'='填充;在剩余2个字节时,取前三个6位(第三个6位只有4位有效,后面2位补0),第四个字符位置用'='填充。 但是,上面的代码逻辑在循环中处理了每一组(每次处理3个字节,但最后一次可能不足)。不过,注意在剩余字节数判断时,我们使用`remaining = data.length - i`,在读取了一个字节后,剩余字节数为`data.length - i`(因为i已经自增了一次)。 另外,我们也可以使用Java标准库中的Base64类进行自定义编码表的实现?实际上,Java标准库(java.util.Base64)提供了标准编码,也提供了URL安全的编码(即用"-_"替换"+/"),但不允许完全自定义编码表。因此,如果需要完全自定义,则需要自己实现。 总结:自定义Base64编码表的方法就是提供自己的64个字符的字符串,然后按照Base64的算法进行编码和解码,注意处理末尾的填充。 引用[1]和[2]都提到了Base64编码的基本原理和规则,自定义编码表就是在保持算法不变的情况下更换编码表。 §§相关问题§§ 1. 自定义Base64编码表时需要注意哪些问题? 2. 如何实现自定义Base64编码表在Java中的解码? 3. 自定义Base64编码表是否会影响编码后的数据长度? 4. 为什么Base64编码中要用到填充字符'='? </think>### 自定义Base64编码表的实现方法 自定义Base64编码表的核心是**替换标准字符集**,同时保持编码算法的基本结构不变。以下是具体实现步骤和注意事项: #### 1. **字符集替换原则** - **长度固定**:必须使用**64个唯一可打印字符**(例如自定义字符集:`"!@#$%^&*()abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456"`) - **兼容性**:避免使用空格、换行符等特殊控制字符 - **解码映射**:需建立字符到索引的反向映射表(如 `{'!':0, '@':1, ...}`) #### 2. **编码实现步骤(Java示例)** ```java public class CustomBase64 { // 自定义编码表(64个字符) private static final String CUSTOM_TABLE = "!@#$%^&*()abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456"; private static final char PAD_CHAR = '='; // 填充字符 public static String encode(byte[] data) { StringBuilder sb = new StringBuilder(); int byteCount = 0; int buffer = 0; for (byte b : data) { buffer = (buffer << 8) | (b & 0xFF); byteCount++; if (byteCount == 3) { sb.append(CUSTOM_TABLE.charAt((buffer >> 18) & 0x3F)); sb.append(CUSTOM_TABLE.charAt((buffer >> 12) & 0x3F)); sb.append(CUSTOM_TABLE.charAt((buffer >> 6) & 0x3F)); sb.append(CUSTOM_TABLE.charAt(buffer & 0x3F)); buffer = 0; byteCount = 0; } } // 处理剩余字节 if (byteCount > 0) { buffer <<= (3 - byteCount) * 8; sb.append(CUSTOM_TABLE.charAt((buffer >> 18) & 0x3F)); sb.append(CUSTOM_TABLE.charAt((buffer >> 12) & 0x3F)); if (byteCount == 1) { sb.append(PAD_CHAR).append(PAD_CHAR); } else { sb.append(CUSTOM_TABLE.charAt((buffer >> 6) & 0x3F)); sb.append(PAD_CHAR); } } return sb.toString(); } } ``` #### 3. **关键实现细节** - **分组处理**:每3字节(24位)分为4组6位数据 - **索引计算**:通过移位操作获取6位索引: `(buffer >> 18) & 0x3F` 获取前6位 `(buffer >> 12) & 0x3F` 获取次6位 - **末尾填充**: - 剩余1字节 → 补两个`=`(如`"A=="`) - 剩余2字节 → 补一个`=`(如`"AB="`) - **解码要求**:必须配套实现解码器,使用相同编码表(需处理填充字符`=`) #### 4. **注意事项** 1. **字符唯一性**:编码表字符不能重复 2. **填充策略**:通常保留`=`作为填充符,但可自定义(需同步修改解码逻辑) 3. **兼容性问题**:自定义编码数据需配套解码器,无法被标准Base64解码 4. **安全增强**:可通过定期轮换编码表提升安全性 > 示例:原始数据`[0x4A, 0x41, 0x56]` > 标准编码:`"SkFW"` > 自定义编码(示例表):`"h!%f"` > 验证公式:$3 \text{字节} \times \frac{4}{3} = 4 \text{字符}$[^2]
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值