Go实战 | url和base64编码原理及应用

2023-01-31 16:34:19 浏览数 (1)

大家好,我是渔夫子。今天跟大家聊聊在实际工作中遇到的对密文进行base64编码和url转义的一个案例。

01 背景

最近在工作中有这样一个场景,有一个url,里面需要带着一个价格的参数进行调用。价格是比较敏感的数据,所以需要对价格进行加密传输,采用GCM对称加密方式。但加密后的密文中有不可见的字符,在url中不能传输。所以要把所有的密文字符变成可见,所以使用到了base64编码。在url传输,为了能够在url中安全的传输(所谓安全传输就是密文中不能存在url标准中已有明确定义的字符),所以又对base64编码进行了url编码, 传输的url如下: http://localhost?price=JuictpX63BaqJlg3:lJ2IoBW7niWCkLnL83tYs4Mp

price密文加密及编码过程如下:

代码语言:javascript复制
encrypt_key := "jSUYHjkt7WTNx/XjLduwiD xwJNN97dNgVE1M0y6Nk8="
plainText := "10"
cipherText := Encrypt(encrypt_key, plainText)

func Encrypt(encrypt_key string, plainText string) string {
    key, _ := base64.StdEncoding.DecodeString(encrypt_key)
    encryptBlock, _ := aes.NewCipher(key)

    aesGcm, _ := cipher.NewGCM(encryptBlock)
    nonce := make([]byte, 12)

    _, _ = io.ReadFull(rand.Reader, nonce)

    seal := aesGcm.Seal(nil, nonce, []byte(plainText), nil)    
    // 这里打印的字符串是乱码
    fmt.Println("seal:", string(seal))

    cipherText := base64.StdEncoding.EncodeToString(nonce) ":" base64.StdEncoding.EncodeToString(seal)  
    //这里打印出的字符串包含 / 字符,该字符是url中用来分隔路径的
    fmt.Println("iv:content:", cipherText)    
    
    // 这里对base64进行编码,转换成web安全的字符串
    cipherText = url.QueryEscape(cipherText)
    fmt.Println("query escape:", cipherText)

    return cipherText
 }
代码语言:javascript复制

在第16行输出原始的密文,我们会看到是一堆乱码,如下:

再看第20行经过base64编码的输出:

变成了可见字符。但还有=号和 / 符号。

再看第24行,经过url转义的字符串:

这个字符串才是最终能被url安全传输的字符串。

下面我们就来分析一下为什么要对密文进行base64编码和url转义呢。

02 什么是base64编码?

base64编码是将二进制字节转换成文本的一种编码方式。该编码方式是将二进制字节转换成可打印的asc码。就是先预定义一个可见字符的编码表,参考RFC4648文档。然后将原字符串的二进制字节序列以每6位为一组进行分组,然后再将每组转换成十进制对应的数字,再根据该数字从预定义的编码表中找到对应的字符,最终组成的字符串就是经过base64编码的字符串。

我们以“golang”字符串为例进行说明。“golang”对应的二进制编码如下:

[01100111 01101111 01101100 01100001 01101110 01100111]

因为是英文字母,所以每个字母对应一个1字节,每个字节有8位二进制。而base64的编码方式是将这些字节序列重新按6位一组进行分组,分组如下:

[011001 110110 111101 101100 011000 010110 111001 100111]

然后将每组再转换成十进制就是如下:

[25 54 61 44 24 22 57 39]

最后根据十进制的数字从base64编码表中依次找到对应的字母如下:

[Z 2 9 s Y W 5 n]

最终经过base64编码得到的字符串就是:Z29sYW5n

这里要重点注意,base64既不是对数据进行压缩,也不是对数据进行加密,而是一种编码。编码是信息从一种形式或格式转换为另一种形式的过程。

03 为什么要用base64编码

由base64的编码原理可知,base64是将二进制字节流编码成可见的ascii码字符。也就是可以将非ascii码字符编码成可见的ascii字符,以适应某些系统中只能处理可见ascii字符的场景。

base64的初衷,是为了满足电子邮件中不能直接使用非ASCII码字符的规定。因为电子邮件是基于SMTP协议(Simple Mail Transfer Protocal 简单文件传输协议)来发送邮件的。而该协议是基于文本的协议,也就是说只能传输可见的文本协议。所以,如果要基于SMTP协议传输一张图片,图片是以二进制流存储的,这时就可以使用base64编码先对图片对齐进行编码,转换成可见的ascii文本,然后再基于SMTP协议来传输了。

还有一种常用的场景就是在http协议中传输文本信息,对传输的内容进行base64编码,可以将url协议中的不安全字符(主要指url协议中保留的关键字,例如冒号、换行符或其他二进制值)编码成安全的字符以便进行可靠的进行传输。在后面的部分我们会讲到在Go中使用的base64.URLEncoding字符编码。

04 url编码

在日常的开发中,我们还会经常遇到对要传输的url进行转义的操作。url中能出现的字符是有标准规定的,依据是RFC3986文档。该标准规定了url中只能出现包含英文字母(a-zA-Z)、数字(0-9)、- _ . ~ 4个特殊字符以及所有保留字符。编码的目的是为了在url传输中避免出现歧义。

哪些字符需要编码呢?

1、除了url标准中规定的能出现的字符及保留字符都需要编码。比如中文。这种是浏览器自动编码的。

2、在url中需要传输url标准中保留字符时也需要编码,这种主要是为了避免歧义。需要被转义的关键词列表如下:

'$', '&', ' ', ',', '/', ':', ';', '=', '?', '@'

这些字符都是有明确含义的,类似于编程语言中的关键词。比如&符号是url标准中规定的分隔查询参数的分隔符,?号是用来分隔路径和查询段的,=号是查询中用于分隔key和value的等。所以如果要在查询参数中传递&符号,就需要在传输之前就对其进行编码。例如:我们需要传递一个redirect参数,其值是http://www.baidu.com/search?query=go学堂&name=渔夫子。

http://localhost?redirect=http://www.baidu.com/search?query=go学堂&name=渔夫子

这里就需要对redirect的参数值进行编码,否则name=公众号将会被认为是http://localhost?的查询参数,而非redirect的值。

如何编码?

使用的是百分号编码,即一个百分号加上字符对应的二进制序列的十六进制的表示。例如,&符号在ASCII表中对应的二进制是 00100110,对应的十六进制是26,所以在进行url转码时&符号会被转成&。

如果是中文,在Go语言中是按照UTF8的编码方式的字节序列进行转码的。比如,字符“中”,utf8编码的字节序列是:[11100100 10111000 10101101],每个字节对应的十六进制是 E4 B8 AD,最终的url编码则是中。当然有的语言中,在对字符进行转义的时候可以指定对应的编码方式,那么在解码的时候也需要使用相应的编码进行解码。

05 为什么做了base64编码后还需要进行url编码?

在上述示例中 我们看到,首先对密文进行了base64编码,最后在通过url传输的时候,又进行了url的编码。为什么呢?因为base64的标准编码表中有url编码标准中的保留字符: 和/。这两个字符对于url来说是有明确含义的,为了避免歧义,所以还需要对base64编码进行url编码,也可以叫做web安全的url。

另外,在base64编码中实际上还有一套关于url的编码方式,其编码表是将 和/两个字符分别用连字符“-”和下划线 “_” 替代。所以如果base64编码需要在url中传输时,也可以直接使用base64的url编码。在Go中使用的是base64.URLEncoding结构体进行编码即可。同样,在解码时也需要使用对应的编码方式进行解码。

06 总结

本文结合示例,讲解了在实际应用中base64编码和url编码结合使用的场景。同时介绍了base64编码和url编码的规则。编码,就是按预定义的好的规则对原字符进行转换,其主要目的就是为了方便传输以及消除歧义。


你的关注,是我写下去的动力

0 人点赞