Golang的web框架之Gin


前言

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,自動獲取請求參數,並賦值給后端struct的字段。

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")
}

  

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.加載靜態文件路徑

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個遞歸函數的執行過程

壓棧: 用戶---------> 認證中間件---------> 用戶權限中間件---------> 錯誤處理中間件---------> 視圖函數執行

出棧: 視圖函數執行完畢---------> 錯誤處理中間件---------> 用戶權限中間件---------> 認證中間件---------> 用戶

 

2.控制中間件執行流程

所為的控制流程我感覺就是設計中間件這個棧里面包含的層層棧針。

我們在彈匣里裝了什么樣的子彈,扣動扳機時就會發射出什么子彈,這樣想會更簡單一些否則很容易被繞進去。

在中間件執行的過程中我們可以控制進棧和出棧流程。

 

 

以上代碼執行結果:

m1 in
m2 in
m1 out

 

調用context.Next(),繼續調用下一個視圖函數進行壓棧。(子彈裝滿彈匣)

調用context.Abort() 阻止繼續調用后續的函數,執行完當前棧針(函數)之后出棧。(1發子彈就夠了)

調用context.Abort() + return,當前位置返回,當前位置之后的代碼都不需要不執行了。(1發啞彈)

 

package main

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

//中間件1
func middleWare1(c *gin.Context) {
	fmt.Println("middleWare1開始----------")
	c.Next() //調用后續的處理函數
	fmt.Println("middleWare1結束----------")

}

//中間件2
func middleWare2(c *gin.Context) {
	fmt.Println("middleWare2開始========")
	c.Abort()//終止后續處理函數的調用,執行完本函數返回
	return  //更極端一些 到這就結束!(本函數也不需要執行完畢了)。
	fmt.Println("middleWare2結束========")
}

//index視圖函數
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)
	}

}

  

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)
    }

}
代碼

 

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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM