Gin?有這一篇就夠了!


Gin

Gin是Golang的一個后端框架,封裝比較優雅,API友好。

go get -u github.com/gin-gonic/gin

1、hello word

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	r := gin.Default()  // 創建引擎
	
	// 綁定路由規則,執行函數,gin.Context封裝了request和response
	r.GET("/", func(c *gin.Context) {
		c.String(http.StatusOK,"hello world")
	})
	
	r.Run() // 監聽端口,默認是:8080
}

2、路由

2.1、基本路由

gin 框架中采用的路由庫是基於httprouter做的

地址為:https://github.com/julienschmidt/httprouter

r.GET("/",func(c *gin.Context){...})
r.POST("/",func(c *gin.Context){...})
r.PUT("/",func(c *gin.Context){...})
r.DELETE("/",func(c *gin.Context){...})

此外還有一個匹配所有的請求方法Any

r.Any("/",func(c *gin.Context){...})

2.2、路由組

我們可以將擁有共同前綴的url划為一個路由組,習慣性用{}包裹同組路由,只是為了看着清晰。

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

	user := r.Group("user")
	{
		user.GET("/index", func(c *gin.Context) {
			c.String(http.StatusOK,"GET")
		})
		user.POST("/index", func(c *gin.Context) {
			c.String(http.StatusOK,"POST")
		})
	}
	r.Run()
}

如上,我們可以借助postman工具,使用GET和POST請求localhost:8080/user/index,會分別返回GET和POST。

同時呢,路由組也是支持嵌套的。

user := r.Group("/user")
	{
		user.GET("/index", func(c *gin.Context) {
			c.String(http.StatusOK,"GET")
		})
		user.POST("/index", func(c *gin.Context) {
			c.String(http.StatusOK,"POST")
		})
		
		// 嵌套路由組
		boy := user.Group("/boy")
		{
			boy.GET("/index", func(c *gin.Context) {
				c.String(http.StatusOK,"boy")
			})
		}	
	}

同樣,我們在postman進行測試,使用get方式訪問localhost:8080/user/boy/index,返回boy。

通常我們將路由組分在划分業務邏輯或划分API版本。

2.3、RESTful API

REST與技術無關,代表一種軟件架構風格,REST是Representational State Transfer的簡稱,中文翻譯為“表征狀態轉移”或“表現層狀態轉化”。

簡單來說,就是客戶端與服務器之間進行交互時候,使用HTTP協議中4個請求方法代表不同的動作。

GET(獲取資源),POST(新建資源),PUT(更新資源),DELETE(刪除資源)

只要API程序尊徐了REST風格,那么就可以稱其為RESTful API。

比如我們現在要編寫一個學生管理系統,我們可以對一個學生進行查詢,創建,更新,刪除等操作。按照以往經驗,我們會設計成如下模式:

請求方法 URL 含義
GET /get_student 查詢學生信息
POST /create_student 創建學生信息
PUT /update_student 更新學生信息
DELETE /delete_student 刪除學生信息

同樣的需求我們使用RESTful API設計:

請求方式 URL 含義
GET /student 查詢學生信息
POST /student 創建學生信息
PUT /student 更新學生信息
DELETE /student 刪除學生信息

如果足夠細心的話,會發現上面我們路由組里面其實已經用到了這種方式,只不過一般返回數據是JSON格式。

func main() {
	r := gin.Default()  // 創建路由

	r.GET("/student", func(c *gin.Context) {
        // 我們在返回數據的時候,可以使用map,也可以使用gin.H。
		c.JSON(http.StatusOK, map[string]interface{}{ 
			"message":"get",
		})
	})
	
	r.POST("/student", func(c *gin.Context) {
		c.JSON(http.StatusOK,gin.H{  //我們在返回數據的時候,可以使用map,也可以使用gin.H。
			"message":"post",
		})
	})
	r.Run()
}

3、參數解析

3.1、API參數

請求的參數通過url路徑進行傳遞,例如:/user/Negan/救世軍。獲取請求參數如下:

注意:出現漢字使用Chrome瀏覽器測試,postman不能解析中文。

func main() {
	r := gin.Default()  
	r.GET("/user/:name/:title", func(c *gin.Context) {
		name := c.Param("name")
		title := c.Param("title")
		c.JSON(http.StatusOK,gin.H{
			"name":name,
			"title":title,
		})
	})
	r.Run()
}

3.2 、獲取querystring參數

querystring是指URL中?后面攜帶的參數,例如:/user?name=Negan&tag=救世軍。獲取請求參數如下:

注意:querystring參數可以通過DefaultQuery()Query()兩個方法獲取,前者如果不存在,則返回一個默認值,后者不存在則返回空字符串。

func main() {
	r := gin.Default()
	r.GET("/user", func(c *gin.Context) {
		name := c.DefaultQuery("name","Rick")  // 如果不存在,就使用默認
		tag := c.Query("tag")  // 如果不存在,則返回空字符串
		c.JSON(http.StatusOK,gin.H{
			"name":name,
			"tag":tag,
		})
	})
	r.Run()
}

3.3、獲取表單參數

表單傳輸為POST請求,http創建的傳世格式為四種:

  • application/json
  • application/x-www/form-urlencoded
  • application/xml
  • multipart/form-data

同樣,表單參數的獲取,gin框架也為我們提供了兩種方法,DefaultPostForm()PostForm()方法。前者如果獲取不到會返回一個默認值,后者會返回一個空字符串。

func main() {
	r := gin.Default()
	r.POST("/", func(c *gin.Context) {
		name := c.PostForm("name")
		tag := c.DefaultPostForm("tag","Boss")
		c.JSON(http.StatusOK,gin.H{
			"name":name,
			"tag":tag,
		})
	})
	r.Run()
}

3.4 、數據解析與綁定

為了能夠更方便的獲取請求相關參數,提高開發效率,我們可以基於請求Content-Type識別請求數據類型並利用反射機制自動提取請求中QueryStringFormJsonXML以及URI等參數到結構體中。

注意:解析的數據必須存在,若接收空值則報錯。

type Login struct {
	User string `form:"user" json:"user" uri:"user" binding:"required"`
	Password string `form:"password" json:"password" uri:"password" binding:"required"`
}


func main() {
	r := gin.Default()
	login := Login{}  // 聲明一個結構體
	// 綁定json示例 {"user":"Negan","password":"123456"}
	r.POST("/json", func(c *gin.Context) {
		// c.ShouldBind() 通吃,會根據content-type自動推導
		if err := c.ShouldBindJSON(&login); err != nil{
			c.JSON(http.StatusBadRequest,gin.H{
				"error":err.Error(),
			})
			return
		}
		c.JSON(http.StatusOK,gin.H{
			"user": login.User,
			"password": login.Password,
		})
	})


	// 綁定form表單示例,我們直接在postman上進行測試
	r.POST("/form", func(c *gin.Context) {
		if err := c.ShouldBind(&login);err != nil{  //ShouldBind()會自動推導
			c.JSON(http.StatusBadRequest,gin.H{
				"error":err.Error(),
			})
			return
		}
		c.JSON(http.StatusOK,gin.H{
			"user":login.User,
			"password":login.Password,
		})
	})

	// 綁定QueryString參數
	r.GET("/query", func(c *gin.Context) {
		if err := c.ShouldBindQuery(&login); err != nil{
			c.JSON(http.StatusBadRequest,gin.H{
				"error":err.Error(),
			})
			return
		}
		c.JSON(http.StatusOK,gin.H{
			"user":login.User,
			"password":login.Password,
		})
	})

	// 綁定API參數
	r.GET("/api/:user/:password", func(c *gin.Context) {
		if err := c.ShouldBindUri(&login); err != nil{
			c.JSON(http.StatusBadRequest,gin.H{
				"error":err.Error(),
			})
			return
		}
		c.JSON(http.StatusOK,gin.H{
			"user":login.User,
			"password":login.Password,
		})
	})

	r.Run()
}

4、文件上傳

4.1、單文件上傳

package main

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

func main() {
	r := gin.Default()
	// 處理multipart forms提交文件時默認的內存限制是32MB
	r.MaxMultipartMemory = 8 << 20 // 修改為8MB
	r.POST("/upload", func(c *gin.Context) {
		// 單個文件
		file, err := c.FormFile("file")  // 表單的name
		if err != nil{
			c.JSON(http.StatusInternalServerError, gin.H{
				"message":err.Error(),
			})
			return
		}
		log.Println(file.Filename)
		dst := fmt.Sprintf("H:\\GinDemo\\lesson03\\%s", file.Filename)  // 拼接文件保存路徑
		if err := c.SaveUploadedFile(file, dst); err != nil{
			c.JSON(http.StatusInternalServerError,gin.H{
				"message":err.Error(),
			})
			return
		}
		c.JSON(http.StatusOK,gin.H{
			"message":fmt.Sprintf("%s upload", file.Filename),
		})
	})
	r.Run()
}

4.2、多文件上傳

func main() {
	r := gin.Default()
	// 處理multipart forms提交文件時默認的內存限制是32MB
	r.MaxMultipartMemory = 8 << 20 // 修改為8MB
	r.POST("/upload", func(c *gin.Context) {
		form, _ := c.MultipartForm()  // 多文件
		files := form.File["files"]  // 表單的name
		for index, file := range files{
			log.Println(file.Filename)
			dst := fmt.Sprintf("H:\\GinDemo\\lesson03\\%s_%d",file.Filename,index)
			// 上傳文件到指定目錄
			c.SaveUploadedFile(file,dst)
		}
		c.JSON(http.StatusOK,gin.H{
			"message":"文件上傳能完成",
		})
	})
	r.Run()
}

4.3、使用FastDFS實現文件上傳

package tool

import (
	"bufio"
	"fmt"
	"github.com/tedcy/fdfs_client"
	"os"
	"strings"
)

// 上傳文件到fastDFS系統
func UploadFile(fileName string)string{
	client, err := fdfs_client.NewClientWithConfig("./config/fastdfs.conf")
	if err != nil{
		fmt.Println("打開fast客戶端失敗",err.Error())
		return ""
	}
	defer client.Destory()
	fileId, err := client.UploadByFilename(fileName)
	if err != nil{
		fmt.Println("上傳文件失敗",err.Error())
		return ""
	}
	return fileId
}


// 從配置文件中讀取服務器的ip和端口配置
func FileServerAddr() string{
	file,err := os.Open("./config/fastdfs.conf")
	if err != nil{
		fmt.Println(err)
		return ""
	}
	reader := bufio.NewReader(file)
	for{
		line, err := reader.ReadString('\n')
		line = strings.TrimSpace(line)
		if err != nil{
			return ""
		}
		line = strings.TrimSuffix(line,"\n")
		str := strings.SplitN(line,"=",2)
		switch str[0] {
		case "http_server_port":return str[1]
		}
	}
}


// 下載文件
func DownLoadFile(fileId,tempFile string){
	client, err := fdfs_client.NewClientWithConfig("./config/fastdfs.conf")
	if err != nil{
		fmt.Println("打開fast客戶端失敗",err.Error())
		return
	}
	defer client.Destory()
	if err = client.DownloadToFile(fileId,tempFile,0,0);err != nil{
		fmt.Println("下載文件失敗", err.Error())
		return
	}
}


// 刪除
func DeleteFile(fileId string){
	client, err := fdfs_client.NewClientWithConfig("./config/fastdfs.conf")
	if err != nil{
		fmt.Println("打開fast客戶端失敗",err.Error())
		return
	}
	defer client.Destory()
	if err = client.DeleteFile(fileId);err != nil{
		fmt.Println("刪除文件失敗", err.Error())
		return
	}
}

配置文件

tracker_server=123.56.243.64:22122
http_server_port=http://123.56.243.64:80
maxConns=100

5、重定向

5.1、http重定向

r.GET("/", func(c *gin.Context) {
		c.Redirect(http.StatusMovedPermanently, "https://www.baidu.com")
	})

5.2、路由重定向

使用HandleContext

// 路由重定向
r.GET("/a", func(c *gin.Context) {
	// 跳轉到/b對應的路由處理函數
	c.Request.URL.Path = "/b"  // 把請求的uri修改
	r.HandleContext(c)  // 繼續后續的處理
})

r.GET("/b", func(c *gin.Context) {
	c.JSON(http.StatusOK,gin.H{
		"msg": "BBBBBB",
	})
})

6、Gin渲染

6.1、HTML渲染

Gin框架中使用LoadHTMLGlob()或者LoadHTMLFiles()方法進行HTML模板渲染。LoadHTMLGlob()可以加載路徑下所有的模板文件,LoadHTMLFiles()加載模板文件需要我們自己填入。

我們定義一個存放模板文件的templates文件夾,然后在內部分別定義一個usersposts文件夾。里面分別定義同名文件index.tmpl

users\index.tmpl文件內容:

{{define "users/index.tmpl"}}
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="/xxx/index.css">
    <title>Document</title>
</head>
<body>
    {{ .title }}
</body>
</html>
{{end}}

posts\index.tmpl文件內容:

{{define "posts/index.tmpl"}}
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
{{ .title }}
</body>
</html>
{{end}}

我們很容易發現,HTML文件開頭和結尾我們定義了{{define}}{{end}},Gin框架在進行模板渲染時候會根據這個我們定義的名字進行查找文件。{{.title}}則是Gin后端給我們傳過來的數據。

Gin后端代碼如下:

package main

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

func main() {
	r := gin.Default()
	// 解析模板
	// r.LoadHTMLFiles("templates/users/index.tmpl","templates/posts/index.tmpl")
	r.LoadHTMLGlob("templates/**/*")  // 加載所有
	r.GET("/posts", func(c *gin.Context) {
		c.HTML(200,"posts/index.tmpl",gin.H{ // 模板渲染
			"title":"我是posts頁面",
		})
	})
	r.GET("/users", func(c *gin.Context) {
		c.HTML(200,"users/index.tmpl",gin.H{ // 模板渲染
			"title":"<a href='http://www.baidu.com'>百度一下</a>",
		})
	})
	r.Run()  // 啟動server
}

我們分別訪問localhost:8080/posts和localhost:8080/users,會得到不同的頁面。

當我們訪問localhost:8080/users時,我們會發現瀏覽器顯示的是<a href='http://www.baidu.com'>百度一下</a>,和我們預期的不一樣,我們預期的是,頁面上應該顯示一個超鏈接才對。但是我們看到的是直接當字符串解析了。

那么怎么解決這個問題呢?

6.2、自定義模板函數

接上面的問題,我們可以定義一個不轉義相應內容的safe模板函數。

package main

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

func main() {
	r := gin.Default()
	// gin框架中添加自定義模板函數
	r.SetFuncMap(template.FuncMap{
		"safe": func(s string) template.HTML {
			return template.HTML(s)
		},
	})
	// 解析模板
	r.LoadHTMLGlob("templates/**/*")  // 加載所有
	r.GET("/users", func(c *gin.Context) {
		c.HTML(200,"users/index.tmpl",gin.H{ // 模板渲染
			"title":"<a href='http://www.baidu.com'>百度一下</a>",
		})
	})
	r.Run()  // 啟動server
}

index.tmpl中使用定義好的safe模板函數:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>修改模板引擎的標識符</title>
</head>
<body>
<div>{{ .title | safe }}</div>
</body>
</html>

6.3、靜態文件處理

當我們渲染HTML文件中需要引入靜態文件時,我們調用gin.Static方法即可。

func main() {
	r := gin.Default()
	r.Static("/static", "./static")  // 第一個參數是url路徑,第二個參數是實際文件所在路徑
	r.LoadHTMLGlob("templates/**/*")
   // ...
	r.Run(":8080")
}

7、中間件

7.1、定義全局中間件

Gin中的中間件必須是一個gin.HandlerFunc類型。

// 定義中間件(統計請求耗時)
func MiddleWare() gin.HandlerFunc{
	return func(c *gin.Context) {
		start := time.Now()
		c.Set("name","Negan")  // 通過c.Set在請求上下文中設置值,后續處理函數能夠取到該值
		c.Next()   // 調用該請求剩余的部分
		// c.Abort() // 不調用該請求剩余的部分
		// 計算耗時
		cost := time.Since(start)
		log.Println(cost)
	}
}

7.2、注冊中間件

在Gin框架中,我們可以為每個路由添加任意數量的中間件。

7.2.1、為全局路由注冊
func main() {
	r := gin.New()  // 新建一個沒有任何默認中間件的路由引擎
	r.Use(MiddleWare())   // 注冊一個全局中間件
	r.GET("/", func(c *gin.Context) {
		name := c.MustGet("name").(string)  // 取值,並自動捕獲處理異常
		c.JSON(http.StatusOK,gin.H{
			"name":name,
		})
	})
	r.Run()
}
7.2.2、為某一個路由單獨注冊(可注冊多個)
func main() {
	r := gin.New()  // 新建一個沒有任何默認中間件的路由引擎
	r.GET("/index1", MiddleWare(), func(c *gin.Context) {
		name := c.MustGet("name").(string)  // 取值,並自動處理異常
		c.JSON(http.StatusOK,gin.H{
			"name":name,
		})
	})
	r.Run()
}

7.2.3、為某一個方法注冊中間件
// 處理器函數
func M1(c *gin.Context){
	c.JSON(http.StatusOK,gin.H{
		"msg":"OK",
	})
}

func main(){
    r := gin.New()
    r.User(M1,MiddleWare())  // m1處理器函數注冊中間件
    r.GET("/m1",M1)
    r.Run()
}
7.2.4、為路由組注冊中間件

為路由組注冊中間件有兩種寫法:

  • 寫法一:
user := r.Group("/user")
user.Use(MiddleWare())
{
    user.GET("/index1",  func(c *gin.Context) {
        name := c.MustGet("name").(string)  // 取值,並自動處理異常
        c.JSON(http.StatusOK,gin.H{
            "name":name,
        })
    })
    user.GET("/index2",func(c *gin.Context) {
        name := c.MustGet("name").(string)  // 取值,並自動處理異常
        c.JSON(http.StatusOK,gin.H{
            "name":name,
        })
    })
}
  • 寫法二:
user := r.Group("/user", MiddleWare())
{
    user.GET("/index1",  func(c *gin.Context) {
        name := c.MustGet("name").(string)  // 取值,並自動處理異常
        c.JSON(http.StatusOK,gin.H{
            "name":name,
        })
    })
    user.GET("/index2",func(c *gin.Context) {
        name := c.MustGet("name").(string)  // 取值,並自動處理異常
        c.JSON(http.StatusOK,gin.H{
            "name":name,
        })
    })
}

7.3、中間件注意事項

gin.Default默認使用了LoggerRecovery中間件,其中:Logger中間件將日志寫入gin.DefaultWriter,即配置了GIN_MODE=releaseRecovery中間件會recover任何panic,如果有panic的話,會寫入500響應碼。

如果不想使用上面兩個默認中間件,可以使用gin.New()新建一個沒有任何默認中間件的路由。

8、會話控制

8.1、Cookie

HTTP是無狀態協議,服務器不能記錄瀏覽器的訪問狀態,也就是說服務器不能區分兩次請求是否由同一個客戶端發出。cookie就是解決HTTP無狀態的方案之一,cookie實際上就是服務器在瀏覽器上保存的一段信息,瀏覽器有了cookie之后,每次向服務器發送請求時都會將該信息發送給服務器,服務器收到請求后,就可以根據該信息處理請求。

8.1.1、Go操作cookie

標准庫net/http中定義了cookie,它代表一個出現在HTTP響應頭中Set-Cookie的值,或者HTTP請求頭中Cookie的值的HTTP cookie

type Cookie struct {
    Name       string
    Value      string
    Path       string
    Domain     string
    Expires    time.Time
    RawExpires string
    // MaxAge=0表示未設置Max-Age屬性
    // MaxAge<0表示立刻刪除該cookie,等價於"Max-Age: 0"
    // MaxAge>0表示存在Max-Age屬性,單位是秒
    MaxAge   int
    Secure   bool
    HttpOnly bool
    Raw      string
    Unparsed []string // 未解析的“屬性-值”對的原始文本
}

具體實現代碼:

// 創建兩個cookie
func setCookie(w http.ResponseWriter, r *http.Request){
	// 創建cookie1
	cookie1 := http.Cookie{
		Name: "user",
		Value: "admin",
		HttpOnly: true,
		MaxAge: 999,
	}
    // 創建cookie2
    cookie2 := http.Cookie{
        Name: "user1",
        Value: "admin1",
        HttpOnly:true,
        MaxAge:999,
    }
    
	// 將cookie發送給瀏覽器
	//w.Header().Set("Set-Cookie", cookie.String())
    //w.Header().Add("Set-Cookie",cookie.String())
	http.SetCookie(w, &cookie)
}


func getCookie(w http.ResponseWriter, r *http.Request){
    // 會將兩個cookie全部獲取
	//cookie := r.Header.Get("Cookie")
	//fmt.Fprintln(w,"獲取的cookie是:", cookie)

	// 如果要得到一個cookie,可以直接調用Cookie方法
	cookie,_ := r.Cookie("user1")
	fmt.Fprintln(w,"獲得的cookie是:",cookie)  // user1=admin1
}


func main() {
	http.HandleFunc("/setCookie", setCookie)
	http.HandleFunc("/getCookie", getCookie)
	http.ListenAndServe(":8888",nil)
}
8.1.2、Gin中操作Cookie
func main() {
	r := gin.New() // 新建一個沒有任何默認中間件的路由引擎
	r.GET("/cookie", func(c *gin.Context) {
		cookie, err := c.Cookie("gin_cookie") // 獲取cookie
		if err != nil {
			cookie = "NotSet"
			// 設置cookie
			c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
		}
		fmt.Println(cookie)
	})
	r.Run()
}

8.2、Session

Cookie雖然在一定程度上解決了"保持狀態"的需求,但是Cookie本身最大支持4096字節,以及Cookie保存在客戶端,可能會被攔截竊取。這時就需要一種新的東西,能支持更多的字節,保存在客戶端,有較高的安全性,這就是Session

我們將Session ID保存到Cookie中,服務器通過該Session ID就能找到與之對應的Session數據 。

Session我們可以使用第三方庫實現(基於Redis)

package main

import (
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/redis"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	store, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret"))
	r.Use(sessions.Sessions("mysession", store))

	r.GET("/incr", func(c *gin.Context) {
		session := sessions.Default(c)
		var count int
		v := session.Get("count")
		if v == nil {
			count = 0
		} else {
			count = v.(int)
			count++
		}
		session.Set("count", count)
		session.Save()
		c.JSON(200, gin.H{"count": count})
	})
	r.Run(":8000")
}

8.3、JWT

JWT全稱JSON Web Token,是一種跨域認證解決方案,屬於一個開放的標准,它規定了一種Token實現方式,目前多用於前后端分離項目和OAth2.0業務場景下。

JWT就是一種基於Token的輕量級認證模式,服務端認證通過后,會生成一個JSON對象,經過簽名后得到一個Token再發回給用戶,用戶后續請求只需要帶上這個Token,服務端解密后就能獲取該用戶的相關信息了。

8.3.1、生成和解析JWT

我們在這里直接使用jwt-go這個庫來實現我們生成和解析JWT的功能。

  • 定義需求

我們根據自己的需求來來決定JWT中保存哪些數據,比如我們規定在JWT中存儲username信息,那么我們就定義一個MyClaims結構體。

// MyClaims 自定義聲明結構體並內嵌jwt.StandardClaims
// jwt.StandardClaims只包含了官方字段
// 我們這里需要額外記錄一個username字段,所以自定義結構體
// 如果想要保存更多信息,都可以添加到這個結構體中
type MyClaims struct {
	Username string `json:"username"`
	jwt.StandardClaims
}

const TokenExpireDuration = time.Hour * 2 // 設置過期時間為兩小時
var MySecret = []byte("永遠不要高估自己")         // 定義一個密鑰
  • 生成JWT和解析JTW
// 生成JWT
func GenToken(username string) (string, error) {
	t := MyClaims{
		username,
		jwt.StandardClaims{
			ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 過期時間
			Issuer:    "Negan",                                    // 簽發人
		},
	}
	// 使用指定的簽名方法創建簽名對象
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, t)
	// 使用指定的secret簽名並獲得完整的編碼后的字符串token
	fmt.Println(token.SignedString(MySecret))
	return token.SignedString(MySecret)
}

// 解析JWT
func ParseToken(tokenString string) (*MyClaims, error) {
	// 解析token
	token, err := jwt.ParseWithClaims(tokenString, &MyClaims{},
		func(token *jwt.Token) (interface{}, error) {
			return MySecret, nil
		})
	if err != nil {
		return nil, err
	}
	if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
		return claims, nil
	}
	return nil, errors.New("invalid token")
}
8.3.2、Gin中使用JWT
// 定義用戶結構體
type UserInfo struct {
	Username string `json:"username" form:"username" binding:"required"`
	Password string `json:"password" form:"password" binding:"required"`
}

func authHandler(c *gin.Context) {
	// 獲取用戶發送的用戶名以及密碼
	user := UserInfo{}
	if err := c.ShouldBind(&user); err != nil {
		c.JSON(http.StatusOK, gin.H{
			"code": 404,
			"msg":  "無效的參數",
		})
		return
	}
	// 檢驗用戶名以及密碼是否正確
	if user.Username == "root" && user.Password == "123456" {
		// 生成token
		tokenString, err := GenToken(user.Username)
		if err != nil{
			fmt.Println(err)
		}
		c.JSON(http.StatusOK, gin.H{
			"code": 200,
			"msg":  "success",
			"data": gin.H{"token": tokenString},
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"code": 400,
		"msg":  "鑒權失敗",
	})
	return
}

func JWTAuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		authHeader := c.Request.Header.Get("Authorization")
		if authHeader == "" {
			c.JSON(http.StatusOK, gin.H{
				"code": 400,
				"msg":  "請求頭auth為空",
			})
			fmt.Println("請求頭為空")
			c.Abort() // 終止下面代碼
			return
		}
		// 按照空格分割
		parts := strings.SplitN(authHeader, " ", 2)
		fmt.Println(parts[0])
		if !(len(parts) == 2 && parts[0] == "Bearer") {
			c.JSON(http.StatusOK, gin.H{
				"code": 404,
				"msg":  "請求頭中auth格式有誤",
			})
			c.Abort()
			return
		}
		// parts[1]是獲取到的tokenString,使用我們之前定義解析函數解析
		msg, err := ParseToken(parts[1])
		if err != nil {
			c.JSON(http.StatusOK, gin.H{
				"code": 400,
				"msg":  "無效的token",
			})
			c.Abort()
			return
		}
		// 將當前獲取的username 保存到上下文中
		c.Set("username", msg.Username)
		fmt.Println(msg.Username)
		c.Next() // 后續處理函數會通過c.Get("username")來獲取
	}
}

func main() {
	r := gin.Default()
	r.POST("/auth", authHandler) // 設置Token

	r.GET("/home",JWTAuthMiddleware(), func(c *gin.Context) {
		username := c.MustGet("username").(string)
		fmt.Println(username)
		c.JSON(http.StatusOK, gin.H{
			"code": 200,
			"msg":  "success",
			"data": username,
		})
	})
	r.Run()
}

9、Gin中使用goroutine

當我們在中間件或者handler中啟動新的gouroutine時,不能使用原始上下文(c *gin.Context),必須使用其只讀副本c.Copy()

如果不使用只讀副本,則c的后續的操作不可控,造成並發不安全。

10、運行多個服務

我們可以在多個端口啟動服務,例如:

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"golang.org/x/sync/errgroup"
)

var (
	g errgroup.Group
)

func router01() http.Handler {
	e := gin.New()
	e.Use(gin.Recovery())
	e.GET("/", func(c *gin.Context) {
		c.JSON(
			http.StatusOK,
			gin.H{
				"code":  http.StatusOK,
				"error": "Welcome server 01",
			},
		)
	})

	return e
}

func router02() http.Handler {
	e := gin.New()
	e.Use(gin.Recovery())
	e.GET("/", func(c *gin.Context) {
		c.JSON(
			http.StatusOK,
			gin.H{
				"code":  http.StatusOK,
				"error": "Welcome server 02",
			},
		)
	})

	return e
}

func main() {
	server01 := &http.Server{
		Addr:         ":8080",
		Handler:      router01(),
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	server02 := &http.Server{
		Addr:         ":8081",
		Handler:      router02(),
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}
	// 借助errgroup.Group或者自行開啟兩個goroutine分別啟動兩個服務
	g.Go(func() error {
		return server01.ListenAndServe()
	})

	g.Go(func() error {
		return server02.ListenAndServe()
	})


	if err := g.Wait(); err != nil {
		log.Fatal(err)
	}
}

11、圖片驗證碼

import (
	"github.com/gin-gonic/gin"
	"github.com/mojocn/base64Captcha"
	"image/color"
)

type CaptchaResult struct{
	Id string `json:"id"`
	Base64lob string `json:"base_64_lob"`
	VertifyValue string `json:"code"`
}

// 生成圖形驗證碼
func GenerateCaptcha(c *gin.Context){
	var parameters = base64Captcha.ConfigCharacter{
		Height: 30,
		Width: 60,
		Mode: 3,
		ComplexOfNoiseDot: 0,
		ComplexOfNoiseText: 0,
		IsShowHollowLine: false,
		IsShowNoiseDot: false,
		IsShowNoiseText: false,
		IsShowSineLine: false,
		IsShowSlimeLine: false,
		IsUseSimpleFont: true,
		CaptchaLen: 4,
		BgColor: &color.RGBA{
			R: 3,
			G: 102,
			B: 214,
			A: 254,
		},
	}

	captchaId, captchaInterfaceInstance := base64Captcha.GenerateCaptcha("",parameters)
	base64blob := base64Captcha.CaptchaWriteToBase64Encoding(captchaInterfaceInstance)

	captchaResult := CaptchaResult{Id: captchaId,Base64lob: base64blob}
	Success(c, map[string]interface{}{
		"captcha_result": captchaResult,
	})
}

// 驗證驗證碼
func VertifyCaptcha(id string, value string)bool{
	return base64Captcha.VerifyCaptcha(id, value)
}


免責聲明!

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



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