路由
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 << 20
r.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 Login
c.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 validator
import (
"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 main
import (
"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()
验证码
执行逻辑:
- 先在session里写入键值对(k->v),把值写在图片上,而后生成图片,显示在浏览器下面
- 前端将验证码发送给后后端,后端依据session中的k获得v,比对校验
package captcha
import (
"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 = 3600
var 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 jwt
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
//自定义字符串
var jwtkey = []byte("itxiaoma")
var str string
type Claims struct {
UserId uint
jwt.StandardClaims
}
//颁发token
func 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})
}
//解析token
func 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, read
p, lizi, data2, write
文件的两行内容示意dajun
对数据data1
有read
权限,lizi
对数据data2
有write
权限
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
加载policy
,Gorm Adapter
默认应用casbin
库中的casbin_rule
表:
package main
import (
"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 data1
lizi CAN write data2
dajun CANNOT write data1
dajun CANNOT read data2
其余
Gin中文文档:https://learnku.com/docs/gin-…
Reference
gin框架
Gin 框架中文文档
gin 中应用session
Gin+Gorm+Casbin实现权限管制demo
Go 每日一库之 casbin
发表回复