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

355 阅读8分钟

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

关注微信公众号”Go学堂“,回复 mistakes ,领取 Go_100_mistakes:How to avoid them 原版pdf文档。

背景

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

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

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
}

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

再看低23行经过base64编码的输出: 密文-02-base64.jpeg

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

再看第27行,经过url转义的字符串: 密文-03-url.jpeg

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

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

什么是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既不是对数据进行压缩,也不是对数据进行加密,而是一种编码。编码是信息从一种形式或格式转换为另一种形式的过程。

为什么要用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字符编码。

url编码

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

哪些字符需要编码呢?

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

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

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

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

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

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

如何编码?

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

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

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

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

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

总结

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