为什么要出现编码和解码
- 因为当字符串数据以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)处理函数中。
```go
package main
if err := router.Run();err != nil {
log.Println("something error");
}
package gin
func (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 http
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
通过上面这个过程我们可以了解到gin和http通信框架建立联系是通过engine *Engine实现的,同时ListenAndServer要求传入一个handler类型的对象,而该对象定义如下:
package http
type 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 gin
type 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、html
Accepted []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 gin
func (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.RawPath
unescape = engine.UnescapePathValues
}
//判断是否需要移除多余的分隔符"/"
if engine.RemoveExtraSlash {
rPath = cleanPath(rPath)
}
t := engine.trees
for 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.handlers
c.Params = value.params
c.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方法不匹配,而路径匹配则返回405
if 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.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
}
}
}
//如果都找不到路由则返回404
c.handlers = engine.allNoRoute
serveError(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)