为什么要出现编码和解码

  1. 因为当字符串数据以url形式传递给服务器时,字符串中是不允许出现空格和特殊字符的
  2. 因为url对字符有限制,比如把一个邮箱放进url, 就需要使用urlencode参数,因为url中不能包含@字符
  3. 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)

  1. enEscapeUrl, _ := url.QueryUnescape(escapeUrl)
  2. fmt.Println("解码:",enEscapeUrl)

} //编码: %E5%82%BB%E4%BA%86%E5%90%A7%3A%25%3A%25%40163%26+.html.html //解码: 傻了吧:%:%@163& .html.html

  1. <a name="y1MmQ"></a>
  2. ## gin 框架如何设置url decode
  3. 首先,需要对gin框架有一个基本了解。gin是使用go语言编写的高性能的web服务。根据官方的测试,性能是httprouter的40倍左右。
  4. <a name="wJBLb"></a>
  5. ### gin是如何解析客户端发送请求中的参数的
  6. 事实上,gin也是基于http包封装来实现网络通信,底层仍然使用http.ListenAndServer来创建的监听端口和服务。只不过将接收到的数据解析为gin的context上下文后,最终传递到type handlerFunc func(*Context)处理函数中。
  7. ```go
  8. package main
  9. if err := router.Run();err != nil {
  10. log.Println("something error");
  11. }
  1. package gin
  2. func (engine *Engine) Run(addr ...string) (err error) {
  3. defer func() { debugPrintError(err) }()
  4. address := resolveAddress(addr)
  5. debugPrint("Listening and serving HTTP on %s\n", address)
  6. err = http.ListenAndServe(address, engine)
  7. return
  8. }
  1. package http
  2. func ListenAndServe(addr string, handler Handler) error {
  3. server := &Server{Addr: addr, Handler: handler}
  4. return server.ListenAndServe()
  5. }

通过上面这个过程我们可以了解到gin和http通信框架建立联系是通过engine *Engine实现的,同时ListenAndServer要求传入一个handler类型的对象,而该对象定义如下:

  1. package http
  2. type Handler interface {
  3. ServeHTTP(ResponseWriter, *Request)
  4. }

ServeHTTP的参数很明显,是ResponseWriter和*Request,请求和响应流。这两个数据通过该接口传递到gin框架内部(这里不是很理解,需要再看一下源码)。

  1. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  2. //从连接池中取出一个上下文对象
  3. c := engine.pool.Get().(*Context)
  4. //将上下文对象中的响应流设置为传入的参数
  5. c.writermem.reset(w)
  6. //将上下文对象中请求数据结构设置为传入参数
  7. c.Request = req
  8. //初始化上下文对象
  9. c.reset()
  10. //正式处理请求
  11. engine.handleHTTPRequest(c)
  12. //使用完毕后放回连接池
  13. engine.pool.Put(c)
  14. }

服务处理

再正式开始了解这个处理过程之前,我们先来了解一下Context这个贯穿整个gin框架的上下文对象,在c/s的通信过程中所有的数据都保存在这个对象中。

  1. package gin
  2. type Context struct {
  3. //响应输出流(私有,供框架内部数据写出)
  4. writermem responseWriter
  5. //客户端发送的所有信息都保存在这个对象里面
  6. Request *http.Request
  7. //响应输出流(公有,供给处理函数写出)
  8. // 在初始化后,由writermem克隆而来的
  9. Writer ResponseWriter
  10. //保存解析得到的参数,路径中的REST参数
  11. Params Params
  12. //该请求对应的处理函数链,从树节点中获取
  13. handlers HandlersChain
  14. //记录已经被处理的函数个数
  15. index int8
  16. //当前请求的完整路径
  17. fullPath string
  18. //Gin的核心引擎
  19. engine *Engine
  20. //并发读写锁
  21. KeysMutex *sync.RWMutex
  22. //用于保存当前会话的键值对,用于不同处理函数中传递
  23. Keys map[string]interface{}
  24. //处理函数链输出的错误信息
  25. Errors errorMsgs
  26. //客户端希望接受的数据类型,如:json、xml、html
  27. Accepted []string
  28. //存储URL中的查询参数,如:/test?name=jhon&age=11
  29. // 这样的参数储存在这个对象里
  30. queryCache url.Values
  31. //这个用于存储POST/PATCH等提交的body中的参数
  32. formCache url.Values
  33. //用来限制第三方 Cookie,一个int值,有Strict、Lax、None
  34. // Strict:只有当前网页的 URL 与请求目标一致,才会带上 Cookie
  35. // Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,
  36. // 但是导航到目标网址的 Get 请求除外
  37. // 设置了Strict或Lax以后,基本就杜绝了 CSRF 攻击
  38. sameSite http.SameSite
  39. }

在了解完Context之后,我们来进入正式的数据解析过程:

  1. package gin
  2. func (engine *Engine) handleHTTPRequest(c *Context) {
  3. //获取客户端的http请求方法
  4. httpMethod := c.Request.Method
  5. //获取请求的URL地址,这里的URL是进过处理的
  6. rPath := c.Request.URL.Path
  7. //是否不启动字符转义
  8. unescape := false
  9. //判断是否启用原URL,未转义字符
  10. if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
  11. rPath = c.Request.URL.RawPath
  12. unescape = engine.UnescapePathValues
  13. }
  14. //判断是否需要移除多余的分隔符"/"
  15. if engine.RemoveExtraSlash {
  16. rPath = cleanPath(rPath)
  17. }
  18. t := engine.trees
  19. for i, tl := 0, len(t); i < tl; i++ {
  20. if t[i].method != httpMethod {
  21. continue
  22. }
  23. //首先获取到指定HTTP方法的搜索树的根节点
  24. root := t[i].root
  25. //从根节点开始搜索匹配该路径的节点
  26. value := root.getValue(rPath, c.Params, unescape)
  27. //将节点中的存储的信息,拷贝到Context上下文中
  28. if value.handlers != nil {
  29. c.handlers = value.handlers
  30. c.Params = value.params
  31. c.fullPath = value.fullPath
  32. //这里就是在遍历执行处理函数链
  33. // func (c *Context) Next() {
  34. // c.index++
  35. // for c.index < int8(len(c.handlers)) {
  36. // c.handlers[c.index](c)
  37. // c.index++
  38. // }
  39. // }
  40. c.Next()
  41. //写出响应状态码
  42. c.writermem.WriteHeaderNow()
  43. return
  44. }
  45. //如果没有找到对应的匹配节点,则考虑是否是以下的特殊情况
  46. if httpMethod != "CONNECT" && rPath != "/" {
  47. //如果启动自动重定向,删除最后的"/"并重定向
  48. if value.tsr && engine.RedirectTrailingSlash {
  49. redirectTrailingSlash(c)
  50. return
  51. }
  52. //启动路径修复后,当/../foo找不到匹配路由时,
  53. // 会自动删除..部分路由,然后重新匹配直到找到匹配路由,并重定向
  54. if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
  55. return
  56. }
  57. }
  58. break
  59. }
  60. //是HTTP方法不匹配,而路径匹配则返回405
  61. if engine.HandleMethodNotAllowed {
  62. for _, tree := range engine.trees {
  63. if tree.method == httpMethod {
  64. continue
  65. }
  66. if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
  67. c.handlers = engine.allNoMethod
  68. serveError(c, http.StatusMethodNotAllowed, default405Body)
  69. return
  70. }
  71. }
  72. }
  73. //如果都找不到路由则返回404
  74. c.handlers = engine.allNoRoute
  75. serveError(c, http.StatusNotFound, default404Body)
  76. }

这篇博客的重点出现了,unescape := false, 那就是是否不启动字符转义(非常拗口),这里配置的是false,也就是说默认是开启字符转义的。而且一般来说,UseRawPath一般设置为false。

神奇的情况出现了,在一个项目中:

  1. token := c.Query("x-auth-token")

这个token是一个base64 encode 过的字符串(结尾中加上了==),理论上来说token默认是会url decode的,而且在前几个部署的版本中也是OK的。但是在某一个时间点之后,%3D并没有decode为==,看了源码,还是没有找到原因。

没有办法,为了提高代码的健壮性,更合适的方法就是将token再做一个url decode:

  1. unescapeToken, _ := url.QueryUnescape(token)

参考:
Golang之Gin框架源码解读——第三章