你好開(kāi)發(fā)者????。在本文中,我們將在 Golang(GIN+GORM) 項(xiàng)目中實(shí)現(xiàn) Casbin 和 JWT 身份驗(yàn)證。
在閱讀本文之前,我強(qiáng)烈建議您查看另一篇我寫(xiě)的關(guān)于 casbin 基礎(chǔ)知識(shí)及其不同模型配置的文章。
執(zhí)行
我想事先展示文件夾結(jié)構(gòu),以便于理解。如果您在某個(gè)地方迷路了,可以查看我的 GitHub 存儲(chǔ)庫(kù)以獲取完整代碼。
文件夾結(jié)構(gòu)
RBAC 模型 (config/rbac_model.conf)
如果您對(duì) casbin 中的模型一無(wú)所知,請(qǐng)查看上一篇文章。你會(huì)在那里找到詳細(xì)的解釋。我將在本文中使用 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
數(shù)據(jù)庫(kù)
我們將使用GORM來(lái)訪問(wèn)數(shù)據(jù)庫(kù)。以下代碼只是創(chuàng)建一個(gè)新的數(shù)據(jù)庫(kù)連接并返回該連接,該連接將在以下部分的代碼的其他部分中使用。
//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、我們的自定義中間件和處理程序方法(我們將在以下部分中創(chuàng)建)的文章的主要部分
//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()
}
讓我簡(jiǎn)單解釋一下上面的代碼
[第 3 行]:這里我們?cè)O(shè)置默認(rèn)的 Gin Router
[第 6 行]:這里我們?yōu)镃asbin設(shè)置了Gorm適配器。使用此適配器,Casbin 可以從 Gorm 支持的數(shù)據(jù)庫(kù)加載策略或?qū)⒉呗员4娴狡渲小_@還將創(chuàng)建一個(gè)名為
casbin_rule
[第 12 行]:在這里,我們通過(guò)提供我們的模態(tài)(上面構(gòu)造的 RBAC)和 gorm 適配器(來(lái)自第 6 行)作為參數(shù)來(lái)構(gòu)造一個(gè) casbin 執(zhí)行器。
[第 17 - 26 行]:這里我們將應(yīng)用程序所需的策略加載到數(shù)據(jù)庫(kù)中。這是一次性操作。所以最好用一些獨(dú)立于主程序流程的命令來(lái)添加策略。但為簡(jiǎn)單起見(jiàn),我在主程序流本身中添加了策略 (
enforcer.AddPolicy
),但前提是確保數(shù)據(jù)庫(kù)中不存在該策略 (enforcer.HasPolicy
)。
[第 44–49 行]:這里我們使用 casbin 中間件(我們將在稍后構(gòu)建)保護(hù)各個(gè)路由。
實(shí)用程序
我想預(yù)先展示一些實(shí)用函數(shù),這些函數(shù)將在下面代碼的不同部分中使用。所有這些都非常簡(jiǎn)單。
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認(rèn)證
我不會(huì)在本文中詳細(xì)介紹身份驗(yàn)證。您可以使用任何身份驗(yàn)證模塊,如 JWT、Firebase 身份驗(yàn)證、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)
}
}
}
}
}
上面的代碼是一個(gè)中間件,用于確保用戶是否擁有有效的令牌。
我在這里唯一想強(qiáng)調(diào)的是我們userID
在從令牌檢索的上下文 [第 22 行] 中進(jìn)行設(shè)置。這userID
將用于稍后的授權(quán)。
CASBIN中間件
該中間件的目的是確保用戶是否具有正確的權(quán)限或是否有權(quán)執(zhí)行某項(xiàng)任務(wù)。
// 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 個(gè)參數(shù),即obj
,act
和enforcer
。obj
是他試圖訪問(wèn)的資源。act
是他試圖對(duì)該資源執(zhí)行的操作。enforcer
就是我們?cè)谏厦媛酚刹糠殖跏蓟腸asbin Enforcer。
[第 5 行]:這里我們從上下文中檢索
sub
whichuserID
以了解誰(shuí)正在嘗試對(duì)資源執(zhí)行特定操作[第 12 行]:這里我們加載數(shù)據(jù)庫(kù)中所有可用的策略(我們?cè)谏厦娴穆酚刹糠痔砑拥模?/p>
[第 19 行]:這里我們使用 casbin
enforce
方法來(lái)確定用戶是應(yīng)該被授予訪問(wèn)權(quán)限還是應(yīng)該被拒絕。
用戶模型
這是我們將用于注冊(cè)/登錄過(guò)程的模型[在后面的部分]。這里role
只是為了從前端檢索用戶的角色。它不會(huì)存儲(chǔ)在數(shù)據(jù)庫(kù)中。因此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"
}
注冊(cè)
此方法的目的是注冊(cè)用戶。
//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 行]:我們將檢索到的用戶信息添加到數(shù)據(jù)庫(kù)中
[第 17 行]:我們正在使用方法將用戶角色添加到
casbin_rule
表中enforcer.AddGroupingPolicy
。
當(dāng)我們打POST /register
前端請(qǐng)求
用戶表
John
with id8
被添加到users
表中。
Casbin表
user_id 為 8(John)
(v0) 和 role 為 doctor
(v1) 的角色添加到 casbin_rule 表中。
登錄
這個(gè)方法的目的是讓用戶能夠登錄我們的系統(tǒng)。
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 行] 并檢查該電子郵件是否在數(shù)據(jù)庫(kù)中可用[第 7-12 行] 并驗(yàn)證密碼是否正確[第 13-18 行]。
主程序
func main() {
db, _ := model.DBConnection()
route.SetupRoutes(db)
}
運(yùn)行項(xiàng)目
如果具有patient
角色的用戶試圖訪問(wèn)PUT users/:user
即試圖更新用戶,他將不會(huì)被允許這樣做。
結(jié)論
如果你對(duì)任何部分感到困惑,你可以查看我的 Github 項(xiàng)目。
我們對(duì) casbin 的理解和在 golang 項(xiàng)目上實(shí)現(xiàn) casbin 的短暫旅程到此結(jié)束。希望這對(duì)您的項(xiàng)目有所幫助。任何類型的建議都將不勝感激??鞓?lè)編碼。
如果你喜歡我的文章,點(diǎn)贊,關(guān)注,轉(zhuǎn)發(fā)!