使用 Casbin 在 Golang 项目中授权
JWT 认证 | GIN | GORM
你好开发者👋👋。在本文中,我们将在 Golang(GIN+GORM) 项目中实现 Casbin 和 JWT 身份验证。
在阅读本文之前,我强烈建议您查看另一篇我写的关于 casbin 基础知识及其不同模型配置的文章。
执行
我想事先展示文件夹结构,以便于理解。如果您在某个地方迷路了,可以查看我的 GitHub 存储库以获取完整代码。
文件夹结构
RBAC 模型 (config/rbac_model.conf)
如果您对 casbin 中的模型一无所知,请查看上一篇文章。你会在那里找到详细的解释。我将在本文中使用 RBAC 模型。
[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
数据库
我们将使用GORM来访问数据库。以下代码只是创建一个新的数据库连接并返回该连接,该连接将在以下部分的代码的其他部分中使用。
//DBConnection -> return db instance
func DBConnection() (*gorm.DB, error) {
USER := "root"
PASS := "root"
HOST := "localhost"
PORT := "3306"
DBNAME := "casbin-golang"
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Info, // Log level
Colorful: true, // Disable color
},
)
url := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", USER, PASS, HOST, PORT, DBNAME)
return gorm.Open(mysql.Open(url), &gorm.Config{Logger: newLogger})
}
路由
这是连接 casbin、我们的自定义中间件和处理程序方法(我们将在以下部分中创建)的文章的主要部分
//SetupRoutes : all the routes are defined here
func SetupRoutes(db *gorm.DB) {
httpRouter := gin.Default()
// Initialize casbin adapter
adapter, err := gormadapter.NewAdapterByDB(db)
if err != nil {
panic(fmt.Sprintf("failed to initialize casbin adapter: %v", err))
}
// Load model configuration file and policy store adapter
enforcer, err := casbin.NewEnforcer("config/rbac_model.conf", adapter)
if err != nil {
panic(fmt.Sprintf("failed to create casbin enforcer: %v", err))
}
//add policy
if hasPolicy := enforcer.HasPolicy("doctor", "report", "read"); !hasPolicy {
enforcer.AddPolicy("doctor", "report", "read")
}
if hasPolicy := enforcer.HasPolicy("doctor", "report", "write"); !hasPolicy {
enforcer.AddPolicy("doctor", "report", "write")
}
if hasPolicy := enforcer.HasPolicy("patient", "report", "read"); !hasPolicy {
enforcer.AddPolicy("patient", "report", "read")
}
userRepository := repository.NewUserRepository(db)
if err := userRepository.Migrate(); err != nil {
log.Fatal("User migrate err", err)
}
userController := controller.NewUserController(userRepository)
apiRoutes := httpRouter.Group("/api")
{
apiRoutes.POST("/register", userController.AddUser(enforcer))
apiRoutes.POST("/signin", userController.SignInUser)
}
userProtectedRoutes := apiRoutes.Group("/users", middleware.AuthorizeJWT())
{
userProtectedRoutes.GET("/", middleware.Authorize("report", "read", enforcer), userController.GetAllUser)
userProtectedRoutes.GET("/:user", middleware.Authorize("report", "read", enforcer), userController.GetUser)
userProtectedRoutes.PUT("/:user", middleware.Authorize("report", "write", enforcer), userController.UpdateUser)
userProtectedRoutes.DELETE("/:user", middleware.Authorize("report", "write", enforcer), userController.DeleteUser)
}
httpRouter.Run()
}
让我简单解释一下上面的代码
- [第 3 行]:这里我们设置默认的 Gin Router
-
[第 6 行]:这里我们为Casbin设置了Gorm适配器。使用此适配器,Casbin 可以从 Gorm 支持的数据库加载策略或将策略保存到其中。这还将创建一个名为
casbin_rule - [第 12 行]:在这里,我们通过提供我们的模态(上面构造的 RBAC)和 gorm 适配器(来自第 6 行)作为参数来构造一个 casbin 执行器。
- [第 17 - 26 行]:这里我们将应用程序所需的策略加载到数据库中。这是一次性操作。所以最好用一些独立于主程序流程的命令来添加策略。但为简单起见,我在主程序流本身中添加了策略 (
enforcer.AddPolicy),但前提是确保数据库中不存在该策略 (enforcer.HasPolicy)。
- [第 44–49 行]:这里我们使用 casbin 中间件(我们将在稍后构建)保护各个路由。
实用程序
我想预先展示一些实用函数,这些函数将在下面代码的不同部分中使用。所有这些都非常简单。
func HashPassword(pass *string) {
bytePass := []byte(*pass)
hPass, _ := bcrypt.GenerateFromPassword(bytePass, bcrypt.DefaultCost)
*pass = string(hPass)
}
func ComparePassword(dbPass, pass string) bool {
return bcrypt.CompareHashAndPassword([]byte(dbPass), []byte(pass)) == nil
}
//GenerateToken -> generates token
func GenerateToken(userid uint) string {
claims := jwt.MapClaims{
"exp": time.Now().Add(time.Hour * 3).Unix(),
"iat": time.Now().Unix(),
"userID": userid,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
t, _ := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
return t
}
//ValidateToken --> validate the given token
func ValidateToken(token string) (*jwt.Token, error) {
//2nd arg function return secret key after checking if the signing method is HMAC and returned key is used by 'Parse' to decode the token)
return jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
//nil secret key
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("JWT_SECRET")), nil
})
}
中间件
JWT认证
我不会在本文中详细介绍身份验证。您可以使用任何身份验证模块,如 JWT、Firebase 身份验证、AWS Cognito。我将在这里展示 JWT 代码。
//AuthorizeJWT -> to authorize JWT Token
func AuthorizeJWT() gin.HandlerFunc {
return func(ctx *gin.Context) {
const BearerSchema string = "Bearer "
authHeader := ctx.GetHeader("Authorization")
if authHeader == "" {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "No Authorization header found"})
}
tokenString := authHeader[len(BearerSchema):]
if token, err := utils.ValidateToken(tokenString); err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Not Valid Token"})
} else {
if claims, ok := token.Claims.(jwt.MapClaims); !ok {
ctx.AbortWithStatus(http.StatusUnauthorized)
} else {
if token.Valid {
ctx.Set("userID", claims["userID"])
} else {
ctx.AbortWithStatus(http.StatusUnauthorized)
}
}
}
}
}
上面的代码是一个中间件,用于确保用户是否拥有有效的令牌。
我在这里唯一想强调的是我们userID在从令牌检索的上下文 [第 22 行] 中进行设置。这userID将用于稍后的授权。
CASBIN中间件
该中间件的目的是确保用户是否具有正确的权限或是否有权执行某项任务。
// Authorize determines if current user has been authorized to take an action on an object.
func Authorize(obj string, act string, enforcer *casbin.Enforcer) gin.HandlerFunc {
return func(c *gin.Context) {
// Get current user/subject
sub, existed := c.Get("userID")
if !existed {
c.AbortWithStatusJSON(401, gin.H{"msg": "User hasn't logged in yet"})
return
}
// Load policy from Database
err := enforcer.LoadPolicy()
if err != nil {
c.AbortWithStatusJSON(500, gin.H{"msg": "Failed to load policy from DB"})
return
}
// Casbin enforces policy
ok, err := enforcer.Enforce(fmt.Sprint(sub), obj, act)
if err != nil {
c.AbortWithStatusJSON(500, gin.H{"msg": "Error occurred when authorizing user"})
return
}
if !ok {
c.AbortWithStatusJSON(403, gin.H{"msg": "You are not authorized"})
return
}
c.Next()
}
}
Authorize方法接受 3 个参数,即obj,act和enforcer。obj是他试图访问的资源。act是他试图对该资源执行的操作。enforcer就是我们在上面路由部分初始化的casbin Enforcer。
- [第 5 行]:这里我们从上下文中检索
subwhichuserID以了解谁正在尝试对资源执行特定操作 - [第 12 行]:这里我们加载数据库中所有可用的策略(我们在上面的路由部分添加的)
- [第 19 行]:这里我们使用 casbin
enforce方法来确定用户是应该被授予访问权限还是应该被拒绝。
用户模型
这是我们将用于注册/登录过程的模型[在后面的部分]。这里role只是为了从前端检索用户的角色。它不会存储在数据库中。因此gorm:”-”使用
//User -> model for users table
type User struct {
gorm.Model
Name string `json:"name" `
Email string `json:"email" gorm:"unique"`
Role string `json:"role" gorm:"-"`
Password string `json:"password" `
}
//TableName --> Table for Product Model
func (User) TableName() string {
return "users"
}
注册
此方法的目的是注册用户。
//AddUser - Register a user
func (h userController) AddUser(enforcer *casbin.Enforcer) gin.HandlerFunc {
return func(ctx *gin.Context) {
var user model.User
if err := ctx.ShouldBindJSON(&user); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
utils.HashPassword(&user.Password)
user, err := h.userRepo.AddUser(user)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
enforcer.AddGroupingPolicy(fmt.Sprint(user.ID), user.Role)
user.Password = ""
ctx.JSON(http.StatusOK, user)
}
}
- [第 4–8 行]:我们正在从前端检索用户信息
- [第 11–16 行]:我们将检索到的用户信息添加到数据库中
- [第 17 行]:我们正在使用方法将用户角色添加到
casbin_rule表中enforcer.AddGroupingPolicy。
当我们打POST /register
前端请求
用户表
Johnwith id8被添加到users表中。
Casbin表
user_id 为 8(John) (v0) 和 role 为 doctor (v1) 的角色添加到 casbin_rule 表中。
登录
这个方法的目的是让用户能够登录我们的系统。
func (h userController) SignInUser(ctx *gin.Context) {
var user model.User
if err := ctx.ShouldBindJSON(&user); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
dbUser, err := h.userRepo.GetByEmail(user.Email)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"msg": "No Such User Found"})
return
}
if isTrue := utils.ComparePassword(dbUser.Password, user.Password); isTrue {
token := utils.GenerateToken(dbUser.ID)
ctx.JSON(http.StatusOK, gin.H{"msg": "Successfully SignIN", "token": token})
return
}
ctx.JSON(http.StatusInternalServerError, gin.H{"msg": "Password not matched"})
return
}
上面的代码检索登录信息[第 2-5 行] 并检查该电子邮件是否在数据库中可用[第 7-12 行] 并验证密码是否正确[第 13-18 行]。
主程序
func main() {
db, _ := model.DBConnection()
route.SetupRoutes(db)
}
运行项目
如果具有patient角色的用户试图访问PUT users/:user即试图更新用户,他将不会被允许这样做。
结论
如果你对任何部分感到困惑,你可以查看我的 Github 项目。
我们对 casbin 的理解和在 golang 项目上实现 casbin 的短暂旅程到此结束。希望这对您的项目有所帮助。任何类型的建议都将不胜感激。快乐编码。
如果你喜欢我的文章,点赞,关注,转发!