Go 微服务访问控制之 Casbin 实践指南

Posted on Sun 13 July 2025 in Journal

Abstract Go 微服务访问控制之 Casbin 实践指南
Authors Walter Fan
Category learning note
Status v1.0
Updated 2025-07-13
License CC-BY-NC-ND 4.0

目录

Go 微服务访问控制之 Casbin 实践指南

1. 概述

在构建 Web 服务时,安全认证及授权是首要解决的问题,主要涉及 Authentication(身份认证)和 Authorization(授权访问)两个核心概念:

  • Authentication: 验证用户身份,确定"谁可以访问"
  • Authorization: 控制访问权限,确定"以什么方式访问什么资源"
  • Audit: 记录访问日志,用于安全审计

1.1 常见访问控制模型

  1. ACL (Access Control List):基于资源和用户的直接授权
  2. RBAC (Role-Based Access Control):通过角色来分配权限
  3. ABAC (Attribute-Based Access Control):基于属性的动态访问控制

1.2 Casbin 简介

对于 Go 服务来说,可以使用 Casbin 这个强大的开源访问控制库,它提供了灵活的权限管理功能,可以实现上述常见的访问控制模型。

Casbin 的核心特点包括:

  • 策略驱动:权限规则存储在策略文件中(如 CSV 或 JSON),便于管理和修改
  • 模型抽象:使用 .conf 文件定义访问控制模型,提高灵活性
  • 高性能:优化的算法确保快速进行权限判断
  • 易于集成:可与主流框架(如 Gin、Beego)无缝结合
  • 多语言支持:支持 Go、Java、Python 等多种编程语言

在 Casbin 中,访问控制模型基于 PERM 元模型(Policy, Effect, Request, Matchers)抽象为一个 CONF 文件。因此,切换或升级项目的授权机制就像修改配置一样简单。您可以通过组合现有模型来定制您自己的访问控制模型。例如,您可以将 RBAC 角色和 ABAC 属性合并到一个模型中,并共享一组策略规则。

2. 访问控制策略

2.1 模型配置文件

2.1.1 request_definition(请求定义)

[request_definition]
r = sub, obj, act
  • 这部分定义了访问请求的结构
  • r 是一个请求,包含三个参数:
  • sub(主体):通常是用户或角色
  • obj(对象):被访问的资源(如 /admin/user
  • act(操作):对资源执行的操作(如 GET、POST、PUT、DELETE)

2.1.2 policy_definition(策略定义)

[policy_definition]
p = sub, obj, act
  • 这部分定义了策略(权限规则)的结构
  • p 是一条策略,也包含三个字段:
  • sub:有权执行操作的主体(用户或角色)
  • obj:可访问的资源
  • act:允许的操作

2.1.3 policy_effect(策略效果)

[policy_effect]
e = some(where (p.eft == allow))
  • 这部分定义了策略的效果
  • e = some(where (p.eft == allow)) 表示只要有一条策略允许该请求(即 p.eft == allow),整个请求就视为允许

2.1.4 matchers(匹配器)

[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
  • 这部分定义了如何将请求与策略进行匹配
  • m 是一个布尔表达式,表示只有当请求中的 subobjact 都与某条策略完全匹配时,才认为该策略适用于当前请求

这个配置文件实现了一个 经典的 RBAC(基于角色的访问控制)模型,其核心思想是:

  • 每个请求由 sub(角色或用户)、obj(资源)、act(操作)组成
  • 系统会查找是否有对应的策略 p 匹配该请求
  • 如果存在匹配且策略允许(allow),则授权通过

2.2 策略配置文件

策略文件(如 policy.csv)定义了具体的访问规则,例如:

p, admin, /api/v1/users/*, *
p, admin, /api/v1/prompts/*, *
p, user, /api/v1/prompts/*, GET
p, user, /api/v1/prompts/*, POST

表示: - admin 角色可以访问 /api/v1/users//api/v1/prompts/* 接口的所有请求 - user 角色可以访问 /api/v1/prompts/* 接口的 GET 和 POST 请求

3. 代码实现

以 一个基于 Gin 的 Go web 项目为例,我们用 Casbin 来构建细粒度的权限系统,比如 API 接口访问控制。 完整代码参见 https://github.com/walterfan/kata-go/tree/master/kata/prompt_service_v2

3.1 定义 User 用户模型

// pkg/models/user.go
package models

import (
    "time"
)

type User struct {
    ID        uint      `json:"id" gorm:"primaryKey"`
    Username  string    `json:"username" gorm:"unique;not null"`
    Password  string    `json:"password" gorm:"not null"`
    Email     string    `json:"email" gorm:"unique;not null"`
    Role      string    `json:"role" gorm:"default:'user'"` // e.g., 'admin', 'user'
    ExpiredAt time.Time `json:"expired_at"`                // expiration time for account
    CreatedAt time.Time `json:"-"`
    UpdatedAt time.Time `json:"-"`
}

3.2 构建登录处理器

// pkg/auth/login.go
package auth

import (
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
    "github.com/walterfan/prompt-service/pkg/database"
    "github.com/walterfan/prompt-service/pkg/models"
    "golang.org/x/crypto/bcrypt"
)

func LoginHandler(c *gin.Context) {
    var req struct {
        Username string `json:"username" binding:"required"`
        Password string `json:"password" binding:"required"`
    }

    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    var user models.User
    result := database.DB.Where("username = ?", req.Username).First(&user)
    if result.Error != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
        return
    }

    // Compare password hash
    if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
        return
    }

    // Check if user account has expired
    if user.ExpiredAt.Before(time.Now()) {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Account expired"})
        return
    }

    // Generate JWT token
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "sub":  user.Username,
        "role": user.Role,
        "exp":  time.Now().Add(time.Hour * 72).Unix(),
        "iat":  time.Now().Unix(),
    })

    tokenString, err := token.SignedString(JwtSecret)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "token": tokenString,
        "user": gin.H{
            "username": user.Username,
            "role":     user.Role,
        },
    })
}

3.3 中间件实现

3.3.1 JWT 中间件

// pkg/auth/jwt.go
package auth

import (
    "fmt"
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
)

var JwtSecret []byte

func InitJwt(secret string) {
    JwtSecret = []byte(secret)
}

func JwtMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Missing authorization header"})
            return
        }

        parts := strings.Split(authHeader, " ")
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization format"})
            return
        }

        tokenString := parts[1]
        token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
            // Validate signing method
            if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
                return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
            }
            return JwtSecret, nil
        })

        if err != nil {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
            return
        }

        if !token.Valid {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
            return
        }

        claims, ok := token.Claims.(jwt.MapClaims)
        if !ok {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
            return
        }

        // Set user info in context
        if sub, ok := claims["sub"].(string); ok {
            c.Set("user", sub)
        }
        if role, ok := claims["role"].(string); ok {
            c.Set("role", role)
        }

        c.Next()
    }
}

3.3.2 Casbin 授权中间件

// pkg/authz/middleware.go
package authz

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func CasbinMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        user, exists := c.Get("user")
        if !exists {
            c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "User not authenticated"})
            return
        }

        sub := user.(string)           // e.g., "alice"
        obj := c.Request.URL.Path      // e.g., "/api/v1/prompts/1"
        act := c.Request.Method        // e.g., "GET"

        // Check permission using Casbin
        allowed, err := Enforcer.Enforce(sub, obj, act)
        if err != nil {
            c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Authorization check failed"})
            return
        }

        if allowed {
            c.Next()
        } else {
            c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Access denied"})
        }
    }
}

3.3.3 Casbin 执行器

// pkg/authz/enforcer.go
package authz

import (
    "bufio"
    "log"
    "os"
    "strings"

    "github.com/casbin/casbin/v2"
    gormadapter "github.com/casbin/gorm-adapter/v3"
    "github.com/walterfan/prompt-service/pkg/database"
)

var Enforcer *casbin.Enforcer

/*
Gorm Adapter 是 Casbin 的 Gorm 适配器。使用此库,Casbin 可以从 Gorm 支持的数据库中加载策略,
或将策略保存到其中。适配器将使用名为"casbin_rule"的表。如果该表不存在,适配器将自动创建。

参考:https://github.com/casbin/gorm-adapter
*/
func InitAuthz(modelConfigFile string) error {
    adapter, err := gormadapter.NewAdapterByDB(database.DB)
    if err != nil {
        return err
    }

    enforcer, err := casbin.NewEnforcer(modelConfigFile, adapter)
    if err != nil {
        return err
    }

    err = enforcer.LoadPolicy()
    if err != nil {
        return err
    }

    Enforcer = enforcer

    // Check if policies exist, if not, add default policies
    policies, err := Enforcer.GetPolicy()
    if err != nil {
        return err
    }

    if len(policies) == 0 {
        log.Println("No policies found, adding default policies...")
        _, err := Enforcer.AddPolicy("admin", "/api/v1/prompts/*", "*")
        if err != nil {
            return err
        }
        _, err = Enforcer.AddPolicy("user", "/api/v1/prompts/*", "GET")
        if err != nil {
            return err
        }
        _, err = Enforcer.AddPolicy("user", "/api/v1/prompts/*", "POST")
        if err != nil {
            return err
        }
    }

    return nil
}

// LoadPoliciesFromCSV loads policies from a CSV file
func LoadPoliciesFromCSV(enforcer *casbin.Enforcer, filePath string) error {
    file, err := os.Open(filePath)
    if err != nil {
        return err
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := strings.TrimSpace(scanner.Text())
        if line == "" || strings.HasPrefix(line, "#") {
            continue
        }

        parts := strings.Split(line, ",")
        if len(parts) < 4 || strings.ToLower(strings.TrimSpace(parts[0])) != "p" {
            continue // only process 'p' type policies
        }

        subject := strings.TrimSpace(parts[1])
        object := strings.TrimSpace(parts[2])
        action := strings.TrimSpace(parts[3])

        _, err := enforcer.AddPolicy(subject, object, action)
        if err != nil {
            return err
        }
    }

    return scanner.Err()
}

3.4 路由配置

// main.go 或 router.go
func setupRoutes(r *gin.Engine) {
    // Public routes
    r.POST("/api/v1/login", auth.LoginHandler)

    // Protected routes
    api := r.Group("/api/v1")
    api.Use(auth.JwtMiddleware())
    api.Use(authz.CasbinMiddleware())
    {
        api.POST("/prompts", handlers.CreatePrompt)
        api.GET("/prompts/:id", handlers.GetPrompt)
        api.PUT("/prompts/:id", handlers.UpdatePrompt)
        api.DELETE("/prompts/:id", handlers.DeletePrompt)
        api.GET("/prompts", handlers.SearchPrompts)
    }
}

4. 最佳实践

4.1 安全建议

  1. JWT 密钥管理:使用强密钥并定期轮换
  2. Token 过期时间:设置合理的过期时间,避免过长
  3. HTTPS:在生产环境中使用 HTTPS
  4. 密码哈希:使用 bcrypt 等安全的哈希算法
  5. 输入验证:对所有输入进行验证和清理

4.2 性能优化

  1. 缓存策略:对频繁访问的权限进行缓存
  2. 批量操作:使用 Casbin 的批量 API 提高性能
  3. 数据库索引:为权限表添加适当的索引

4.3 监控和日志

  1. 访问日志:记录所有权限检查的结果
  2. 性能监控:监控权限检查的响应时间
  3. 异常告警:对权限异常进行告警

5. 总结

通过本文的介绍,我们了解了如何在 Go 微服务中集成 Casbin 实现灵活的访问控制:

  • 使用 Casbin 可以实现灵活的访问控制策略,满足不同场景的需求
  • 结合 JWT 可以实现安全的身份认证和授权
  • 通过配置文件可以轻松切换和扩展访问控制模型
  • 支持多种访问控制模型,如 RBAC、ABAC 等
  • 易于集成和维护,适合微服务架构

Casbin 的强大之处在于其灵活性和可扩展性,可以根据业务需求定制不同的访问控制策略,是构建安全微服务的优秀选择。


参考资料: - Casbin 官方文档 - Casbin Go 示例 - Gin 框架文档


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。