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 常见访问控制模型
- ACL (Access Control List):基于资源和用户的直接授权
- RBAC (Role-Based Access Control):通过角色来分配权限
- 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
是一个布尔表达式,表示只有当请求中的sub
、obj
、act
都与某条策略完全匹配时,才认为该策略适用于当前请求
这个配置文件实现了一个 经典的 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 安全建议
- JWT 密钥管理:使用强密钥并定期轮换
- Token 过期时间:设置合理的过期时间,避免过长
- HTTPS:在生产环境中使用 HTTPS
- 密码哈希:使用 bcrypt 等安全的哈希算法
- 输入验证:对所有输入进行验证和清理
4.2 性能优化
- 缓存策略:对频繁访问的权限进行缓存
- 批量操作:使用 Casbin 的批量 API 提高性能
- 数据库索引:为权限表添加适当的索引
4.3 监控和日志
- 访问日志:记录所有权限检查的结果
- 性能监控:监控权限检查的响应时间
- 异常告警:对权限异常进行告警
5. 总结
通过本文的介绍,我们了解了如何在 Go 微服务中集成 Casbin 实现灵活的访问控制:
- 使用 Casbin 可以实现灵活的访问控制策略,满足不同场景的需求
- 结合 JWT 可以实现安全的身份认证和授权
- 通过配置文件可以轻松切换和扩展访问控制模型
- 支持多种访问控制模型,如 RBAC、ABAC 等
- 易于集成和维护,适合微服务架构
Casbin 的强大之处在于其灵活性和可扩展性,可以根据业务需求定制不同的访问控制策略,是构建安全微服务的优秀选择。
参考资料: - Casbin 官方文档 - Casbin Go 示例 - Gin 框架文档
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。