为什么要出现编码和解码
- 因为当字符串数据以url形式传递给服务器时,字符串中是不允许出现空格和特殊字符的
 - 因为url对字符有限制,比如把一个邮箱放进url, 就需要使用urlencode参数,因为url中不能包含@字符
 - url转义其实也只是为了符合url规范而已,因为在标准的url规范中中文和很多的字符是不允许出现在url中
 
哪些字符是需要转化的?
- ASCII的控制字符:这些字符都是不可打印的,自然需要进行转化
 - 一些非法的ASCII字符:这些字符自然是非法的字符范围,转化也是理所当然的
 - 一些保留字符:很明显最常见的就是“&”了
 - 一些不安全的字符,为了防止引起歧义,空格需要被转为为“%20”,等号需要被转化为“%3D”
 - 和字符编码无关:通过urlencode的转化规则和目的,我们很容易看出,urlencode是基于字符编码的。同样一个汉字,不同的编码类型,肯定对应不同的urlcode的串。gbk编码的库有gbk的encode结果。urlencode只是为了url中一些非ascii字符,可以被正确无误的被传输,至于使用哪种编码,就不是encode所关心和解决的问题了。需要具体的业务系统去识别
golang中通用的url encode和url decode
```go package main 
import( “fmt” “net/url” )
func main() { var urlStr string = “傻了吧:%:%@163& .html.html” escapeUrl := url.QueryEscape(urlStr) fmt.Println(“编码:”,escapeUrl)
enEscapeUrl, _ := url.QueryUnescape(escapeUrl)fmt.Println("解码:",enEscapeUrl)
} //编码: %E5%82%BB%E4%BA%86%E5%90%A7%3A%25%3A%25%40163%26+.html.html //解码: 傻了吧:%:%@163& .html.html
<a name="y1MmQ"></a>## gin 框架如何设置url decode首先,需要对gin框架有一个基本了解。gin是使用go语言编写的高性能的web服务。根据官方的测试,性能是httprouter的40倍左右。<a name="wJBLb"></a>### gin是如何解析客户端发送请求中的参数的事实上,gin也是基于http包封装来实现网络通信,底层仍然使用http.ListenAndServer来创建的监听端口和服务。只不过将接收到的数据解析为gin的context上下文后,最终传递到type handlerFunc func(*Context)处理函数中。```gopackage mainif err := router.Run();err != nil {log.Println("something error");}
package ginfunc (engine *Engine) Run(addr ...string) (err error) {defer func() { debugPrintError(err) }()address := resolveAddress(addr)debugPrint("Listening and serving HTTP on %s\n", address)err = http.ListenAndServe(address, engine)return}
package httpfunc ListenAndServe(addr string, handler Handler) error {server := &Server{Addr: addr, Handler: handler}return server.ListenAndServe()}
通过上面这个过程我们可以了解到gin和http通信框架建立联系是通过engine *Engine实现的,同时ListenAndServer要求传入一个handler类型的对象,而该对象定义如下:
package httptype Handler interface {ServeHTTP(ResponseWriter, *Request)}
ServeHTTP的参数很明显,是ResponseWriter和*Request,请求和响应流。这两个数据通过该接口传递到gin框架内部(这里不是很理解,需要再看一下源码)。
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {//从连接池中取出一个上下文对象c := engine.pool.Get().(*Context)//将上下文对象中的响应流设置为传入的参数c.writermem.reset(w)//将上下文对象中请求数据结构设置为传入参数c.Request = req//初始化上下文对象c.reset()//正式处理请求engine.handleHTTPRequest(c)//使用完毕后放回连接池engine.pool.Put(c)}
服务处理
再正式开始了解这个处理过程之前,我们先来了解一下Context这个贯穿整个gin框架的上下文对象,在c/s的通信过程中所有的数据都保存在这个对象中。
package gintype Context struct {//响应输出流(私有,供框架内部数据写出)writermem responseWriter//客户端发送的所有信息都保存在这个对象里面Request *http.Request//响应输出流(公有,供给处理函数写出)// 在初始化后,由writermem克隆而来的Writer ResponseWriter//保存解析得到的参数,路径中的REST参数Params Params//该请求对应的处理函数链,从树节点中获取handlers HandlersChain//记录已经被处理的函数个数index int8//当前请求的完整路径fullPath string//Gin的核心引擎engine *Engine//并发读写锁KeysMutex *sync.RWMutex//用于保存当前会话的键值对,用于不同处理函数中传递Keys map[string]interface{}//处理函数链输出的错误信息Errors errorMsgs//客户端希望接受的数据类型,如:json、xml、htmlAccepted []string//存储URL中的查询参数,如:/test?name=jhon&age=11// 这样的参数储存在这个对象里queryCache url.Values//这个用于存储POST/PATCH等提交的body中的参数formCache url.Values//用来限制第三方 Cookie,一个int值,有Strict、Lax、None// Strict:只有当前网页的 URL 与请求目标一致,才会带上 Cookie// Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,// 但是导航到目标网址的 Get 请求除外// 设置了Strict或Lax以后,基本就杜绝了 CSRF 攻击sameSite http.SameSite}
在了解完Context之后,我们来进入正式的数据解析过程:
package ginfunc (engine *Engine) handleHTTPRequest(c *Context) {//获取客户端的http请求方法httpMethod := c.Request.Method//获取请求的URL地址,这里的URL是进过处理的rPath := c.Request.URL.Path//是否不启动字符转义unescape := false//判断是否启用原URL,未转义字符if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {rPath = c.Request.URL.RawPathunescape = engine.UnescapePathValues}//判断是否需要移除多余的分隔符"/"if engine.RemoveExtraSlash {rPath = cleanPath(rPath)}t := engine.treesfor i, tl := 0, len(t); i < tl; i++ {if t[i].method != httpMethod {continue}//首先获取到指定HTTP方法的搜索树的根节点root := t[i].root//从根节点开始搜索匹配该路径的节点value := root.getValue(rPath, c.Params, unescape)//将节点中的存储的信息,拷贝到Context上下文中if value.handlers != nil {c.handlers = value.handlersc.Params = value.paramsc.fullPath = value.fullPath//这里就是在遍历执行处理函数链// func (c *Context) Next() {// c.index++// for c.index < int8(len(c.handlers)) {// c.handlers[c.index](c)// c.index++// }// }c.Next()//写出响应状态码c.writermem.WriteHeaderNow()return}//如果没有找到对应的匹配节点,则考虑是否是以下的特殊情况if httpMethod != "CONNECT" && rPath != "/" {//如果启动自动重定向,删除最后的"/"并重定向if value.tsr && engine.RedirectTrailingSlash {redirectTrailingSlash(c)return}//启动路径修复后,当/../foo找不到匹配路由时,// 会自动删除..部分路由,然后重新匹配直到找到匹配路由,并重定向if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {return}}break}//是HTTP方法不匹配,而路径匹配则返回405if engine.HandleMethodNotAllowed {for _, tree := range engine.trees {if tree.method == httpMethod {continue}if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {c.handlers = engine.allNoMethodserveError(c, http.StatusMethodNotAllowed, default405Body)return}}}//如果都找不到路由则返回404c.handlers = engine.allNoRouteserveError(c, http.StatusNotFound, default404Body)}
这篇博客的重点出现了,unescape := false, 那就是是否不启动字符转义(非常拗口),这里配置的是false,也就是说默认是开启字符转义的。而且一般来说,UseRawPath一般设置为false。
神奇的情况出现了,在一个项目中:
token := c.Query("x-auth-token")
这个token是一个base64 encode 过的字符串(结尾中加上了==),理论上来说token默认是会url decode的,而且在前几个部署的版本中也是OK的。但是在某一个时间点之后,%3D并没有decode为==,看了源码,还是没有找到原因。
没有办法,为了提高代码的健壮性,更合适的方法就是将token再做一个url decode:
unescapeToken, _ := url.QueryUnescape(token)
