Gin 快速入门知识点总结(奇淼)

参考视频:go圈里最会写js的奇淼 - gin 教程

参考文档:

最基础的项目:

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(":8090") // 修改端口号, 默认8080
}

浏览器访问:http://localhost:1010/path,得到以下响应:

{
    
    
	"success":true,
}

各种请求方式获取参数

获取 URI 中的参数:Param

// 此规则能够匹配 /user/john 这种格式,但不能匹配 /user/ 或 /user 这种格式
router.GET("/user/:name", func(c *gin.Context) {
    
    
  name := c.Param("name")
  c.String(http.StatusOK, "Hello %s", name)
})

// 此规则既能匹配 /user/john/ 格式也能匹配 /user/john/send 这种格式
// 如果没有其他路由器匹配 /user/john,它将重定向到 /user/john/
router.GET("/user/:name/*action", func(c *gin.Context) {
    
    
  name := c.Param("name")
  action := c.Param("action")
  message := name + " is " + action
  c.String(http.StatusOK, message)
})

获取 Get 参数:QueryDefaultQuery

// 匹配的url格式: /welcome?firstname=Jane&lastname=Doe
router.GET("/welcome", func(c *gin.Context) {
    
    
  firstname := c.DefaultQuery("firstname", "Guest") // 设置默认值
  lastname := c.Query("lastname")

  c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})

获取 Post 参数:PostFormDefaultPostForm

router.POST("/form_post", func(c *gin.Context) {
    
    
  message := c.PostForm("message")
  nick := c.DefaultPostForm("nick", "anonymous") // 此方法可以设置默认值

  c.JSON(200, gin.H{
    
    
    "status":  "posted",
    "message": message,
    "nick":    nick,
  })
})

绑定参数和参数验证

文档:模型绑定和验证

Gin 提供了 MustBindShouldBind 两类绑定方法,使用 ShouldBind 就可以了。

文件的上传和返回

文档:

这里主要学习一下单文件的流程,多文件是差不多的。

读取文件

利用 c.FormFile("file") 读取文件,其中 "file" 是前端放到 file 里的 name

利用原生的方法将文件写到本地:

import (
	"io"
	"os"

	"github.com/gin-gonic/gin"
)

func main() {
    
    
	router := gin.Default()
	router.POST("/upload", func(c *gin.Context) {
    
    
		file, _ := c.FormFile("file")
		name := c.PostForm("name") // 获取其他数据
	
    // 将文件写到本地
		in, _ := file.Open()
		defer in.Close()
		out, _ := os.Create("./" + file.Filename)
		defer out.Close()
		io.Copy(out, in)

		c.JSON(200, gin.H{
    
    
			"file": file,
			"name": name,
		})
	})
	router.Run(":8080")
}

上面代码中也可以使用 Gin 提供的保存文件到指定位置的方法

file, _ := c.FormFile("file")
c.SaveUploadedFile(file, "./"+file.Filename) // 保存文件到指定位置

给前端返回文件

func main() {
    
    
	router := gin.Default()
	router.POST("/upload", func(c *gin.Context) {
    
    
		file, _ := c.FormFile("file")

		// 保存文件到本地
		c.SaveUploadedFile(file, "./"+file.Filename)

		// 将文件返给前端
		c.Writer.Header().Add("Content-Disposition",
			fmt.Sprintf("attachment; filename=%s", file.Filename))
		c.File("./" + file.Filename)
	})
	router.Run(":8080")
}

中间件和路由分组

路由组 | Gin Web Framework (gin-gonic.com)

分组

什么是分组

  • 对 router 创建 Group 就是分组,同一分组会拥有同一前缀和同一中间件

如何创建路由组

router := gin.Default()

// 简单的路由组: v1
v1 := router.Group("/v1")
{
    
    
  v1.POST("/login", loginEndpoint)
  v1.POST("/submit", submitEndpoint)
  v1.POST("/read", readEndpoint)
}

// 简单的路由组: v2
v2 := router.Group("/v2")
{
    
    
  v2.POST("/login", loginEndpoint)
  v2.POST("/submit", submitEndpoint)
  v2.POST("/read", readEndpoint)
}

为什么要分组

  • 让路由结构更加清晰,更加方便的管理路由

中间件

使用中间件 | Gin Web Framework (gin-gonic.com)

自定义中间件 | Gin Web Framework (gin-gonic.com)

什么是中间件

  • 在请求达到路由的方法的前和后进行的一系列操作(洋葱模型)

如何使用中间件

  • 在路由组上进行 use 操作,后面传入中间件函数即可

如何创建中间件

func Logger() gin.HandlerFunc {
    
    
	return func(c *gin.Context) {
    
    
		t := time.Now()

		// 设置 example 变量
		c.Set("example", "12345")

		// 请求前
		c.Next()

		// 请求后
		latency := time.Since(t)
		log.Print(latency)

		// 获取发送的 status
		status := c.Writer.Status()
	}
}

日志和日志格式

为什么要使用日志

  • 记录参数信息
  • 猜测用户行为
  • 复现系统 bug 并修复

Gin 中自带日志写入的中间件,但是自定义比较麻烦,不推荐使用。

第三方日志工具

  • go-logging
  • logrus
  • zap

日志切割

  • 自行根据时间在写入时进行切割日志
  • 借助现成的日志包:go-file-rotatelogs、file-rotatelogs

使用 GORM 进行数据库操作

参考:

什么是 orm

  • 一种数据库操作辅助工具
  • 在 go 的结构体和数据库之间进行映射,让表的内容直观的体现在结构体上
  • 使用结构体来完成增删改查操作

orm 如何链接数据库

  • 导入 gorm
  • 导入 mysql 驱动器
  • 使用 open 链接得到 数据库操作对象
func main() {
    
    
  db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
  defer db.Close()
}

自动化创建数据库表

  • gorm 支持自动迁移模式,使用 AutoMigrate 方法来自动化创建数据库表
db.AutoMigrate(&User{
    
    })

增删改查操作主要参考文档,比较详细。

GORM 结构体的创建技巧

常用 tag 设置模型定义 | GORM

  • 设置主键:primary_key
  • 自定义字段名字:column:user_id
  • 忽略:"_"
  • 指定数据类型:type:varchar(100)
  • 非空:not null
  • 创建索引:index
  • 设置外键:ForeignKey
  • 关联外键:AssociationForeignKey
  • 多对多:many2many2:表名

使用示例:

type user struct {
    
    
  gorm.Model
  Name string `gorm:"primary_key;column:user_name;type:varchar(100);"`
}

自定义表名(动态表名)约定 | GORM

func (User) TableName() string {
    
    
	return "qm_user"
}
  • 甚至可以做一些 条件判断
func (u User) TableName() string {
    
    
	if u.Role == "admin" {
    
    
		return "admin_users"
	} else {
    
    
		return "qm_users"
	}
}

结构体声明

type Student struct {
    
    
	gorm.Model
	ClassID uint
	IDCard  IDCard // 1个学生拥有1张身份证
}

type IDCard struct {
    
    
	gorm.Model
	StudentID uint // 1张身份证属于1个学生
	Num       int
}
type Class struct {
    
    
	gorm.Model
	Students []Student // 1个班级有N个学生 
}

type IDCard struct {
    
    
	gorm.Model
	StudentID uint // 1张身份证属于1个学生
	Num       int
}
// User 拥有并属于多种 language,`user_languages` 是连接表
type User struct {
    
    
  gorm.Model
  // 用户可以拥有多种语言
  Languages []*Language `gorm:"many2many:user_languages;"`
}

type Language struct {
    
    
  gorm.Model
  Name string
  // 
  Users []*User `gorm:"many2many:user_languages;"`
}

建表示例:

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type Class struct {
    
    
	gorm.Model
	ClassName string
	Students  []Student // 1个班级有多个学生
}

type Student struct {
    
    
	gorm.Model
	StudentName string
	ClassID     uint   // 1个学生属于多个班级
	IDCard      IDCard // 1个学生拥有1张身份证
	// 多对多, 通过中间表关联
	Teachers []Teacher `gorm:"many2many:student_teachers;"`
}

type IDCard struct {
    
    
	gorm.Model
	Num       int
	StudentID uint // 1张身份证属于1个学生
}
type Teacher struct {
    
    
	gorm.Model
	TeacherName string
	// 多对多, 通过中间表关联
	Students []Student `gorm:"many2many:student_teachers;"`
}

func main() {
    
    
  CreateTable()
}

func CreateTable() {
    
    
	dsn := "root:lzy123456@tcp(127.0.0.1:3306)/ginclass?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
    
    })
	db.AutoMigrate(&Teacher{
    
    }, &Class{
    
    }, &Student{
    
    }, &IDCard{
    
    })

	i := IDCard{
    
    
		Num: 123456,
	}
	t := Teacher{
    
    
		TeacherName: "老师傅",
	}
	s := Student{
    
    
		StudentName: "qm",
		IDCard:      i,
		Teachers:    []Teacher{
    
    t},
	}
	c := Class{
    
    
		ClassName: "奇妙的班级",
		Students:  []Student{
    
    s},
	}

	db.Create(&c)
	db.Create(&t)
}

GORM 结合 Gin 示例

预加载 | 关联 |《GORM 中文文档 v2》| Go 技术论坛 (learnku.com)

注意:使用 预加载Preload 可以查询出关联数据

func main() {
    
    
  // 连接数据库
	dsn := "root:lzy123456@tcp(127.0.0.1:3306)/ginclass?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
    
    })
	db.AutoMigrate(&Teacher{
    
    }, &Class{
    
    }, &Student{
    
    }, &IDCard{
    
    })

	r := gin.Default()

	// 新增学生
	r.POST("/student", func(c *gin.Context) {
    
    
		var student Student
		_ = c.BindJSON(&student)
		db.Create(&student)
	})
  
	// 根据id查询学生
	r.GET("/student/:id", func(c *gin.Context) {
    
    
		id := c.Param("id")
		var student Student
		_ = c.BindJSON(&student)
		// db.First(&student, "id = ?", id) // 无法加载出关联的数据
		db.Preload("Teachers").Preload("IDCard").
    	First(&student, "id = ?", id) // 可以查出关联数据
		c.JSON(200, gin.H{
    
    "student": student})
	})
  
	// 根据id查询班级
	r.GET("/class/:id", func(c *gin.Context) {
    
    
		id := c.Param("id")
		var class Class
		// db.First(&class, "id = ?", id) // 无法加载出关联的数据
		db.Preload("Students").Preload("Students.Teachers").Preload("Students.IDCard").
			First(&class, "id = ?", id) // 可以查出关联数据
		c.JSON(200, gin.H{
    
    "class": class})
	})

	r.Run(":8888")
}

jwt-go

什么是 jwt

  • 全称 JSON WEB TOKEN
  • 一种后台不做存储的前端身份验证的工具
  • 分为三部分 Header Claims Signature

也有的地方分成:Header Paload Signature

引入 jwt-go 第三方库:

import "github.com/dgrijalva/jwt-go"

文档地址:https://pkg.go.dev/github.com/dgrijalva/[email protected]+incompatible#section-readme

创建一个 JWT

通常使用 NewWithClaims,因为我们可以通过匿名结构体来实现 Claims 接口,从而携带自己的参数。

源码中的 StandardClaims 结构体:

// Structured version of Claims Section, as referenced at
// https://tools.ietf.org/html/rfc7519#section-4.1
// See examples for how to use this with your own claim types
type StandardClaims struct {
    
    
	Audience  string `json:"aud,omitempty"`
	ExpiresAt int64  `json:"exp,omitempty"`
	Id        string `json:"jti,omitempty"`
	IssuedAt  int64  `json:"iat,omitempty"`
	Issuer    string `json:"iss,omitempty"`
	NotBefore int64  `json:"nbf,omitempty"`
	Subject   string `json:"sub,omitempty"`
}

其中最重要的三个参数:NoteBefore 生效时间、ExpiresAt 过期时间、Issuer 签发者

两种常用的 Claims 的实现方式:

  • 自定义结构体嵌入接口
type MyClaims struct {
    
    
	Username string `json:"username"`
	jwt.StandardClaims
}
  • 使用它提供的 map
type MapClaims map[string]interface{
    
    }

创建一个 Token:(详见后面的示例)

token := jwt.NewWithClaims(jwt.SigningMethodHS256, c) // 创建
s, err := t.SignedString(mySigningKey) // 签发

解析一个 JWT

主要通过以下方法实现:

func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) 

其中 keyFunc 是个特殊的回调函数,固定接受 *Token 类型指针,返回一个 i 和 err,i 就是我们的密钥

token, err := jwt.ParseWithClaims(s, &MyClaims{
    
    },
                                  func(t *jwt.Token) (interface{
    
    }, error) {
    
    
  return mySigningKey, nil
})

获取的 token 是一个 jwtToken 类型的数据,我们需要其中的 Claims

对 Claims 进行断言,然后取用即可

fmt.Println(token.Claims.(*MyClaims).Issuer)

示例:创建和解析 JWT

创建和解析 JWT 的示例:

type MyClaims struct {
    
    
	Username string `json:"username"`
	jwt.StandardClaims
}

func main() {
    
    
	// 创建一个 JWT
	mySigningKey := []byte("woxiangbianqiang!")
	c := MyClaims{
    
    
		Username: "qimiao",
		StandardClaims: jwt.StandardClaims{
    
    
			NotBefore: time.Now().Unix() - 60,      // 生效时间 1 分钟
			ExpiresAt: time.Now().Unix() + 2*60*60, // 失效时间 2 小时
			Issuer:    "lzy",                       // 签发者
		},
	}
	t := jwt.NewWithClaims(jwt.SigningMethodHS256, c) // 创建 Token
	s, _ := t.SignedString(mySigningKey) // 签发 Token
	fmt.Println(s) // token 的字符串

	// 解析 JWT
	token, err := jwt.ParseWithClaims(s, &MyClaims{
    
    }, 
                                    func(t *jwt.Token) (interface{
    
    }, error) {
    
    
		return mySigningKey, nil
	})
	if err != nil {
    
    
		fmt.Println(err)
	}
  // 对 Claims 进行断言并使用
	fmt.Println(token.Claims.(*MyClaims))
}

Casbin

官网:https://casbin.org/zh-CN/

官方文档:https://casbin.org/docs/zh-CN/overview

模拟学习:https://casbin.org/zh-CN/editor

Casbin 模型基础

Casbin 工作原理

在 Casbin 中, 访问控制模型被抽象为基于 PERM (Policy, Effect, Request, Matcher) 的一个文件。

PERM 模式由四个基础组成,描述资源与用户之间的关系:

  • Request 请求,定义了我们应该提供访问控制匹配功能的参数名称和顺序。

    例如:r = {sub, obj, act}

  • Policy 政策,在政策规则文件中定义字段的名称和顺序。

    例如:p = {sub, obj, act}p = {sub, obj, act, eft}

    注:如果未定义 eft,则策略文件中的结果字段将不会被读取,匹配的策略结果将默认被允许

  • Matcher 匹配规则,匹配请求和政策的规则。

    例如:m = r.sub == p.sub && r.act == p.act && r.obj == p.obj

  • Effect 影响,对匹配结果再次作出逻辑组合判断。

    例如:e = some (where (p.eft == allow))

Casbin 中最基本、最简单的 model 是 ACL。ACL 中的 model 配置为:

# Request definition
[request_definition]
r = sub, obj, act

# Policy definition
[policy_definition]
p = sub, obj, act

# Policy effect
[policy_effect]
e = some(where (p.eft == allow))

# Matchers
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

ACL 模型的一个示例策略类似:

p, alice, data1, read
p, bob, data2, write

它意味着:

  • alice 可以读取 data1
  • bob 可以读取 data2

RBAC

在模型中使用 RBAC 角色:基于角色的控制访问

编辑器 中选择 RBAC 模型。

实战模型

以角色为基础

[request_definition]
r = sub, obj, act
# 请求入参(实体,资源,方法)

[policy_definition]
p = sub, obj, act
# 策略(实体,资源,方法)

[role_definition]
g = _, _
# 这个情况下 g 写啥都行,因为 match 里没有涉及到 g
# 不过规范一点,按照角色权限,这里 g 接收两个参数
# g = 用户, 角色

[policy_effect]
e = some(where(p.eft==allow))
# 看看经过下面那些匹配规则后的返回值
# 是否有一条等于里面那个 allow

[matchers]
m = r.sub == p.sub && ParamsMatch(r.obj, p.obj) && r.act == p.act
# 进来的 实体、资源、方法 能不能在 权限表(p) 里面找到一个一模一样的

多租户模型

参考:域内 RBAC

r = sub, dom, obj, act
# 请求入参(实体,域【商户】,资源,方法)

[policy_definition]
p = sub, dom, obj, act
# 权限模型(实体,域【商户】,资源,方法)

[role_definition]
g = _, _, _
# 域匹配规则,后面g会说,这里意思是g接受3个参数

[policy_effect]
e = some(where(p.eft==allow))
# 看看经过下面那些匹配规则后的返回值
# 是否有一条等于里面那个 allow

[matchers]
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act
# 先列出来一个权限定义里面的东西 g, qm, teacher, classOne
# 然后用g做一些解析动作
# 一般我们会收到前端的入参, 大概长这样 "qm", "classOne", "/base/api", "get"
# 上面我们定义的g就是我们需要的模型

模拟

ACL 模型真的很简单,就是 Request 和 Policy 一一对应:

RBAC 模型也比较简单,在 ACL 的基础上,多了对角色 (role) 的定义。

RBAC with domains/tenants 多租户模型

Casbin 代码

提示:Casbin 的文档非常清晰,必须学会看文档!

本地文件模式

官方文档:Casbin 开始使用

  • 引入包:github.com/casbin/casbin/v2

  • 创建模型和 policy 并且引入

    e, _ := casbin.NewEnforcer("casbin/model.conf", "casbin/policy.csv")

  • 调用 api 并使用

model.conf

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

policy.csv

p, zhangsan, data1, read

main.go

import (
	"fmt"

	"github.com/casbin/casbin/v2"
)
func main() {
    
    
	// 读取配置文件
	e, _ := casbin.NewEnforcer("casbin/model.conf", "casbin/policy.csv")

	sub := "alice" // 想要访问资源的用户。
	obj := "data1" // 将被访问的资源。
	act := "read"  // 用户对资源执行的操作。

	// 添加权限, 代码中
	added, _ := e.AddPolicy("alice", "data1", "read")
	fmt.Println(added) // true

	ok, _ := e.Enforce(sub, obj, act)

	if ok {
    
    
    // 配置文件中没有, 但是在代码中添加了, 可以通过
		fmt.Println("通过")
	} else {
    
    
		fmt.Println("未通过")
	}
}

使用数据库存储 policy

文档:适配器 中寻找合适的适配器

import (
	"fmt"

	"github.com/casbin/casbin/v2"
	gormadapter "github.com/casbin/gorm-adapter/v3"
	_ "github.com/go-sql-driver/mysql"
)

func main() {
    
    
	// 连接数据库并自动建表
	a, _ := gormadapter.NewAdapter("mysql", 
                                 "root:lzy123456@tcp(127.0.0.1:3306)/casbin", 
                                 true)
	e, _ := casbin.NewEnforcer("casbin/model.conf", a)

	ok, _ := e.Enforce("alice", "data1", "read")

	if ok {
    
    
		fmt.Println("通过")
	} else {
    
    
		fmt.Println("未通过")
	}
}

数据库对 policy 进行增删改查

参考文档:Casbin 管理 API,这部分完全参考文档就可以了,很详细。

示例:获取策略中的所有授权规则。

func main() {
    
    
	// 连接数据库并自动建表
	a, _ := gormadapter.NewAdapter("mysql", 
                                 "root:lzy123456@tcp(127.0.0.1:3306)/casbin", true)
	e, _ := casbin.NewEnforcer("casbin/model.conf", a)
	
  // 获取策略中的所有授权规则
	policy := e.GetPolicy()
	fmt.Println(policy)
}

自定义比较函数

参考文档:Casbin 函数

import (
	"fmt"

	"github.com/casbin/casbin/v2"
	gormadapter "github.com/casbin/gorm-adapter/v3"
	_ "github.com/go-sql-driver/mysql"
)

func main() {
    
    
	// 连接数据库并自动建表
	a, _ := gormadapter.NewAdapter("mysql", 
                                 "root:lzy123456@tcp(127.0.0.1:3306)/casbin", true)
	e, _ := casbin.NewEnforcer("casbin/model.conf", a)

	// 注册自定义比较函数
	e.AddFunction("my_func", KeyMatchFunc)

	ok, _ := e.Enforce("alice", "data1", "read")

	if ok {
    
    
		fmt.Println("通过")
	} else {
    
    
		fmt.Println("未通过")
	}
}

// 自定义比较函数
func KeyMatch(key1 string, key2 string) bool {
    
    
	return key1 == "data1"
}

// 使用 interface{} 类型的接口包装一下
func KeyMatchFunc(args ...interface{
    
    }) (interface{
    
    }, error) {
    
    
	name1 := args[0].(string)
	name2 := args[1].(string)

	return (bool)(KeyMatch(name1, name2)), nil
}

可以在模型中使用该函数:

# ...

[matchers]
m = r.sub == p.sub && my_func(r.obj, p.obj) && r.act == p.act

猜你喜欢

转载自blog.csdn.net/weixin_43734095/article/details/123514971
今日推荐