路由

gin 框架中采纳的路由库是基于httprouter做的

Restful格调的API

Representational State Transfer 体现层状态转化,是一种互联网应用程序的API设计理念:

  • URL定位资源,用HTTP形容操作
  • 增 POST / 删 DELETE / 改 PUT / 查 GET

参数

  • API参数:Param办法

    r.GET("/:name/*action", func(c *gin.Context) {        name := c.Param("name")        action := c.Param("action")        // action = /yyy        action = strings.Trim(action, "/") //截取        c.String(http.StatusOK, name+" - "+action)    })
  • URL参数

    • DefaultQuery():参数不存在,返回默认值

      c.DefaultQuery("name", "枯藤")
    • Query():参数不存在,返回空
  • 表单参数:PostForm办法

    types := c.DefaultPostForm("type", "post")username := c.PostForm("username")

上传文件

  • multipart/form-data格局用于文件上传
  • gin文件上传与原生的net/http办法相似,不同在于gin把原生的request封装到c.Request中

上传单个文件

<form action="http://localhost:8080/upload" method="post" enctype="multipart/form-data">      上传文件:<input type="file" name="file" >      <input type="submit" value="提交"></form>
//限度上传最大尺寸r.MaxMultipartMemory = 8 << 20r.POST("/upload", func(c *gin.Context) {    file, err := c.FormFile("file")    if err != nil {        c.String(500, "上传图片出错")        return    }    // c.JSON(200, gin.H{"message": file.Header.Context})    c.SaveUploadedFile(file, file.Filename)    c.String(http.StatusOK, file.Filename)})

上传限度

  • 限度文件类型:headers.Header.Get("Content-Type")
  • 限度文件大小:headers.Size
r.POST("/upload-limit", func(c *gin.Context) {    _, headers, err := c.Request.FormFile("file")    if err != nil {        c.String(500, "上传图片出错")    return    }    if headers.Size > 1024*1024*2 {        c.String(500, "图片太大了")        return    }    t := headers.Header.Get("Content-Type")    if t != "image/jpeg" {        c.String(500, "图片格式谬误:"+t)        return    }    // c.JSON(200, gin.H{"message": file.Header.Context})    c.SaveUploadedFile(headers, "./upload/"+headers.Filename)    c.String(http.StatusOK, headers.Filename)})

上传多个文件

<form action="http://localhost:8000/upload" method="post" enctype="multipart/form-data">      上传文件:<input type="file" name="files" multiple>      <input type="submit" value="提交"></form>
r.POST("/upload", func(c *gin.Context) {   form, err := c.MultipartForm()   if err != nil {      c.String(http.StatusBadRequest, fmt.Sprintf("get err %s", err.Error()))   }   // 获取所有图片   files := form.File["files"]   // 遍历所有图片   for _, file := range files {      // 一一存      if err := c.SaveUploadedFile(file, file.Filename); err != nil {         c.String(http.StatusBadRequest, fmt.Sprintf("upload err %s", err.Error()))         return      }   }   c.String(200, fmt.Sprintf("upload ok %d files", len(files)))})

分组 Group

v1 := r.Group("/v1"){   v1.GET("/login", login)   v1.GET("submit", submit)}

数据解析和绑定

type Login struct {   // binding:"required"润饰的字段,若接管为空值,则报错,是必须字段   User    string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`   Password string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`}

Json

var json Login// 将request的body中的数据,主动依照json格局解析到构造体c.ShouldBindJSON(&json); //json.User json.Password

表单

<form action="http://localhost:8000/loginForm" method="post" enctype="application/x-www-form-urlencoded">    用户名<input type="text" name="username"><br>    明码<input type="password" name="password">    <input type="submit" value="提交"></form>
var form Loginc.Bind(&form); // form.User form.Password

URI

r.GET("/:user/:password", func(c *gin.Context) {    var login Login    c.ShouldBindUri(&login);// login.User login.Password}  

渲染

数据渲染

  • JSON

    c.JSON(200, gin.H{"msg": "json"}) //{"msg":"json"}
  • XML

    c.XML(200, gin.H{"msg": "xml"}) //<map><msg>xml</msg></map>
  • YAML

    c.YAML(200, gin.H{"msg": "yaml"}) //文件下载 msg: yaml
  • ProtoBuf

    reps := []int64{int64(1), int64(2)}// 定义数据label := "label"// 传protobuf格局数据data := &protoexample.Test{    Label: &label,    Reps:  reps,}r.GET("/proto", func(c *gin.Context) {    c.ProtoBuf(200, data) //文件下载 label})

HTML渲染

  • gin反对加载HTML模板, 而后依据模板参数进行配置并返回相应的数据,实质上就是字符串替换
  • LoadHTMLGlob()办法能够加载模板文件
r.LoadHTMLGlob("html/*.html")r.GET("/html", func(c *gin.Context) {    c.HTML(200, "index.html", gin.H{"name": "test"})})

重定向

//301重定向c.Redirect(http.StatusMovedPermanently, "https://blog.itxiaoma.cn")

异步

  • 在启动新的goroutine时,不应该应用原始上下文,必须应用它的只读正本
copyContext := c.Copy() // Context正本go func() {   time.Sleep(3 * time.Second)   log.Println("异步执行:" + copyContext.Request.URL.Path) //异步执行:/async}()c.String(200, "Sync...")

中间件

  • 中间件(Middleware)指的是能够拦挡http申请-响应生命周期的非凡函数
  • Gin默认应用了Logger(), Recovery()两个全局中间件

    //去除默认全局中间件r := gin.New()//不带中间件

全局中间件

func FullMiddleware() gin.HandlerFunc {    return func(c *gin.Context) {        c.Set("request", "中间件")    }}
r.Use(FullMiddleware())//应用中间件r.GET("/middleware", func(c *gin.Context) {   req, _ := c.Get("request")   c.String(200, req.(string)) })

Next

  • c.Next() 之前的操作是在 Handler 执行之前就执行;个别做验证解决,拜访是否容许。
  • c.Next() 之后的操作是在 Handler 执行之后再执行;个别是做总结解决,比方格式化输入、响应完结工夫,响应时长计算。
func NextMiddleware() gin.HandlerFunc {    return func(c *gin.Context) {        fmt.Println("中间件开始执行")        c.Next()        fmt.Println("中间件执行结束")    }}func NextResponse(r *gin.Engine) {    r.Use(NextMiddleware())    r.GET("/next", func(c *gin.Context) {        fmt.Println("申请执行...")        // 中间件开始执行        // 申请执行...        // 中间件执行结束    })}

Abort

  • 表⽰终⽌,也就是说,执⾏Abort的时候会停⽌所有的后⾯的中间件函数的调⽤。
func AbortMiddleware() gin.HandlerFunc {   return func(c *gin.Context) {      fmt.Println("中间件开始执行")      if c.Query("key") == "abort" {         c.String(200, "Abort")         c.Abort()      }   }}func AbortResponse(r *gin.Engine) {   r.Use(AbortMiddleware())   r.GET("/abort", func(c *gin.Context) {      fmt.Println("申请执行...")      c.String(200, "OK")   })}

部分中间件

//部分中间件func PartMiddleware() gin.HandlerFunc {   return func(c *gin.Context) {      fmt.Println("中间件开始执行")   }}func PartResponse(r *gin.Engine) {   r.GET("/part", PartMiddleware(), func(c *gin.Context) {      fmt.Println("申请执行...")      // 中间件开始执行      // 申请执行...   })}

中间件举荐

  • RestGate - REST API端点的平安身份验证
  • staticbin - 用于从二进制数据提供动态文件的中间件/处理程序
  • gin-cors - CORS杜松子酒的官网中间件
  • gin-csrf - CSRF爱护
  • gin-health - 通过gocraft/health报告的中间件
  • gin-merry - 带有上下文的丑陋 打印 谬误的中间件
  • gin-revision - 用于Gin框架的订正中间件
  • gin-jwt - 用于Gin框架的JWT中间件
  • gin-sessions - 基于mongodb和mysql的会话中间件
  • gin-location - 用于公开服务器的主机名和计划的中间件
  • gin-nice-recovery - 紧急复原中间件,可让您构建更好的用户体验
  • gin-limit - 限度同时申请;能够帮忙减少交通流量
  • gin-limit-by-key - 一种内存中的中间件,用于通过自定义键和速率限度拜访速率。
  • ez-gin-template - gin简略模板包装
  • gin-hydra - gin中间件Hydra
  • gin-glog - 旨在代替Gin的默认日志
  • gin-gomonitor - 用于通过Go-Monitor公开指标
  • gin-oauth2 - 用于OAuth2
  • static gin框架的代替动态资产处理程序。
  • xss-mw - XssMw是一种中间件,旨在从用户提交的输出中“主动删除XSS”
  • gin-helmet - 简略的平安中间件汇合。
  • gin-jwt-session - 提供JWT / Session / Flash的中间件,易于应用,同时还提供必要的调整选项。也提供样品。
  • gin-template - 用于gin框架的html / template易于应用。
  • gin-redis-ip-limiter - 基于IP地址的申请限制器。它能够与redis和滑动窗口机制一起应用。
  • gin-method-override - _method受Ruby的同名机架启发而被POST形式参数笼罩的办法
  • gin-access-limit - limit-通过指定容许的源CIDR表示法的访问控制中间件。
  • gin-session - 用于Gin的Session中间件
  • gin-stats - 轻量级和有用的申请指标中间件
  • gin-statsd - 向statsd守护过程报告的Gin中间件
  • gin-health-check - check-用于Gin的健康检查中间件
  • gin-session-middleware - 一个无效,平安且易于应用的Go Session库。
  • ginception - 丑陋的例外页面
  • gin-inspector - 用于考察http申请的Gin中间件。
  • gin-dump - Gin中间件/处理程序,用于转储申请和响应的标头/注释。对调试应用程序十分有帮忙。
  • go-gin-prometheus - Gin Prometheus metrics exporter
  • ginprom - Gin的Prometheus指标导出器
  • gin-go-metrics - Gin middleware to gather and store metrics using rcrowley/go-metrics
  • ginrpc - Gin 中间件/处理器主动绑定工具。通过像beego这样的正文路线来反对对象注册

原文: https://github.com/gin-gonic/...

会话管制

Cookie

func CookieHandler(r *gin.Engine) {   r.GET("/cookie", func(c *gin.Context) {      cookie, err := c.Cookie("test")      if err != nil {         c.SetCookie(            "test",            "value",            60,          //maxAge int, 单位为秒            "/",         //path,cookie所在目录            "localhost", //domain string,域名            false,       //secure 是否智能通过https拜访            true,        //httpOnly bool  是否容许他人通过js获取本人的cookie         )      }      c.String(200, cookie)   })}

Cookie校验

func AuthMiddleWare() gin.HandlerFunc {   return func(c *gin.Context) {      // 获取客户端cookie并校验      if cookie, err := c.Cookie("abc"); err == nil {         if cookie == "123" {            c.Next()            return         }      }      // 返回谬误      c.JSON(http.StatusUnauthorized, gin.H{"error": "err"})      // 若验证不通过,不再调用后续的函数解决      c.Abort()      return   }}

Session

装置

go get github.com/gin-contrib/sessions

应用

var store = cookie.NewStore([]byte("secret"))func SessionHandler(r *gin.Engine) {   r.GET("/session",      sessions.Sessions("mysession", store), //路由上退出session中间件      func(c *gin.Context) {         session := sessions.Default(c)         if session.Get("name") != "itxiaoma" {            session.Set("name", "itxiaoma")            //记着调用save办法,写入session            session.Save()         }         c.JSON(200, gin.H{"name": session.Get("name")}) //{"name":"itxiaoma"}      })}

参数验证

用gin框架的数据验证,能够不必解析数据,缩小if else,会简洁许多。

构造体验证

type Person struct {   Name string `form:"name"`}func JsonHandler(r *gin.Engine) {   r.GET("/structure", func(c *gin.Context) {      var person Person      c.ShouldBind(&person)      c.String(200, fmt.Sprintf("%#v", person))      //拜访:http://localhost:8080/structure?name=xxx      //输入:structure.Person{Name:"xxx"}   })}

自定义验证

对绑定解析到构造体上的参数,自定义验证性能

官网示例:https://github.com/gin-gonic/...

引入

go get github.com/go-playground/validator/v10

应用:

package validatorimport (    "github.com/gin-gonic/gin"    "github.com/gin-gonic/gin/binding"    "github.com/go-playground/validator/v10"    "net/http")type Person struct {    Name string `form:"name" binding:"NotAdmin"` // 1、自定义注册名称}// 2、自定义校验办法var notAdmin validator.Func = func(fl validator.FieldLevel) bool {    name, ok := fl.Field().Interface().(string)    if ok {        return name != "admin"    }    return true}func MyValidatorHandler(r *gin.Engine) {    // 3、将自定义的校验办法注册到 validator 中    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {        // 这里的 key 和 fn 能够不一样最终在 struct 应用的是 key        v.RegisterValidation("NotAdmin", notAdmin)    }    r.GET("/validator", func(c *gin.Context) {        var person Person        if e := c.ShouldBind(&person); e == nil {            c.String(http.StatusOK, "%v", person)        } else {            c.String(http.StatusOK, "person bind err:%v", e.Error())            //person bind err:Key: 'Person.Name' Error:Field validation for 'Name' failed on the 'NotAdmin' tag        }    })}

Multipart/Urlencoded 绑定

package mainimport (    "github.com/gin-gonic/gin")type LoginForm struct {    User     string `form:"user" binding:"required"`    Password string `form:"password" binding:"required"`}func main() {    router := gin.Default()    router.POST("/login", func(c *gin.Context) {        // 你能够应用显式绑定申明绑定 multipart form:        // c.ShouldBindWith(&form, binding.Form)        // 或者简略地应用 ShouldBind 办法主动绑定:        var form LoginForm        // 在这种状况下,将主动抉择适合的绑定        if c.ShouldBind(&form) == nil {            if form.User == "user" && form.Password == "password" {                c.JSON(200, gin.H{"status": "you are logged in"})            } else {                c.JSON(401, gin.H{"status": "unauthorized"})            }        }    })    router.Run(":8080")}

测试:

curl -v --form user=user --form password=password http://localhost:8080/login

其余类型:URL参数

c.ShouldBindWith(&p, binding.Query);

其余

日志

f, _ := os.Create("log/gin.log")gin.DefaultWriter = io.MultiWriter(f)r = gin.Default()r.GET("/log", func(c *gin.Context) {   //同时将日志写入文件和控制台   //gin.DefaultWriter = io.MultiWriter(f, os.Stdout)   c.String(200, "log ok")})r.Run()

验证码

执行逻辑:

  1. 先在session里写入键值对(k->v),把值写在图片上,而后生成图片,显示在浏览器下面
  2. 前端将验证码发送给后后端,后端依据session中的k获得v,比对校验
package captchaimport (   "bytes"   "github.com/dchest/captcha"   "github.com/gin-contrib/sessions"   "github.com/gin-contrib/sessions/cookie"   "github.com/gin-gonic/gin"   "net/http"   "time")var sessionMaxAge = 3600var sessionSecret = "itxiaoma"func SessionMiddleware(keyPairs string) gin.HandlerFunc {   var store sessions.Store   store = cookie.NewStore([]byte(sessionSecret))   store.Options(sessions.Options{      MaxAge: sessionMaxAge, //seconds      Path:   "/",   })   return sessions.Sessions(keyPairs, store)}func Captcha(c *gin.Context, length ...int) {   l := captcha.DefaultLen   w, h := 107, 36   if len(length) == 1 {      l = length[0]   }   if len(length) == 2 {      w = length[1]   }   if len(length) == 3 {      h = length[2]   }   captchaId := captcha.NewLen(l)   session := sessions.Default(c)   session.Set("captcha", captchaId)   _ = session.Save()   _ = Serve(c.Writer, c.Request, captchaId, ".png", "zh", false, w, h)}func CaptchaVerify(c *gin.Context, code string) bool {   session := sessions.Default(c)   if captchaId := session.Get("captcha"); captchaId != nil {      session.Delete("captcha")      _ = session.Save()      if captcha.VerifyString(captchaId.(string), code) {         return true      } else {         return false      }   } else {      return false   }}func Serve(w http.ResponseWriter, r *http.Request, id, ext, lang string, download bool, width, height int) error {   w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")   w.Header().Set("Pragma", "no-cache")   w.Header().Set("Expires", "0")   var content bytes.Buffer   switch ext {   case ".png":      w.Header().Set("Content-Type", "image/png")      _ = captcha.WriteImage(&content, id, width, height)   case ".wav":      w.Header().Set("Content-Type", "audio/x-wav")      _ = captcha.WriteAudio(&content, id, lang)   default:      return captcha.ErrNotFound   }   if download {      w.Header().Set("Content-Type", "application/octet-stream")   }   http.ServeContent(w, r, id+ext, time.Time{}, bytes.NewReader(content.Bytes()))   return nil}func main() {  r := gin.Default()  r.LoadHTMLGlob("captcha/*.html")  r.Use(SessionMiddleware("itxiaoma"))  r.GET("/captcha", func(c *gin.Context) {     Captcha(c, 4)  })  r.GET("/captcha-html", func(c *gin.Context) {     c.HTML(http.StatusOK, "captcha.html", nil)  })  r.GET("/captcha/verify/:value", func(c *gin.Context) {     value := c.Param("value")     if CaptchaVerify(c, value) {        c.JSON(http.StatusOK, gin.H{"status": 0, "msg": "success"})     } else {        c.JSON(http.StatusOK, gin.H{"status": 1, "msg": "failed"})     }  })  r.Run()}

JWT

package jwtimport (   "fmt"   "github.com/dgrijalva/jwt-go"   "github.com/gin-gonic/gin"   "net/http"   "time")//自定义字符串var jwtkey = []byte("itxiaoma")var str stringtype Claims struct {   UserId uint   jwt.StandardClaims}//颁发tokenfunc setting(ctx *gin.Context) {   expireTime := time.Now().Add(7 * 24 * time.Hour)   claims := &Claims{      UserId: 2,      StandardClaims: jwt.StandardClaims{         ExpiresAt: expireTime.Unix(), //过期工夫         IssuedAt:  time.Now().Unix(),         Issuer:    "127.0.0.1",  // 签名颁发者         Subject:   "user token", //签名主题      },   }   token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)   // fmt.Println(token)   tokenString, err := token.SignedString(jwtkey)   if err != nil {      fmt.Println(err)   }   str = tokenString   ctx.JSON(200, gin.H{"token": tokenString})}//解析tokenfunc getting(ctx *gin.Context) {   tokenString := ctx.GetHeader("Authorization")   fmt.Println(tokenString)   //vcalidate token formate   if tokenString == "" {      ctx.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "权限有余1"})      ctx.Abort()      return   }   token, claims, err := ParseToken(tokenString)   if err != nil || !token.Valid {      ctx.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "权限有余2"})      ctx.Abort()      return   }   ctx.JSON(200, gin.H{"claims": claims})}func ParseToken(tokenString string) (*jwt.Token, *Claims, error) {   Claims := &Claims{}   token, err := jwt.ParseWithClaims(tokenString, Claims, func(token *jwt.Token) (i interface{}, err error) {      return jwtkey, nil   })   return token, Claims, err}func main() {     r := gin.Default()   r.GET("/set-jwt", setting)   r.GET("/get-jwt", getting)   r.Run()}

调试

curl http://localhost:8080/get-jwt -H "Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjIsImV4cCI6MTY1NDA1NzQ1MCwiaWF0IjoxNjUzNDUyNjUwLCJpc3MiOiIxMjcuMC4wLjEiLCJzdWIiOiJ1c2VyIHRva2VuIn0.IN_Tj-M6CMHFlunnRIvUgog2GMDyWpj7iOsjwUeD0Sk"

权限治理 Casbin

Casbin是用于Golang我的项目的功能强大且高效的开源访问控制库。

权限实际上就是管制能对什么资源进行什么操作。

Casbin将访问控制模型形象到一个基于 PERM(Policy,Effect,Request,Matchers) 元模型的配置文件(模型文件)中。因而切换或更新受权机制只须要简略地批改配置文件。

引入:

go get github.com/casbin/casbin/v2

模型文件 model.conf

[request_definition]r = sub, obj, act[policy_definition]p = sub, obj, act[role_definition]g = _, _[policy_effect]e = some(where (p.eft == allow))[matchers]m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
  • request是对拜访申请的形象,它与e.Enforce()函数的参数是一一对应的
  • policy是策略或者说是规定的定义。它定义了具体的规定。
  • request是对拜访申请的形象,它与e.Enforce()函数的参数是一一对应的matcher匹配器会将申请与定义的每个policy一一匹配,生成多个匹配后果。
  • effect依据对申请使用匹配器得出的所有后果进行汇总,来决定该申请是容许还是回绝。

sub:拜访实体 obj:拜访对象 act:拜访动作

下面模型文件规定了权限由sub,obj,act三要素组成,只有在策略列表中有和它完全相同的策略时,该申请能力通过。匹配器的后果能够通过p.eft获取,some(where (p.eft == allow))示意只有有一条策略容许即可。

文件存储策略 policy.csv

即谁能对什么资源进行什么操作:

p, dajun, data1, readp, lizi, data2, write

文件的两行内容示意dajun对数据data1read权限,lizi对数据data2write权限

func check(e *casbin.Enforcer, sub, obj, act string) {  ok, _ := e.Enforce(sub, obj, act)  if ok {    fmt.Printf("%s CAN %s %s\n", sub, act, obj)  } else {    fmt.Printf("%s CANNOT %s %s\n", sub, act, obj)  }}func main() {  e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")  if err != nil {    log.Fatalf("NewEnforecer failed:%v\n", err)  }  check(e, "dajun", "data1", "read") //dajun CAN read data1  check(e, "lizi", "data2", "write") //lizi CAN write data2  check(e, "dajun", "data1", "write")//dajun CANNOT write data1  check(e, "dajun", "data2", "read") //dajun CANNOT read data2}

超级管理员:

[matchers]e = r.sub == p.sub && r.obj == p.obj && r.act == p.act || r.sub == "root"

校验

check(e, "root", "data1", "read")check(e, "root", "data2", "write")check(e, "root", "data1", "execute")check(e, "root", "data3", "rwx")

RBAC模型

模型文件:

[role_definition]g = _, _[matchers]m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

g = _,_定义了用户——角色,角色——角色的映射关系,前者是后者的成员,领有后者的权限。

g(r.sub, p.sub)用来判断申请主体r.sub是否属于p.sub这个角色。

Gorm Adapter

数据:

CREATE DATABASE IF NOT EXISTS casbin;USE casbin;CREATE TABLE IF NOT EXISTS casbin_rule (  p_type VARCHAR(100) NOT NULL,  v0 VARCHAR(100),  v1 VARCHAR(100),  v2 VARCHAR(100),  v3 VARCHAR(100),  v4 VARCHAR(100),  v5 VARCHAR(100));INSERT INTO casbin_rule VALUES('p', 'dajun', 'data1', 'read', '', '', ''),('p', 'lizi', 'data2', 'write', '', '', '');

而后应用Gorm Adapter加载policyGorm Adapter默认应用casbin库中的casbin_rule表:

package mainimport (  "fmt"  "github.com/casbin/casbin/v2"  gormadapter "github.com/casbin/gorm-adapter/v2"  _ "github.com/go-sql-driver/mysql")func check(e *casbin.Enforcer, sub, obj, act string) {  ok, _ := e.Enforce(sub, obj, act)  if ok {    fmt.Printf("%s CAN %s %s\n", sub, act, obj)  } else {    fmt.Printf("%s CANNOT %s %s\n", sub, act, obj)  }}func main() {  a, _ := gormadapter.NewAdapter("mysql", "root:12345@tcp(127.0.0.1:3306)/")  e, _ := casbin.NewEnforcer("./model.conf", a)  check(e, "dajun", "data1", "read")  check(e, "lizi", "data2", "write")  check(e, "dajun", "data1", "write")  check(e, "dajun", "data2", "read")}

运行:

dajun CAN read data1lizi CAN write data2dajun CANNOT write data1dajun CANNOT read data2

其余

Gin中文文档:https://learnku.com/docs/gin-...

Reference

gin框架

Gin 框架中文文档

gin 中应用session

Gin+Gorm+Casbin实现权限管制demo

Go 每日一库之 casbin