随笔 - 240  文章 - 1  评论 - 58  阅读 - 85万 

前言

Gin是一个用Go语言编写的web框架。它是一个类似于martini但拥有更好性能的API框架, 由于使用了httprouter,速度提高了近40倍。

如果你是性能和高效的追求者, 你会爱上Gin

Go语言里最流行的Web框架,Github上有32K+star。 基于httprouter开发的Web框架。 中文文档齐全,简单易用的轻量级框架。

 

安装

复制代码
D:\learning-gin>set GOPROXY=https://goproxy.cn
-------------------------------------------------------
D:\learning-gin>go get -u github.com/gin-gonic/gin
go: google.golang.org
/protobuf upgrade => v1.25.0 go: gopkg.in/yaml.v2 upgrade => v2.4.0 go: github.com/golang/protobuf upgrade => v1.4.3 go: github.com/ugorji/go/codec upgrade => v1.2.1 go: golang.org/x/sys upgrade => v0.0.0-20201211090839-8ad439b19e0f go: github.com/json-iterator/go upgrade => v1.1.10 go: github.com/modern-go/reflect2 upgrade => v1.0.1 go: github.com/go-playground/validator/v10 upgrade => v10.4.1 go: github.com/modern-go/concurrent upgrade => v0.0.0-20180306012644-bacd9c7ef1dd go: golang.org/x/crypto upgrade => v0.0.0-20201208171446-5f87f3452ae9
复制代码

 

Gin简单示例

复制代码
package main

import "github.com/gin-gonic/gin"

func index(c *gin.Context) {
    //返回json类型的数据,h=type H map[string]interface{}
    c.JSON(200, gin.H{"msg": "您好呀!"},
    )
}

func main() {
    //定义1个默认路由(基于httprouter的)
    router := gin.Default()
    //增加url
    router.GET("/index", index)
    //server段开始linsten运行
    router.Run("127.0.0.1:8000")

}
复制代码

 

request&response Header 

vue设置请求头中的token

this.$http.defaults.headers.common.X-token = 'sidhlmajldhbd-vue'

Gin获取请求头中的Token

token := c.Request.Header.Get("X-token")

Gin响应头设置Token

c.Header("X-token","sidhlmajldhbd-gin")

 

Gin request

我们可以通过gin的context获取到客户端请求携带的url参数、form表单、json数据、文件等。

复制代码
package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
)


type user struct {
    Name string `json:"name"`
    City string `json:"city"`
}
var person=&user{}
//从url获取参数
func urlData(c *gin.Context) {
    //name:=c.Query("name") //获取url参数,获取不到获取空字符串
     name:=c.DefaultQuery("name","zhanggen") ////获取url参数,获取不到获取默认!
    city:=c.DefaultQuery("city","bj")
    person.Name=name
    person.City=city
    c.JSON(200,person)
}

//从form表单中获取数据
func formData(c *gin.Context) {
    c.PostForm("name")
    person.Name=c.DefaultPostForm("name","Martin")
    person.City=c.DefaultPostForm("city","London")
    c.JSON(200,person)
}

//获取url地址参数
func pathData(c *gin.Context){
    person.City=c.Param("city")
    person.Name=c.Param("name")
    c.JSON(200,*person)
}
//获取json数据
func jsonData(c *gin.Context){
    c.Bind(person)
    fmt.Println("-----------------",*person)
    c.JSON(200,person)
}


func main()  {
    r:=gin.Default()
    //http://127.0.0.1:8001/user?name=zhanggen&city=beijing
    r.GET("/user",urlData)
    r.POST("/user",formData)
    //http://127.0.0.1:8001/user/bj/zhanggen
    r.GET("/user/:city/:name",pathData)
    r.POST("/user/json/",jsonData)
    r.Run(":8001")
}
复制代码

 

Gin shouldBind

默认情况下,我们需要根据客户端请求的content-type,在后端使用不同的方式获取客户端请求参数。

获取请求参还需要c.Query、c.PostForm、c.Bind、C.Param,这也太麻烦了~

shouldBind可帮助我们根据客户端request的content-type,把requestBody中数据自动绑定到后台Struct的字段,但不支持重复绑定。

BindXXX:会自动返回信息,输入无效时,在header写入状态码=400。

ShouldBindXXX:返回消息,输入无效时,不会在header写入400状态码,这时候可以自定义返回信息,在使用上相对来说更加灵活。

ShouldBindWith:

在gin 1.4 之前,重复使用ShouldBind绑定会报错EOF。

gin 1.4 之后官方提供了一个 ShouldBindBodyWith 的方法,可以支持重复绑定,原理就是将body的数据缓存了下来,但是二次取数据的时候还是得用 ShouldBindBodyWith 才行,直接用 ShouldBind 还是会报错的。

package main
 
import (
    "fmt"
    "github.com/gin-gonic/gin"
)
 
//0.contentType对应ShouldBind对应的结构体
type UserInfo struct {
    Username string `form:"username" json:"username"`
    Password string `form:"password" json:"password"`
}
 
func index(c *gin.Context) {
    requestMethod := c.Request.Method
    //1.声明1个值类型uerinfo类型的变量u
    var user UserInfo
    //2.把客户端request请求的参数和后端的结合体字段进行绑定
    err := c.ShouldBind(&user)
    if err != nil {
        c.JSON(400, gin.H{"err": err.Error()})
        return
    }
    //3.可以通过反射的方式,根据客户端request的contentType自动获取数据了
    if requestMethod == "GET" {
        fmt.Println(user)
        c.HTML(200, "index.html", gin.H{})
    }
    if requestMethod == "POST" {
        fmt.Println(user)
        c.JSON(200, gin.H{"data": "postOkay"})
    }
 
}
 
func main() {
    router := gin.Default()
    router.Static("/static", "./static")
    router.LoadHTMLGlob("templates/*")
    router.GET("/user", index)
    router.POST("/user", index)
    router.Run(":8002")
}

自定义验证的错误信息

对前端传入的参数进行校验,当验证不通过时,会给出错误的信息。

复制代码
package main

import (
    "fmt"
    "reflect"

    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator/v10"
)


type LoginDto struct {
    Username string `json:"username" binding:"required" msg:"用户名不能为空"`
    Password string `json:"password" binding:"min=3,max=6" msg:"密码长度不能小于3大于6"`
    Email    string `json:"email" binding:"email" msg:"邮箱地址格式不正确"`
}

func GetValidMsg(err error, obj interface{}) string {
    fmt.Println("sherlock")
    getObj := reflect.TypeOf(obj)
    if errs, ok := err.(validator.ValidationErrors); ok {
        for _, e := range errs {
            if f, exist := getObj.Elem().FieldByName(e.Field()); exist {
                return f.Tag.Get("msg")
            }
        }
    }
    return err.Error()
}

func main() {
    r := gin.Default()

    r.POST("/login", func(c *gin.Context) {
        userDto := &LoginDto{}
        if err := c.ShouldBindJSON(userDto); err != nil {
            c.JSON(200, gin.H{
                "status":  "err",
                "message": GetValidMsg(err, userDto),
            })

        } else {
            c.JSON(200, gin.H{
                "status":  "change",
                "message": userDto,
            })
        }
    })
    r.Run(":8080")
}
复制代码

 

Gin response

我们web开发过程中,大型项目会采用MVVM(前后端分离)的架构,小型项目会采用MTV(模板渲染)的架构。

疏通同归其目的都是完成数据驱动视图,不同的是数据驱动视图的地方不一样。

貌似web开发玩得就是这6个字,数据----》 驱动-----》视图。空谈误国,怎么才能更好的驱动视图才是关键。

MTV模式(模板渲染):后端使用模板语法也就是字符串替换的方式,在后端直接完成数据和HTML的渲染,直接返回给客户端。

MVVM(前后端分离架构):后端返回json数据,前端使用axios/ajax的方式获取到数据,使用vue等前端框架完成数据到HTML的渲染。

 

1.RESTful API

只要API程序遵循了REST风格,那就可以称其为RESTful API。

其实核心思想是1个资源对应1个URL,客户端对这1资源操作时(不同的request.get/post/put/delete方法)对应后端的增/删/改/查操作。

例如,我们现在要编写一个管理书籍的系统,我们可以查询对一本书进行查询、创建、更新和删除等操作。

我们在编写程序的时候就要设计客户端浏览器与我们Web服务端交互的方式和路径。按照经验我们通常会设计成如下模式:

请求方法URL含义
GET /book 查询书籍信息
POST /create_book 创建书籍记录
POST /update_book 更新书籍信息
POST /delete_book 删除书籍信息

我们按照RESTful API设计如下:

请求方法URL含义
GET /book 查询书籍信息
POST /book 创建书籍记录
PUT /book 更新书籍信息
DELETE /book 删除书籍信息

 

 

c.JSON响应json数据

复制代码
package main

import "github.com/gin-gonic/gin"
//结构体
type user struct {
    Name string `json:"name"`
    Age int     `json:"age"`
}
//视图函数
func perosn(c *gin.Context)  {
    var userInfor=user{Name: "张根",Age:18}
    c.JSON(200,userInfor)
}
func main(){
    r:=gin.Default()
    r.GET("/person/",perosn)
    r.Run(":8002")
}
复制代码

 

2.MVC模板渲染

如果是小型项目、历史原因、SEO优化我们使用模板渲染,Gin也是支持MTV模式的。

复制代码
package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
)

func index(c *gin.Context) {
    //3.gin 模板渲染
    c.HTML(200, "index.html", gin.H{"title": "首页", "body": "hello"})

}

func main() {
    //1.创建1个默认的路由引擎
    router := gin.Default()
    router.GET("/", index)
    //2.gin模板解析
    router.LoadHTMLGlob("templates/*")    //正则匹配templates/所有文件
    router.LoadHTMLGlob("templates/**/*") //正则匹配template/目录/所有文件
    err := router.Run(":9001")
    if err != nil {
        fmt.Println("gin启动失败", err)
    }
}
复制代码

 

3.文件上传

http请求也可以传输文件,有时候我们可以使用gin搭建1个ftp服务器。

单个文件上传

package main
 
import (
    "fmt"
    "github.com/gin-gonic/gin"
    "path"
)
 
func handleFile(c *gin.Context) {
    method := c.Request.Method
    if method == "GET" {
        c.HTML(200, "file.html", gin.H{})
    }
 
    if method == "POST" {
        //1.从客户端请求中获取文件
        fileObj, err := c.FormFile("localFile")
        if err != nil {
            c.JSON(400, gin.H{"err": err.Error()})
            return
        }
        //2.保存到服务端
        fileStorePath := path.Join("./upload/", fileObj.Filename)
        err = c.SaveUploadedFile(fileObj, fileStorePath)
        if err != nil {
            errMsg := fmt.Sprintf("文件保存失败:%s\n", err.Error())
            c.JSON(200, gin.H{"err": errMsg})
        }
        c.JSON(200, gin.H{"data": "上传成功"})
    }
}
 
func main() {
    router := gin.Default()
    router.Static("/static", "./static")
    router.LoadHTMLGlob("templates/*")
    router.GET("/file/", handleFile)
    router.POST("/file/", handleFile)
    err := router.Run(":8002")
    if err != nil {
        fmt.Println("gin启动失败", err)
        return
    }
}

 

多个文件上传

func main() {
    router := gin.Default()
    // 处理multipart forms提交文件时默认的内存限制是32 MiB
    // 可以通过下面的方式修改
    // router.MaxMultipartMemory = 8 << 20  // 8 MiB
    router.POST("/upload", func(c *gin.Context) {
        // Multipart form
        form, _ := c.MultipartForm()
        files := form.File["file"]
 
        for index, file := range files {
            log.Println(file.Filename)
            dst := fmt.Sprintf("C:/tmp/%s_%d", file.Filename, index)
            // 上传文件到指定的目录
            c.SaveUploadedFile(file, dst)
        }
        c.JSON(http.StatusOK, gin.H{
            "message": fmt.Sprintf("%d files uploaded!", len(files)),
        })
    })
    router.Run()
}

  

 

Gin模板渲染 

现在大部分都是前后端分离的架构,除了seo优化我们基本不会使用gin做模板渲染。

 

1.扩展gin模板函数

复制代码
package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "html/template"
)

func index(c *gin.Context) {
    //3.gin 模板渲染
    c.HTML(200, "index.html", gin.H{"title": "首页", "name": "Martin", "age": "hello", "url": `<a href="https://www.cnblogs.com/sss4/">主页</a>`})

}

func main() {
    //1.创建1个默认的路由引擎
    router := gin.Default()
    router.GET("/", index)
    //1.5 gin框架模板自定义模板函数
    router.SetFuncMap(template.FuncMap{
        "safe": func(safeString string) template.HTML {
            return template.HTML(safeString)
        },
    })
    //2.gin模板解析
    //router.LoadHTMLGlob("templates/*")    //正则匹配templates/所有文件
    router.LoadHTMLGlob("templates/**/*") //正则匹配template/目录/所有文件

    err := router.Run(":9001")
    if err != nil {
        fmt.Println("gin启动失败", err)
    }
}
复制代码

模板

复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{.title}}</title>

</head>
<body>
<ul>
    <li>{{.name}}</li>
    <li>{{.age}}</li>
    <li>{{.url | safe}}</li>
</ul>
</body>
</html>
复制代码

 

2.加载静态文件路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main
 
import (
    "fmt"
    "github.com/gin-gonic/gin"
    "html/template"
)
 
func index(c *gin.Context) {
    //5.gin 模板渲染
    c.HTML(200, "index.html", gin.H{"title": "首页", "name": "Martin", "age": "hello", "url": `<a href="https://www.cnblogs.com/sss4/">主页</a>`})
 
}
 
func main() {
    //1.创建1个默认的路由引擎
    router := gin.Default()
    router.GET("/", index)
    //2.加载静态文件路径 .css
    router.Static("/static","./static")
    //3. 扩展gin框架模板自定义模板函数
    router.SetFuncMap(template.FuncMap{
        "safe": func(safeString string) template.HTML {
            return template.HTML(safeString)
        },
    })
    //4.gin模板解析
    //router.LoadHTMLGlob("templates/*")    //正则匹配templates/所有文件
    router.LoadHTMLGlob("templates/**/*") //正则匹配template/目录/所有文件
 
 
    err := router.Run(":9001")
    if err != nil {
        fmt.Println("gin启动失败", err)
    }
}

 模板

复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{.title}}</title>
    <link rel="stylesheet" href="/static/dist/css/bootstrap.min.css">
    <script src="/static/jquery-3.2.1.min.js"></script>
    <script src="/static/dist/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
    <table class="table table-hover">
        <theader>
            <tr>
                <td>姓名</td>
                <td>年龄</td>
                <td>主页</td>
            </tr>
        </theader>
        <tbody>
        <tr>
            <td>{{.name}}</td>
            <td>{{.age}}</td>
            <td>{{.url|safe}}</td>
        </tr>
        </tbody>
    </table>
</div>
</body>
</html>
复制代码

 

3.Gin模板继承

html/template实现了模板的嵌套和继承,但是gin不包含此功能。但是我们使用第第三方包。github.com/gin-contrib/multitemplate

 

Gin 路由组

URL路由太多了就需要分组管理,类似Flask的蓝图、Django里面的include URL。这些都是基于反射实现的。

但是Gin框架中的路由使用的是httprouter这个库,其基本原理就是构造一个路由地址的前缀树

1.单支路由

//所有请求方式都汇聚到handleBook
router.Any("/book/", handleBook)
//处理404错误
router.NoRoute(handle404)

 

2.路由组

复制代码
    //cmdb路由组
    cmdbRouter := router.Group("/cmdb")
    {
        cmdbRouter.GET("/list/")
        cmdbRouter.GET("/hosts/")
        cmdbRouter.GET("/option/")
    }
    //工单路由组
    workOrder := router.Group("/works")
    {
        workOrder.GET("/daily/")
        cmdbRouter.GET("/momthly")
        cmdbRouter.GET("/quarterly")
    }
复制代码

 

3.路由嵌套

虽然gin的路由支持嵌套,但是出于对查询性能的考虑我们一般都会不会嵌套很多层路由。

复制代码
//cmdb路由组
    cmdbRouter := router.Group("/cmdb")
    {
        cmdbRouter.GET("/list/")
        cmdbRouter.GET("/hosts/")
        //1.cmdb的主机
        hostRouter := cmdbRouter.Group("/host")
        {
            //1.1主机的cpu
            hostRouter.GET("/cup/")
            //1.2主机的内存
            hostRouter.GET("/memory/")
            //1.3主机的硬盘
            hostRouter.GET("/disks/")
            //1.4主机运行的服务
            hostRouter.GET("/process/")
            //1.5网络流量
            hostRouter.GET("/networks/")

        }

        cmdbRouter.GET("/option/")
    }
    //2.工单路由组
    workOrder := router.Group("/works")
    {
        workOrder.GET("/daily/")
        cmdbRouter.GET("/momthly")
        cmdbRouter.GET("/quarterly")
    }
复制代码

 

Gin中间件

我们可以在不修改视图函数的前提下,利用Web框架中携带的钩子函数也就是中间件 做权限控制、登录认证、权限校验、数据分页、记录日志、耗时统计.........

注意我们的中间件不仅可以设置1个,也根据我们的业务逻辑设置N个,相当于对用户请求增加了多层过滤。

就像Python里面的多层装饰器。

 

1.中间件执行流程

 

 

由于http请求包含request、response 2个动作所以中间件是双行线,中间件的执行流程就像1个递归函数的执行过程

1
2
3
压栈: 用户---------> 认证中间件---------> 用户权限中间件---------> 错误处理中间件---------> 视图函数执行
 
出栈: 视图函数执行完毕---------> 错误处理中间件---------> 用户权限中间件---------> 认证中间件---------> 用户

 

2.控制中间件执行流程

所为的控制流程我感觉就是设计中间件这个栈里面包含的层层栈针。

我们在弹匣里装了什么样的子弹,扣动扳机时就会发射出什么子弹,这样想会更简单一些否则很容易被绕进去。

在中间件执行的过程中我们可以控制进栈和出栈流程。

 

 

以上代码执行结果:

m1 in
m2 in
m1 out

 

调用context.Next(),继续调用下一个视图函数进行压栈。(子弹装满弹匣)

调用context.Abort() 阻止继续调用后续的函数,执行完当前栈针(函数)之后出栈。(1发子弹就够了)

调用context.Abort() + return,当前位置返回,当前位置之后的代码都不需要不执行了。(1发哑弹)

 

  

3.给单个路由(url)设置中间件

当我们需要对特定的视图函数增加新功能时,可以给它增加1个中间件。

复制代码
package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "time"
)

//中间件1
func middleWare1(c *gin.Context) {
    fmt.Println("--------------I`m going through middleWare1----------")
    start := time.Now()
    c.Next() //调用后续的处理函数
    cost := time.Since(start)
    fmt.Printf("耗时----------%v\n", cost)
    c.Abort() //终止请求
}

//index handlerfunc类型的函数
func index(c *gin.Context) {
    fmt.Println("--------------I`m going through handlerfunc----------")
    c.JSON(200, gin.H{"data": "ok"})
    c.Next()

}

//中间件2
func middleWare2(c *gin.Context) {
    fmt.Println("--------------I`m going through middleWare2----------")
    c.Next()
    c.Abort() //请求终止
}

func main() {
    router := gin.Default()
    //设置中间件流程:middleWare1-----》index----》middleWare2
    router.GET("/index/", middleWare1, index, middleWare2)
    err := router.Run(":9001")
    if err != nil {
        fmt.Println("Gin启动失败", err)
    }

}
复制代码

 

4.全局注册中间件

如果我们需要每个视图函数都设置1个中间件,把这一中间件写到每个视图函数前面会非常不方便,我们可以使用use进行全局注册。

复制代码
package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
)

//中间件1
func middleWare1(c *gin.Context) {
    fmt.Println("middleWare1开始----------")
    c.Next() //调用后续的处理函数
    fmt.Println("middleWare1结束----------")
    //c.Abort() //终止请求
}

//中间件2
func middleWare2(c *gin.Context) {
    fmt.Println("middleWare2开始========")
    c.Next()
    fmt.Println("middleWare2结束========")
}

//index handlerfunc类型的函数
func index(c *gin.Context) {
    fmt.Println("index开始+++++++++")
    c.JSON(200, gin.H{"data": "ok"})
    fmt.Println("index结束+++++++++")
}

func main() {
    router := gin.Default()
    //全局注册中间件:middleWare1, middleWare2
    router.Use(middleWare1, middleWare2)
    router.GET("/index/", index)
    err := router.Run(":9001")
    if err != nil {
        fmt.Println("Gin启动失败", err)
    }

}
复制代码

输出:

验证web框架里中间件设计思想是的递归思想。

middleWare1开始----------
middleWare2开始========
index开始+++++++++
index结束+++++++++
middleWare2结束========
middleWare1结束----------

 

5.路由组注册中间件

给路由组注册中间件有2种写法

写法1:

shopGroup := r.Group("/shop", StatCost())
{
    shopGroup.GET("/index", func(c *gin.Context) {...})
    ...
}

 

写法2:

shopGroup := r.Group("/shop")
shopGroup.Use(StatCost())
{
    shopGroup.GET("/index", func(c *gin.Context) {...})
    ...
}

 

6.闭包的中间件

以上我们得知:Gin的中间件是以1种gin.HandlerFunc类型存在,在路由和路由组里进行注册。

router.GET("/index/", authMiddleWare(false), index)

那我们可以使用闭包将1个开关参数和这个handlerFunc一起包起来。实现对中间进行开关控制比较灵活。

package main
 
import (
    "fmt"
    "github.com/gin-gonic/gin"
)
 
//使用闭包函数返回,gin.HandlerFunc。可以实现对中间进行开关控制,比较灵活。
func authMiddleWare(work bool) gin.HandlerFunc {
    if work{
        //连接数据库
        //其他准备工作
        dbDB := "Martin"
        return func(c *gin.Context) {
            username := c.Query("username")
            if username == dbDB {
                c.Next()
            } else {
                c.Abort()
                c.JSON(403, gin.H{"data": "没有访问权限"})
            }
 
        }
    }
    return func(context *gin.Context) {
    }
}
 
//index视图函数
func index(c *gin.Context) {
    fmt.Println("index视图函数开始")
    c.JSON(200, gin.H{"data": "ok"})
    fmt.Println("index视图函数结束")
 
}
 
func main() {
    router := gin.Default()
    router.GET("/index/", authMiddleWare(false), index)
    err := router.Run(":9001")
    if err != nil {
        fmt.Println("Gin启动失败", err)
    }
 
}

  

7.夸中间件进行传值

中间件可以有多层,假如我们上游的中间得出的值,如何传递到下游中间件呢?。通过上下文content。

当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。以保证我们传递的值是一致的。

c.Set("username", username)
currentUser, ok := c.Get("username")

 

8.gin默认中间件

gin.Defaut生成的路由引擎,默认使用了Logger(), Recovery()的中间件。

//生成的路由引擎,默认使用了Logger(), Recovery()的中间件
 gin.Default()
router :
= gin.New()

Logger:用于记录日志

Recovery:用于保证在gin 发生错误时进程不会终止。

复制代码
package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
)

//使用闭包函数返回,gin.HandlerFunc。可以实现对中间进行开关控制,比较灵活。
func authMiddleWare(work bool) gin.HandlerFunc {
    if work {
        //连接数据库
        //其他准备工作
        dbDB := "Martin"
        return func(c *gin.Context) {
            username := c.Query("username")
            if username == dbDB {
                //1.在中间中设置值进行传递
                c.Set("username", username)
                fmt.Println("-----------", username)
                c.Next()
            } else {
                c.Abort()
                c.JSON(403, gin.H{"data": "没有访问权限"})
            }

        }
    }
    return func(context *gin.Context) {
    }
}

//index视图函数
func index(c *gin.Context) {
    //2.获取上流传递的值
    currentUser, ok := c.Get("username")
    if !ok {
        currentUser = "anonymous"
    }
    fmt.Println("index视图函数开始")
    c.JSON(200, gin.H{"data": currentUser})
    fmt.Println("index视图函数结束")

}

func main() {
    //生成的路由引擎,默认使用了Logger(), Recovery()的中间件
    gin.Default()
    //router := gin.Default()
    router := gin.New()
    router.GET("/index/", authMiddleWare(true), index)
    err := router.Run(":9001")
    if err != nil {
        fmt.Println("Gin启动失败", err)
    }

}
代码
复制代码

9.自定义日志中间件

  • 记录正常请求信息
  • 捕捉到错误信记录特定格式的日志 

Gin设置cookie

1.cookie是server端保存在browser中的用户信息

2.每客户端次访问server端时都会携带该域下的cooki信息到server端。

3.不同域名之间的cookie是不共享的。(无法跨域,所以前后端分离的项目只能使用token)

 HttpOnly:如果cookie中设置了HttpOnly属性,那么通过js脚本将无法读取到该cookie信息。

1.设置cookie

//设置cookie:key=username,value=zhanggen,maxAge=10秒,path=/,domain=127.0.0.1,secure=false,httpOnly=false
        c.SetCookie("username", currentUser.Username, 10, "/", "127.0.0.1", false, false)

2.获取cookie

 //获取cookie
 userName, err := c.Cookie("username")

 

总结

Cookie虽然在一定程度上解决了保持客户端状态的需求,但是cookie有一些缺陷。

1.Cookie保存在客户端,可能在http传输过程中被拦截、窃取。

2.即使我们对Cookie进行加密,cookie最多也只能保存4096字节(4KB)的数据。

 

Gin设置session

1.cookie和session的数据结构

复制代码
客户端cookie:
用户1的cookie
={"sessionID":"8b9c1c99-1e84-4a1f-85f9-d0216980706b"} 用户6的cookie={"sessionID":"d351b75f-042c-4eb6-978f-fc6e31178cdc"} ...........
服务端端session:
{
"8b9c1c99-1e84-4a1f-85f9-d0216980706b":{"islogin":true,"username":"用户1"}, "750e641b-23c3-4b48-ac5d-adda3a6d584a":{"islogin":true,"username":"用户2"}, "d351b75f-042c-4eb6-978f-fc6e31178cdc":{"islogin":true,"username":"用户3"}, "b2c5997d-e958-48b3-bc87-f08d5bd16e22":{"islogin":true,"username":"用户4"}, "750e641b-23c3-4b48-ac5d-adda3a6d584a":{"islogin":true,"username":"用户5"}, "d351b75f-042c-4eb6-978f-fc6e31178cdc":{"islogin":true,"username":"用户6"}, }
复制代码

1.首先客户端和服务端约定1个固定的key比如叫sessionID

2.每次客户端请求到达server端之后,server用cookie中sessionID( key in client side)对应的value做key(key in server side )获取server端对应的value. 

 

2.session实现流程

上面我们说到我们可以在浏览器的cookie信息中设置1个sessionID(唯一标识)之后浏览器每次发送http请求到server端都会携带该域名下的cookie。

改进:

session是借助客户端cookie完成服务端数据存储,唯一标识的key通常设置为Session-ID,key对应的value就是服务端的sessionID。

Session是一种在服务端保存的数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中 etc;

Gin没有像Django那样自带session中间件,这是我开源的session中间件。

基于以上总结出:

1.cookie存储来自各个域名的sessionID。

2.sessionID就是cookie中存储唯一标识的key,这个key的名字我想叫什么就叫什么叫token也行。

3.只要能确保客户端每次都携带一个唯一标识到服务端来把sessionID存储到URL参数上都行,服务端就能识别用户身份(无论是通过数据结构还是通过算法校验的方式)。

ps:

cookie的好处在于浏览器每次在请求server端时会自动携带,但是无法跨域

如果我们不把sessionID/Token放在cookie里,每次请求服务端之前都需要在客户端(vue的路由守卫)检查是否携带SessionID/token?

 

Gin设置JWT

虽然我们可以做到使用redis集群对用户session信息进行分布式存储,保证了用户session数据在后端存储的可扩展性,但是这种方案得不偿失。

1.随着用户量的增加,每次客户端请求到来时server端都要连接redis、查询sessionID会速度比较慢,也无法改变server端存储的session数据与日俱增的态势。

2.sessionID是存储在浏览器的cookie之中,无法兼容微信小程序、APP这些不支持cookie的客户端类型,所以cookie和session都会无用武之地。

3.从安全方面考虑:如果hake从浏览器的cookie中获取到了sessionID,服务器端也无法对sessionID做任何来源甄别和校验。

JWT:json web token其机制类似于session,不同之处在于sever端不再保存client的sessionID/Token,而是通过算法检验的方式计算得出用户的身份是否合法?

其实现生成和校验token的过程详见我另一篇博客

 

1. 下载依赖包 jwt-go

go get -u github.com/dgrijalva/jwt-go

2.jwt生成和校验

复制代码
// 指定加密密钥
var jwtSecret=[]byte("ssjwh")

//Claim是一些实体(通常指的用户)的状态和额外的元数据
type Claims struct{
    Username string `json:"username"`
    Password string `json:"password"`
    jwt.StandardClaims
}

// 根据用户的用户名和密码产生token
func GenerateToken(username ,password string)(string,error){
    //设置token有效时间
    nowTime:=time.Now()
    expireTime:=nowTime.Add(3*time.Hour)

    claims:=Claims{
        Username:       username,
        Password:       password,
        StandardClaims: jwt.StandardClaims{
            // 过期时间
            ExpiresAt:expireTime.Unix(),
            // 指定token发行人
            Issuer:"zhanggen",
        },
    }

    tokenClaims:=jwt.NewWithClaims(jwt.SigningMethodHS256,claims)
    //该方法内部生成签名字符串,再用于获取完整、已签名的token
    token,err:=tokenClaims.SignedString(jwtSecret)
    return token,err
}

// 根据传入的token值获取到Claims对象信息,(进而获取其中的用户名和密码)
func ParseToken(token string)(*Claims,error){

    //用于解析鉴权的声明,方法内部主要是具体的解码和校验的过程,最终返回*Token
    tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
        return jwtSecret, nil
    })

    if tokenClaims!=nil{
        // 从tokenClaims中获取到Claims对象,并使用断言,将该对象转换为我们自己定义的Claims
        // 要传入指针,项目中结构体都是用指针传递,节省空间。
        if claims,ok:=tokenClaims.Claims.(*Claims);ok&&tokenClaims.Valid{
            return claims,nil
        }
    }
    return nil,err

}
func main() {
    token, _ := GenerateToken("YourUsername", "YourPasword")
    claims,_:=ParseToken(token)
    fmt.Println(claims)
}
jwt的使用
复制代码

3.Gin中间件

  

Gin设置跨域

在设置跨域的时候如前端需要在请求头中携带token和cookie。

前端:需要在前端请求头加上withCredentials: true

后端:需要配置Access-Control-Allow-Credentials允许客户也携带。

ps:此时Access-Control-Allow-Origin, 只能写指定的域名例如: "http://localhost:8080",不能写 “ * “”。

浏览器的同源策略(同IP+端口)就会出现跨域问题,所以想要解决就在server端的response header(响应头) 中设置一下允许浏览器跨域的参数。

1.下载cors

go get github.com/gin-contrib/cors

 

2.中间件

复制代码
// 处理跨域请求,支持options访问
func GinCors() gin.HandlerFunc {
    return func(c *gin.Context) {
        method := c.Request.Method
        c.Header("Access-Control-Allow-Origin", "http://localhost:8080")
        c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token, developerId")
        c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
        c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
        c.Header("Access-Control-Allow-Credentials", "true")

        //放行所有OPTIONS方法
        if method == "OPTIONS" {
            c.AbortWithStatus(http.StatusNoContent)
        }
        // 处理请求
        c.Next()
    }
}
复制代码

 

3.vue

复制代码
<template>
<div class="login_container">
    <div class="login_box">
        <!-- 头像区域 -->
        <div class="avatar_box">
            <img src="../assets/logo.png" alt="">
        </div>
         <!-- 头像区域 -->
         <el-form ref="loginFormRef" label-width="0px" class="login_form" :model="loginForm" :rules="loginFormRules"
         >
            <el-form-item  prop="username">
                <el-input v-model="loginForm.username"  prefix-icon="iconfont icon-user" ></el-input>
            </el-form-item>
            <el-form-item prop="password" >
                <el-input v-model="loginForm.password"  type="password" prefix-icon="iconfont icon-3702mima" ></el-input>
            </el-form-item>
            <el-form-item class="btns">
                <el-button type="primary" @click="login" >登录</el-button>
                <el-button type="info" @click="restLoginForm">重置</el-button>
            </el-form-item>
        </el-form>
    </div>

</div>
</template>

<script>
export default {
  data () {
    return {
      loginForm: {
        username: '',
        pasword: ''

      },
      // 表单验证
      loginFormRules: {
        username: [
          { required: true, message: '请输入用户名', trigger: 'blur' },
          { min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
        ],
        password: [
          { required: true, message: '请输入用户密码', trigger: 'blur' },
          { min: 6, max: 18, message: '长度在 6 到 18 个字符', trigger: 'blur' }
        ]
      }
    }
  },
  methods: {
    // 重置表单
    restLoginForm () {
      this.$refs.loginFormRef.resetFields()
    },
    login () {
      this.$refs.loginFormRef.validate((valid) => {
        if (!valid) { return }
        // 把form表单提交到后端
        this.$http({
          method: 'post',
          headers: { 'Content-Type': 'application/json;charset=utf-8' },
          url: '/login/',
          withCredentials: true,
          data: this.loginForm
        }).then(response => {
          const status = response.status
          if (status === 200) {
            const data = response.data
            console.log(data)
          }
        })
          .catch(err => {
            console.log(`Get data from serverr faild:${err}`)
          })
      })
    }
  }
}
</script>
<style lang="less" scoped>
/* // lang="less" 支持less格式
// scoped vue的指令,只在当前组件生效 */
.login_container {
  background-color: #2b4b6b;
  height: 100%;
}
.login_box {
  width: 450px;
  height: 320px;
  background-color: #fff;
  border-radius: 3px;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  background-color: #fff;

  .avatar_box {
    width: 130px;
    height: 130px;
    border: 1px solid #eee;
    border-radius: 50%;
    padding: 10px;
    box-shadow: 0 0 10px #ddd;
    position: absolute;
    left: 50%;
    transform: translate(-50%, -50%);
    background-color: #fff;
    img {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background-color: #eee;
    }
  }
}
.login_form {
  position: absolute;
  bottom: 60px;
  width: 100%;
  padding: 0 20px;
  box-sizing: border-box;
}
.btns {
  display: flex;
  justify-content: center;
}
.info {
  font-size: 13px;
  margin: 10px 15px;
}
</style>
login.vue
复制代码

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

参考

posted on   Martin8866  阅读(1046)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示