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
識別請求數據類型並利用反射機制自動提取請求中QueryString
、Form
、Json
、XML
以及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
文件夾,然后在內部分別定義一個users
和posts
文件夾。里面分別定義同名文件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
默認使用了Logger
和Recovery
中間件,其中:Logger
中間件將日志寫入gin.DefaultWriter
,即配置了GIN_MODE=release
。Recovery
中間件會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)
}