Gin框架使用Casbin进行用户权限校验

以下是测试项目目录

在这里插入图片描述

一、配置model

conf/casbin_rbac_model.conf

# 请求
[request_definition]
r = sub,obj,act
# sub ——> 想要访问资源的用户角色(Subject)——请求实体
# obj ——> 访问的资源(Object)
# act ——> 访问的方法(Action: get、post...)

# 策略(.csv文件p的格式,定义的每一行为policy rule;p,p2为policy rule的名字。)
[policy_definition]
p = sub,obj,act
# p2 = sub,act 表示sub对所有资源都能执行act

# 组定义
[role_definition]
g = _, _
# _,_表示用户,角色/用户组
# g2 = _,_,_ 表示用户, 角色/用户组, 域(也就是租户)

# 策略效果
[policy_effect]
e = some(where (p.eft == allow))
# 上面表示有任意一条 policy rule 满足, 则最终结果为 allow;p.eft它可以是allow或deny,它是可选的,默认是allow

# 匹配器
[matchers]
m = r.sub == p.sub && keyMatch(r.obj,p.obj) && (r.act==p.act || p.act == "*") || r.sub=="superTest"
# r.sub="xxx"表示实体为superTest的直接通过


二、配置policy

官方文档的示例使用.csv文件来进行管理,这样不利于动态管理,这里我使用mysql数据库来进行存储

需要用到的包:

# 贴出的代码中保留了所有用到的import的包

controler/casbins.go

package controler

import (
	"casbin_test/logic"
	"casbin_test/models"
	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"net/http"
)

/**
 *@Method 添加规则入口函数
 *@Params
 *@Return
 *@Tips:
 */
func AddPHandler(c *gin.Context){
	var cb = new(models.AddPReq)
	err := c.ShouldBindWith(cb, binding.Form)
	if err != nil {
		c.JSON(http.StatusBadRequest,gin.H{
			"success":false,
			"msg":"求情参数异常",
		})
		return
	}
	_, err = logic.AddPLogic(cb)
	if err != nil {
		c.JSON(http.StatusOK,gin.H{
			"success":false,
			"msg":"添加失败",
		})
		return
	}
	c.JSON(http.StatusOK,gin.H{
		"success":true,
		"msg":"添加成功",
	})
}

logic/casbins.go

package logic

import (
	"casbin_test/dao"
	"casbin_test/models"
)

/**
 *@Method 添加规则逻辑函数
 *@Params
 *@Return
 *@Tips:
 */
func AddPLogic(cb *models.AddPReq) (bool, error) {
	e := dao.Casbin()
	//policy := e.GetPolicy()
	return e.AddPolicy(cb.RoleName, cb.Path, cb.Method) // 查看源码发现库中对已存在重复的规则进行了处理
}

dao/mysql.go

package dao

import (
	"fmt"
	"github.com/Blank-Xu/sqlx-adapter"
	"github.com/casbin/casbin/v2"
	_ "github.com/go-sql-driver/mysql"
	"github.com/jmoiron/sqlx"
	"log"
	"time"
)

var MDB *sqlx.DB

/**
 *@Method 数据库初始化
 *@Params
 *@Return
 *@Tips:
 */
func Init() (err error) {
	dsn := fmt.Sprintf("root:root@tcp(127.0.0.1:3306)/casbin_test?charset=utf8mb4&parseTime=true")
	MDB, err = sqlx.Connect("mysql", dsn)
	if err != nil {
		log.Fatal("connect MDB failed:",err.Error())
		return
	}
	// 设置最大连接数
	MDB.SetMaxOpenConns(20)
	MDB.SetMaxIdleConns(10)
	MDB.SetConnMaxLifetime(time.Minute*5)
	return
}

/**
 *@Method 关闭连接
 *@Params
 *@Return
 *@Tips:
 */
func CloseMysql(db *sqlx.DB) {
	err := db.Close()
	if err != nil {
		panic(err)
	}
}

/**
 *@Method 实例化
 *@Params
 *@Return
 *@Tips:
 */
func Casbin() *casbin.Enforcer{
	var err error
	a, err := sqlxadapter.NewAdapter(MDB, "casbin_rule")
	if err != nil {
		log.Fatal(err.Error())
	}
	e, err := casbin.NewEnforcer("./conf/casbin_rbac_model.conf", a)
	if err != nil {
		log.Fatal(err.Error())
	}
	// 加载规则
	err = e.LoadPolicy()
	if err != nil {
		fmt.Printf("加载失败,error:%s",err.Error())
	}
	return e
}

models/casbin_model.go

package models

type AddPReq struct {
	RoleName string `form:"rolename" binding:"required"`
	Path     string `form:"path" binding:"required"`
	Method   string `form:"method" binding:"required"`
}

三、配置jwt和权限校验中间件,模拟用户登录

pkg/jwt/jwt.go

package jwt

import (
	"errors"
	"github.com/dgrijalva/jwt-go"
	"time"
)

const (
	AcceptTokenKey             = "ACToken"
	RefreshTokenKey            = "RFToken"
	TokenIssuer                = "testIssuer"
	mySecret                   = "666test"
	TokenExpireDuration        = time.Hour * 24 *7
	RefreshTokenExpireDuration = time.Hour * 24 * 30
)

var (
	// 预设错误信息
	TokenExpired     = errors.New("Token is expired")
	TokenNotValidYet = errors.New("Token not active yet")
	TokenMalformed   = errors.New("That's not even a token")
	TokenInvalid     = errors.New("Couldn't handle this token:")
)

type MyClaims struct {
	UserName string `json:"username"`
	Role string `json:"role"`
	jwt.StandardClaims
}

func GenAccessToken(userName,role string) (aToken string, err error) {
	// 创建一个我们自己的声明的数据
	cl := MyClaims{
		UserName: userName,
		Role: role,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间
			Issuer:    TokenIssuer,
		},
	}
	// 使用指定的签名方法创建签名对象
	aToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, cl).SignedString([]byte(mySecret))
	return
}

func GenRefreshToken() (rToken string, err error) {
	// refresh token
	rToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
		ExpiresAt: time.Now().Add(RefreshTokenExpireDuration).Unix(),
		Issuer:    TokenIssuer,
	}).SignedString([]byte(mySecret))
	return
}

func ParseToken(tokenString string) (*MyClaims, error) {
	// 解析token
	var mc = new(MyClaims)
	token, err := jwt.ParseWithClaims(tokenString, mc, func(token *jwt.Token) (interface{}, error) {
		return []byte(mySecret), nil
	})
	if err != nil {
		// 如果强转*jwt.ValidationError成功,对错误进行判断
		if validationError, ok := err.(*jwt.ValidationError); ok {
			/*
				当validationError中的错误信息由错误的token结构引起时,
				**************************************************
				源码vErr.Errors |= ValidationErrorExpired,
				或运算,只有都为0才为0,0000 0000|0000 0101 = 0000 0101
				由于vErr.Errors的初始值为0,所以等价于将ValidationErrorMalformed赋值给validationError的Errors,
				*****************************************************
				如果没有赋值,Errors的初始值为0,那么validationError.Errors&jwt.ValidationErrorMalformed = 0,
				赋值后造成validationError.Errors不为0,那么validationError.Errors&jwt.ValidationErrorMalformed != 0
			*/
			if validationError.Errors&jwt.ValidationErrorMalformed != 0 {
				return nil, TokenMalformed
				// 以下与上方原理相同
			} else if validationError.Errors&jwt.ValidationErrorExpired != 0 {
				return nil, TokenExpired
			} else if validationError.Errors&jwt.ValidationErrorNotValidYet != 0 {
				return nil, TokenNotValidYet
			} else {
				return nil, TokenInvalid
			}
		}
	}
	if token != nil {
		// 强转成jwtClaims
		if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
			// 如果合法返回claims
			return claims, nil
		}
		return nil, TokenInvalid
	} else {
		return nil, TokenInvalid
	}
}

func RefreshToken(aToken, rToken string) (newAToken string, err error) {
	// refresh token 无效直接返回
	if _, err = jwt.Parse(rToken, func(token *jwt.Token) (interface{}, error) {
		return []byte(mySecret), nil
	}); err != nil {
		return
	}

	// 从旧的token解析出claims数据
	var claims = new(MyClaims)
	_, err = jwt.ParseWithClaims(aToken, claims, func(token *jwt.Token) (interface{}, error) {
		return []byte(mySecret), nil
	})
	v, _ := err.(*jwt.ValidationError)
	if v.Errors == jwt.ValidationErrorExpired {
		return GenAccessToken(claims.UserName,claims.Role)
	}
	return
}

middleware/auth.go

package middleware

import (
	"github.com/gin-gonic/gin"
	myJwt "casbin_test/pkg/jwt"
	"net/http"
)

func JWTAuthMiddleWare() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		// 获取前端传回的token(传递方式不同,获取的位置也不同,根据实际情况选择)
		authHeader := ctx.Request.Header.Get(myJwt.AcceptTokenKey)
		// 无token直接返回错误
		if authHeader == "" {
			ctx.JSON(http.StatusOK, gin.H{
				"success": false,
				"msg":     "未登录或非法访问",
			})
			// 校验失败终止后续操作
			ctx.Abort()
			return
		}
		// 解析token
		claims, err := myJwt.ParseToken(authHeader)
		// 错误处理
		if err != nil {
			ctx.JSON(http.StatusOK, gin.H{
				"success": false,
				"msg":     err.Error(),
			})
			ctx.Abort()
			return
		}
		// 将claim加入上下文,便于后续使用
		ctx.Set("userClaim", claims)
		ctx.Next()
	}
}

middleware/permission.go

package middleware

import (
	"casbin_test/dao"
	myJwt "casbin_test/pkg/jwt"
	"github.com/gin-gonic/gin"
	"net/http"
)

func PermissionMiddleWare() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 获取claim
		claim := c.MustGet("userClaim").(*myJwt.MyClaims)
		e := dao.Casbin()
		// 检查用户权限
		isPass, err := e.Enforce(claim.Role, c.Request.URL.Path, c.Request.Method)
		if err != nil {
			c.JSON(http.StatusOK, gin.H{
				"success": false,
				"msg":     err.Error(),
			})
			c.Abort()
			return
		}
		if isPass {
			c.Next()
		} else {
			c.JSON(http.StatusOK, gin.H{
				"success": false,
				"msg":     "无此权限",
			})
			c.Abort()
			return
		}
	}
}

models/users.go

package models

type LoginReq struct {
	UserName string `form:"username" binding:"required"`
	Password string `form:"password" binding:"required"`
}

controler/user.go

package controler

import (
	"casbin_test/models"
	myJwt "casbin_test/pkg/jwt"
	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"net/http"
)

var (
	name1       = "熊大"
	name2       = "熊二"
  name3       = "光头强"
	nameRoleMap = map[string]string{
		"熊大": "superTest",
		"熊二": "admin",
		"光头强": "staff",
	}
)

// 模拟登陆
func Login(c *gin.Context) {
	var u = new(models.LoginReq)
	err := c.ShouldBindWith(u, binding.Form)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"success": false,
			"msg":     "参数错误",
		})
		return
	}
	if u.UserName == name1 || u.UserName == name2 || u.UserName == name3 && u.Password == "123456" {
		// 获取token
		token, err := myJwt.GenAccessToken(u.UserName, nameRoleMap[u.UserName])
		if err != nil {
			c.JSON(http.StatusOK, gin.H{
				"success": false,
				"msg":     "对不起我崩了!",
			})
		}
		c.JSON(http.StatusOK, gin.H{
			"success": true,
			"msg":     "登陆成功",
			"data":    token,
		})
	} else {
		c.JSON(http.StatusOK, gin.H{
			"success": false,
			"msg":     "用户名或密码错误!",
		})
	}

}

main.go

package main

import (
	"casbin_test/controler"
	"casbin_test/dao"
	"casbin_test/middleware"
	"github.com/gin-gonic/gin"
	"log"
	"runtime"
)

func main() {
	err := dao.Init()
	if err != nil {
		log.Fatal(err.Error())
	}
	runtime.SetFinalizer(dao.MDB, dao.CloseMysql)
	router := gin.Default()
	r := router.Group("/api")
	r.POST("/login", controler.Login)
	r.Use(middleware.JWTAuthMiddleWare()).Use(middleware.PermissionMiddleWare()) // 测试时可先注释,先添加规则
	r.POST("/addcasbin",controler.AddPHandler)

	router.Run(":8081")

}

四、使用postman测试效果

此时我预先只配置了一个规则:

在这里插入图片描述

模拟登录中预设的user和role对应关系为:

在这里插入图片描述

按照匹配的规则,应该只有熊二可以执行/api/addcasbin,但是由于在匹配其中增加了r.sub==“superTest”,所以熊大不需要经过校验就能获取该资源

看看postman测试结果是否和我们配置的一致:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值