这是我参与「第三届青训营 -后端场」笔记创作活动的第10篇笔记。
gin资料
Github地址:gin
官方文档:gin官网
一、常见请求案例
Gin 是一个 go 写的 web 框架,具有高性能的优点。
1.1、初始案例
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() //启动服务以及监听:127.0.0.1::8080
}
1.2、get、post的常见请求处理方式
配套代码:go-ginLearn/demo1
简介
get:①获取路径参数,例如:/user/:name。②获取get参数,例如:/welcome?firstname=Jane&lastname=Doe。
post:①获取表单参数,form-data。
示例
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
//使用默认中间件创建一个gin路由器
r := gin.Default()
//1、常见的方法
//get
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
//get-1:获取路径中的参数【注意:无法匹配/user/,/user这类路径】 //示例:/hello/changlu
r.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name") //获取url中的相应参数
c.String(http.StatusOK, "hello %s", name)
})
//get-2:获取get参数 //示例:/welcome?firstname=Jane&lastname=Doe
r.GET("/welcome", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest") //若是没有获取到,可获取到默认值。【底层还是走query方法】
lastname := c.Query("lastname")
c.String(http.StatusOK, "hello %s %s", firstname, lastname)
})
//post
r.POST("/ping", commonResponse)
//post-1:获取form表单
r.POST("/form_post", func(c *gin.Context) {
//获取表单,和get的query类似
message := c.PostForm("message")
nick := c.DefaultPostForm("nick", "anonymous")
c.JSON(200, gin.H{
"status": "posted",
"message": message,
"nick": nick,
})
})
//put
r.PUT("/ping", commonResponse)
//其他方法:delete、patch、head、options
//2、启动服务
//r.Run() //默认启动服务以及监听:127.0.0.1::8080
r.Run(":3000") //指定端口
}
func commonResponse(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
}
中间PostMan测试展示:
post-1:建立表单
1.3、POST处理JSON操作
配套代码:go-ginLearn/demo2
模拟请求示例
Postman请求:
代码实操
两种方式来进行序列化:①使用JSON的工具类进行。②使用gin框架给我们自带的bind方法来进行解析。
package demo2
import (
"encoding/json"
"github.com/gin-gonic/gin"
)
type User struct {
Username string `json:"username"`
Password string `json:"password"`
}
func Main() {
r := gin.Default()
//方式一:使用JSON工具来进行序列化
r.POST("/user", func(c *gin.Context) {
//1、定义一个Map来进行接收
requestMap := make(map[string]string)
//2、进行JSON序列化(使用JSON工具类)
if err := json.NewDecoder(c.Request.Body).Decode(&requestMap); err != nil{
panic(err)
}
c.JSON(200, gin.H{
"code": 200,
"result": requestMap,
})
})
//方式二:使用gin自带的bind方法来进行(底层会根据对应的类型来判断进行序列化,建议用框架带的更方便)
r.POST("/user2", func(c *gin.Context) {
//1、定义一个user对象
user := User{}
//2、使用gin的bind来进行序列化
if err := c.BindJSON(&user); err != nil {
panic(err)
}
c.JSON(200, gin.H{
"code": 200,
"result": user,
})
})
r.Run()
}
使用gin的bind处理的坑点:gin 结构体json解析的坑
1、对应结构体的字段名一定要大写,否则解析不到,如下是可以的。【虽然这里字段开头是大写的不过不影响解析】
type User struct {
Username string
Password string
}
//若是你想要更加清晰明了的话可以如下使用`json`描述
type User struct {
Username string `json:"username"`
Password string `json:"password"`
}
1.4、上传文件
配套代码:go-ginLearn/demo3
参考文章:UUID生成、go获取文件名和后缀
Postman模拟请求
单个文件:/upload POST
多个文件:/uploads POST
运行效果:所有上传的图片名称都进行UUID处理
代码
common.go:封装的工具方法
package common
import (
uuid "github.com/satori/go.uuid"
"path"
)
func GenerateFileName(fileName string)string {
return uuid.NewV4().String() + path.Ext(fileName)
}
demo3.go:相应的文件上传demo
package demo3
import (
"github.com/gin-gonic/gin"
"go-ginLearn/common"
"log"
"mime/multipart"
)
var (
dst = "C:\Users\93997\Desktop\upload\755b7cdd-ed6c-43c3-b62f-14a5c9e167a1.png"
)
func Main() {
r := gin.Default()
//1、上传单个文件
//表单限制上传大小(默认32MiB)
//r.MaxMultipartMemory = 8 << 20 // 8Mib
r.POST("/upload", func(c *gin.Context) {
//单文件
file, _ := c.FormFile("file") //直接取出key为file的文件
log.Println(file.Filename)
//保存文件
saveFile(file, c)
c.JSON(200, gin.H{
"code": 200,
"message": "上传成功",
})
})
//2、上传多个文件
r.POST("/uploads", func(c *gin.Context) {
//1、取出文件数组
form, _ := c.MultipartForm()
files := form.File["file"] //根据上传的key来取出对应的文件数组
//2、遍历保存
for _ , file := range files {
saveFile(file, c)
}
c.JSON(200, gin.H{
"code": 200,
"message": "上传成功",
})
})
r.Run()
}
//保存文件
func saveFile(file *multipart.FileHeader, c *gin.Context) {
//上传文件到制定目录
targetPath := dst + common.GenerateFileName(file.Filename)
if err := c.SaveUploadedFile(file, targetPath); err != nil {
panic(err)
}
}
二、路由分组
2.1、路由分组与无路由接口
配套代码:go-ginLearn/demo4/demo4.go
案例:①我们可以来使用gin的group来进行对url进行分组。②noroute:指的是无路由的url会走的相同方法。
代码
package demo4
import "github.com/gin-gonic/gin"
func Main() {
r := gin.Default()
//v1的API
v1 := r.Group("/v1")
{
v1.POST("/login", func(c *gin.Context) {
commonResponse(c, "/v1/login")
})
v1.POST("/register",func(c *gin.Context) {
commonResponse(c, "/v1/register")
})
}
//v2的API
v2 := r.Group("v2")
{
v2.POST("/login", func(c *gin.Context) {
commonResponse(c, "/v2/login")
})
v2.POST("/register", func(c *gin.Context) {
commonResponse(c, "/v2/register")
})
}
//其他无对应路由时走的接口
r.NoRoute(func(c *gin.Context) {
c.JSON(404, gin.H{
"code": "404",
"message": "page not found",
})
})
r.Run()
}
func commonResponse(c *gin.Context, url string) {
c.JSON(200, gin.H{
"code": 200,
"url": url,
})
}
测试结果
①/v1/login测试
②/v2/login测试
③随意一个不存在路由:此时就会走相应的一个无路由的接口
三、插件
3.1、gin-jwt
3.1.1、gin-jwt描述
描述:gin的扩展插件,用于认证。
gin-jwt:
/469xxx/第25章 web层开发-用户接口开发/jwt
jwt-go的一个实现
链接:https://pan.baidu.com/s/1eJRbmjipyTOoF1loF_5ucA
提取码:b2rs
gin-jwt 中间件是对 jwt-go 的封装以适应 gin 框架。gin-jwt 对不同的请求流程有不同的 handler:
- 登录请求流程 是用 LoginHandler。
- 需要 jwt 令牌的后续请求 是用 MiddlewareFunc。
- 注销流程 是用 LogoutHandler。
- 刷新请求流程 是用 RefreshHandler。
- 登录失败、token不正确、权限不足 都会进入 Unauthorized 流程中。
以上每种处理句柄中有一个必须要实现的方法,还有其它可选的方法。
3.1.2、案例实现
配套代码:gin-jwt-Learn/gin-jwtLearn.go
demo描述
loginHandler:登录方法
1、Authorizator()方法:进行登录认证
2、PayloadFunc()处理载荷:也就是在token中进行携带的载荷信息。
MiddlewareFunc:需要认证的接口会使用到的插件
1、会对token进行认证处理
2、对于IdentityHandler是否提前实现:认证处理完成之后会进行解析token并存储到context中
①没有自己实现:"identity"
if mw.IdentityHandler == nil {
mw.IdentityHandler = func(c *gin.Context) interface{} {
claims := ExtractClaims(c)
return claims[mw.IdentityKey]
}
}
②若是自己实现了IdentityHandler的话
name就会取出自己提前定义好的IdentityKey
c.Set("JWT_PAYLOAD", claims) //设置载荷信息
identity := mw.IdentityHandler(c)
if identity != nil {
c.Set(mw.IdentityKey, identity) //自定义存储的一个身份信息
}
3、若是实现了角色认证,就会走Authorizator()的方法。
代码实现:使用的是官方仓库中的demo示例
package gin_jwt_Learn
import (
"log"
"os"
"time"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
)
//登录实体类
type login struct {
Username string `form:"username" json:"username" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
//token中存储相应信息的key名称
var identityKey = "id"
//测试需要认证接口
func helloHandler(c *gin.Context) {
claims := jwt.ExtractClaims(c) //取出claims
user, _ := c.Get(identityKey) //获取自己提前定义好的身份信息
c.JSON(200, gin.H{
"userID": claims[identityKey],
"userName": user.(*User).UserName,
"text": "Hello World.",
})
}
// User demo
type User struct {
UserName string
FirstName string
LastName string
}
func Main() {
//获取命令参数PORT,指定端口号
port := os.Getenv("PORT")
r := gin.Default()
if port == "" {
port = "8000"
}
//jwt插件
// the jwt middleware
authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{ //相应jwt插件的接口,我们这里去进行实例化
Realm: "test zone", //身份
Key: []byte("secret key"), //秘钥
Timeout: time.Minute, //超时时长
MaxRefresh: time.Minute, //最大的一个刷新时间
IdentityKey: identityKey, //身份标识key
PayloadFunc: func(data interface{}) jwt.MapClaims { //载荷信息,实际上这个data就是自定义执行的Authenticator方法的返回对象
if v, ok := data.(*User); ok {
return jwt.MapClaims{
identityKey: v.UserName, //存放的是对应的用户名
}
}
return jwt.MapClaims{}
},
IdentityHandler: func(c *gin.Context) interface{} { //用于取出身份信息
claims := jwt.ExtractClaims(c)
return &User{
UserName: claims[identityKey].(string),
}
},
Authenticator: func(c *gin.Context) (interface{}, error) { //login的身份认证
var loginVals login
if err := c.ShouldBind(&loginVals); err != nil {
return "", jwt.ErrMissingLoginValues
}
userID := loginVals.Username
password := loginVals.Password
if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
return &User{
UserName: userID,
LastName: "Bo-Yi",
FirstName: "Wu",
}, nil
}
return nil, jwt.ErrFailedAuthentication
},
Authorizator: func(data interface{}, c *gin.Context) bool { //权限认证
if v, ok := data.(*User); ok && v.UserName == "admin" {
return true
}
return false
},
Unauthorized: func(c *gin.Context, code int, message string) { //若是身份未认证成功。情况:①登录失败。②token不正确。③权限不足。
c.JSON(code, gin.H{
"code": code,
"message": message,
})
},
// TokenLookup is a string in the form of "<source>:<name>" that is used
// to extract token from the request.
// Optional. Default value "header:Authorization".
// Possible values:
// - "header:<name>"
// - "query:<name>"
// - "cookie:<name>"
// - "param:<name>"
TokenLookup: "header: Authorization, query: token, cookie: jwt", //token查询方式:头部、query查询以及cookie携带
// TokenLookup: "query:token",
// TokenLookup: "cookie:token",
// TokenHeadName is a string in the header. Default value is "Bearer"
TokenHeadName: "Bearer", //请求头的header的值
// TimeFunc provides the current time. You can override it to use another time value. This is useful for testing or if your server uses a different time zone than your tokens.
TimeFunc: time.Now,
})
if err != nil {
log.Fatal("JWT Error:" + err.Error())
}
// When you use jwt.New(), the function is already automatically called for checking,
// which means you don't need to call it again.
errInit := authMiddleware.MiddlewareInit()
if errInit != nil {
log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error())
}
//1、登录接口
//测试接口:http://localhost:8080/login
/**
{
"username": "admin",
"password": "admin"
}
*/
r.POST("/login", authMiddleware.LoginHandler) //LoginHandler:登录逻辑
//2、无路由接口,执行之前会进行认证
//测试接口:http://localhost:8080/test
//对于没有路由的会进行认证校验
r.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) {
claims := jwt.ExtractClaims(c)
log.Printf("NoRoute claims: %#v\n", claims)
c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
})
//3、需要认证接口
auth := r.Group("/auth")
// 接口:http://localhost:8080/auth/refresh_token,刷新token接口
// Refresh time can be longer than token timeout
auth.GET("/refresh_token", authMiddleware.RefreshHandler)
//使用jwt认证插件
auth.Use(authMiddleware.MiddlewareFunc())
{
//接口:http://localhost:8080/auth/hello,用于查看当前的token信息
auth.GET("/hello", helloHandler)
}
//指定端口运行
//if err := http.ListenAndServe(":"+port, r); err != nil {
// log.Fatal(err)
//}
r.Run()
}
测试接口:
①登录接口:
②需要认证校验的接口
3.1.3、扩展gin-jwt
①支持form读取token
需求
接口文档在form-data中需要进行读取鉴权:
解决方案
①由于gin-jwt不支持从form中读取token,那么就需要修改源码了,首先去到对应的项目地址进行克隆。
②修改核心代码
1、添加相应的读取form方法
case "form":
token, err = mw.jwtFromFormData(c, v)
2、添加方法
func (mw *GinJWTMiddleware) jwtFromFormData(c *gin.Context, key string) (string, error) {
token := c.DefaultPostForm("token", "")
if token == "" {
return "", ErrEmptyParamToken
}
return token, nil
}
如何使用?
新增即可:
, form: token
②自己调用方法生成token,而不是走loginhandler(调用原始方法)
这个方法在官方示例中没有使用,通过看源码发现给我们提供了这个方法
var authMiddleware *jwt.GinJWTMiddleware
authMiddleware.TokenGenerator(jwt.MapClaims{
//对应的id
constants.IdentityKey: 用户id,
})
四、异常处理
4.1、gin的全局异常处理捕捉并统一返回
配套代码:go-ginLearn/demo5/demo5.go
对于web项目中进行全局异常捕获:
package demo5
import (
"github.com/gin-gonic/gin"
"log"
"net/http"
"runtime/debug"
)
func Recover(c *gin.Context) {
defer func() {
if r := recover(); r != nil{
//打印错误堆栈信息
log.Printf("panic: %v\n", r)
debug.PrintStack()
//封装通用JSON返回
c.JSON(http.StatusOK, gin.H{
"code": "1",
"msg": errorToString(r),
})
//终止后续接口调用,不加的话recover到异常后,还会继续执行接口里后续代码
c.Abort()
}
}()
//加载完 defer recover,继续后续的插件及代码执行
c.Next()
}
//错误转字符串
func errorToString(r interface{}) string {
switch v := r.(type) {
case error:
return v.Error()
default:
return r.(string)
}
}
func Main() {
r := gin.Default()
//使用全局异常捕捉插件:Recover 要尽量放在第一个被加载
r.Use(Recover)
r.GET("/test", func(c *gin.Context) {
// 无意抛出 panic
var slice = []int{1, 2, 3, 4, 5}
slice[6] = 6
})
r.Run()
}
测试接口:当进行panic时,就会向上抛出,之后被defer进行捕捉到
五、请求参数校验
字符串:
max=32 # 最大值为10,即小于等于10
使用方式:
type Timeout struct {
Connect int `json:"connect" binding:"required;max=32"`
Read int `json:"read" binding:"required"`
Send int `json:"send" binding:"required"`
}
对于shouldBindQuery对应字段的别名:form:xx
//登录
type UserLoginParam struct {
UserName string `json:"username" form:"username" `
PassWord string `json:"password" form:"password"`
}
扩展
1、JSON序列化与反序列化
import (
"encoding/json"
"fmt"
)
type userInfo struct {
Name string
Age int `json:"age"` //json序列化时替换字段名称
Hobby []string
}
func main() {
a := userInfo{
Name: "cl",
Age: 10,
Hobby: []string{"golang","java"},
}
//1、对象 => JSON
buf, err := json.Marshal(a)
//若是出现异常
if err != nil {
panic(buf)
}
//打印序列化后的json
fmt.Println(string(buf))
//2、对象 => JSON,过程中进行处理
buf, err = json.MarshalIndent(a, "", "\t")
if err != nil {
panic(buf)
}
fmt.Println(string(buf))
//3、JSON => 对象
var b userInfo
err = json.Unmarshal(buf, &b)
if err != nil {
panic(err) //作为报告致命错误的一种方式,当某些不应该发生的场景发生时,我们就应该调用panic。【替代try catch】
}
fmt.Printf("%#v\n", b)
}
参考文章
[1]. gin 结构体json解析的坑
[2]. Gin中文文档