fupanc's blog

gin框架

一个使用较广的web框架,这里来快速学习一下。

因为是外部的库,所以需要下载gin框架依赖包,先更新goproxy为阿里云:

go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct

然后再下载并运行如下命令即可:

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

运行完毕后即可在go.mod文件中看到成功添加:

image-20260202174642169

下面来简单学习相关的语法。

基本利用了解

路由参数(GET传参):

package main

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

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

	g.GET("/user/:name/*action", func(c *gin.Context) {
		name := c.Param("name")
		//*表示通配符,即匹配后面的所有路径,并且需要放在最后
		action := c.Param("action")
		c.String(200, "Hello %s,你的剩余路径为:%s", name, action)
	})
	g.GET("/user", func(c *gin.Context) {
		name := c.Query("name")
		age := c.DefaultQuery("age", "0")
		c.JSON(http.StatusOK, gin.H{
			"name": name,
			"age":  age,
		})
	})

	g.Run(":8081")
}

理解一下,然后自己敲一边即可,不是非常难。

关于通配符的输出,如下demo:

![image-20260203161150134](/Users/fanpengcheng/Library/Application Support/typora-user-images/image-20260203161150134.png)

————————

处理POST表单数据

处理POST请求中的表单数据:

package main

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

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

	g.POST("/login", func(c *gin.Context) {
		username := c.PostForm("username")
		password := c.DefaultPostForm("password", "")
		if username == "admin" && password == "123456" {
			c.JSON(http.StatusOK, gin.H{
				"status":   "账号密码正确",
				"Username": username,
			})
		} else {
			c.JSON(http.StatusOK, gin.H{
				"status": "登陆失败",
			})
		}
	})

	g.Run(":8081")
}

处理POST JSON数据

主要记住一个点,需要结合结构体来进行利用。

package main

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

type LoginRequests struct {
	//后面的为结构体的tag,当解析这个结构体时会使用到,比如这里就是Username表示在json中为username,并且required表示必须要有这个字段
	Username string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required"`
}

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

	//用结构体来接收JSON数据:
	g.POST("/login", func(c *gin.Context) {
		var loginRequest LoginRequests
		var message string
		//绑定请求体到结构体上:
		if err := c.BindJSON(&loginRequest); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		}
		if loginRequest.Username == "admin" && loginRequest.Password == "123456" {
			message = "sucess"
		} else {
			message = "false"
		}
		c.JSON(http.StatusOK, gin.H{
			"message":  message,
			"username": loginRequest.Username,
		})
	})

	//直接使用map来接收JSON数据。适合不确定请求包参数的json请求体
	g.POST("/raw", func(c *gin.Context) {
		var recieve_map map[string]interface{}
		if err := c.BindJSON(&recieve_map); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		}
		c.JSON(http.StatusOK, gin.H{
			"message": recieve_map,
		})
	})

	g.Run(":8081")
}

验证:

curl -X POST http://127.0.0.1:8081/raw \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"123456"}'

自己验证一下即可。

其他的同理,什么XML以及前面的表单格式等等,但是都是需要和结构体绑定的,注意一下tag(这个tag用途很多,可以在接收参数同时对参数进行一些要求校验,后续真正开始开发的时候记得使用),遇到多试试。

文件上传

package main

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

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

	//单文件上传
	g.POST("/upload", func(c *gin.Context) {
		file, err := c.FormFile("file")
		if err != nil {
			errors.New("happen worse")
		}
		c.SaveUploadedFile(file, "./uploads/"+file.Filename)
	})

	g.Run(":8081")
}

测试:

curl -F "file=@123.jpg;filename=custom_name.jpg" http://localhost:8081/upload
#
curl -F "file=@123.jpg" http://localhost:8081/upload

随后成功上传到uploads目录下,有custom_name.jpg以及123.jpg的文件。

还存在多文件上传,但是这里就不涉及了,后面真有这方面需求再补充。

路由分组

package main

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

type LoginUser struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

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

	v1 := g.Group("/v1")
	{
		v1.GET("/user", RepeatName)
		v1.POST("/login", Loginrequest)
	}

	g.Run(":8081")
}

func RepeatName(c *gin.Context) {
	name := c.Query("name")
	age := c.DefaultQuery("age", "0")
	c.JSON(http.StatusOK, gin.H{
		"name": name,
		"age":  age,
	})
}

func Loginrequest(c *gin.Context) {
	var login_req LoginUser
	var message string
	if err := c.BindJSON(&login_req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{})
	}
	if login_req.Username == "admin" && login_req.Password == "123456" {
		message = "sucess"
	} else {
		message = "false"
	}
	c.JSON(http.StatusOK, gin.H{
		"status":   message,
		"username": login_req.Username,
	})
}

测试一下就行:

curl -X POST http://127.0.0.1:8081/v1/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"123456"}'

通过分组,我们可以很方便定义不同路由下的功能分类。另外从代码中可以看到,这里是绑定的路由以及方法名称,没有直接将路由和方法具体内容写一起了,和前面对比一下然后理解一下就行,本质只是把方法提取出来了而已。

中间件

分全局、局部中间件,简单给个demo:

package main

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

func main() {
	g := gin.Default()
	g.Use(MiddleHandler())
	{
		g.GET("/shop", func(c *gin.Context) {
			time.Sleep(5 * time.Second)
			now_message, _ := c.Get("exists")
			c.String(http.StatusOK, now_message.(string))
		})
	}

	g.Run(":8081")
}

func MiddleHandler() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		c.Set("exists", "not exists")
		c.Next()
		end := time.Since(start)
		fmt.Println("此次程序用时:", end)
	}
}

成功页面输出not exists,并且控制台如下:

image-20260203172106599

中间件成功实现。里面值得注意的就是Next()函数,大概意思就是跳过执行后续中间件和最终 handler,理解一下即可。还有很多东西,值得后面再挖掘挖掘。

另外中间件代码其实可以直接写成如下格式:

func MiddleHandler(c *gin.Context) {
	start := time.Now()
	c.Set("exists", "not exists")
	c.Next()
	end := time.Since(start)
	fmt.Println("此次程序用时:", end)
}
//注册中间件部分需要改成g.Use(MiddleHandler)

原来的写法就是返回一个函数,故后续需要调用他来获取到需要的函数本身(算中间件函数生成工厂),从Use()形参也能看出来:

image-20260203173043095

注意区分不同函数类型,参考形参gin.HandlerFunc,是否为需要的中间件函数。

身份验证

比较重要的模块,用于身份验证。

利用Cookie

可以结合中间件来进行判断,代码如下:

package main

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

func main() {
	g := gin.Default()
	g.POST("/login", func(c *gin.Context) {
		username := c.PostForm("username")
		password := c.PostForm("password")

		if username == "admin" && password == "123456" {
			c.SetCookie("username", username, 120, "/", "localhost", false, true)
			c.Redirect(http.StatusFound, "/home")
			return
		}
		c.JSON(http.StatusUnauthorized, gin.H{"error": "wrong username or password"})
	})

	//局部中间件,其实本质就是先后执行顺序的问题,在我看来(看形参)
	g.GET("/home", MiddleHandler, func(c *gin.Context) {
		username, _ := c.Cookie("username")
		c.JSON(http.StatusOK, gin.H{
			"status":   "ok",
			"username": username,
		})
	})

	g.Run(":8081")
}

func MiddleHandler(c *gin.Context) {
	username, _ := c.Cookie("username")
	if username == "admin" {
		c.Next()
	} else {
		c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "please login"})
	}
}

代码没什么问题,自己运行看看结果即可,但是注意一个点,登陆成功:

image-20260203202016644

可以看到这里是以键值对的形式来存储的cookie,这种分分钟被越权:

image-20260203202309063

通过伪造cookie即可成功进入home页面,在一个动态交互的网站,这样鉴权不是很行,但是如果是内部使用,就可以考虑这个,明文内容写复杂点即可,注意防止明文泄露。

也许关于Cookie还有其他更好的设置方式,待以后发掘了。

利用JWT

JWT就不多说了,直接看代码可以怎么利用吧。需要下载并引入JWT的包:

go get -u github.com/golang-jwt/jwt/v5

用最常见的HS256来加解密,demo如下:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v5"
	"math/rand"
	"net/http"
	"time"
)

var jwtSecret = []byte("your_secret_key")

type Claims struct {
	//uint代表无符号整数,就是非负数
	UserID  uint `json:"user_id"`
	Isadmin bool `json:"isadmin"`
	//用于jwt自定义的数据
	jwt.RegisteredClaims
}

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

	v1 := g.Group("/v1/public")
	{
		v1.GET("/getToken", func(c *gin.Context) {

			judg := c.DefaultQuery("need", "no")
			if judg == "yes" {
				token, _ := GenerateToken(uint(rand.Intn(3)), true)
				c.SetCookie("token", token, 3600, "/", "", false, true)
				return
			}
			token, _ := GenerateToken(uint(rand.Intn(3)), false)
			c.SetCookie("token", token, 3600, "/", "", false, true)
		})
	}

	v2 := g.Group("/v2/admin")
	{
		v2.Use(MiddlewareParseJWT)
		v2.GET("/info", func(c *gin.Context) {
			user_id, _ := c.Get("user_id")
			c.String(http.StatusOK, "Hello admin , your user id: %v", user_id)
		})
	}

	g.Run(":8081")
}

func MiddlewareParseJWT(g *gin.Context) {
	claims := &Claims{}
	jwt_token, err := g.Cookie("token")
	if err != nil {
	}
	_, err1 := jwt.ParseWithClaims(jwt_token, claims, func(token *jwt.Token) (interface{}, error) {
		return jwtSecret, nil
	})
	if err1 != nil {
	}
	if claims.Isadmin {
		g.Set("user_id", claims.UserID)
		g.Next()
	} else {
		g.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
			"error": fmt.Sprintf(
				"you are not admin, your user_id is %d",
				claims.UserID,
			),
		})
	}
}

// 生成 JWT Token
func GenerateToken(user_id uint, is_admin bool) (string, error) {
	claim := Claims{
		UserID:  user_id,
		Isadmin: is_admin,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)), //两小时有效期
		},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim)
	return token.SignedString(jwtSecret)
}

//解析jwt
func ParserToken(tokenString string) (*Claims, error) {
	claims := &Claims{}
	_, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
		return jwtSecret, nil
	})
	return claims, err
}

理解一下,然后跑一下框架来验证一下,后面直接用轮子即可。

————————

并且由此也可以知道设置身份验证的中间件存在如下三种:全局中间件、单API中间件、路由组中间件,还是可以简单记一下这个。

gin框架暂时完结。

参考文章:

https://golangstar.cn/go_series/go_framework/gin.html#%E5%B0%8F%E7%BB%93

https://www.topgoer.com/gin%E6%A1%86%E6%9E%B6/