backend/pkg/permission/README.md

17 KiB
Raw Blame History

Permission Module

一個完整的 Go 權限管理模組,提供 JWT 令牌管理、RBAC基於角色的訪問控制以及權限樹管理功能。

📋 目錄

🎯 功能特性

1. JWT 令牌管理

  • Access Token 與 Refresh Token 機制
  • One-Time Token 支持(一次性令牌)
  • 設備追蹤與管理
  • 令牌黑名單機制
  • 多設備登錄限制
  • 令牌自動過期與刷新

2. RBAC 權限控制

  • 層級式權限結構(權限樹)
  • 角色與權限關聯管理
  • 使用者與角色關聯管理
  • 動態權限檢查
  • HTTP API 權限映射
  • 權限繼承(父權限自動包含)

3. 資料持久化

  • Redis 令牌存儲與快取
  • MongoDB 權限/角色數據存儲
  • 批量查詢優化(避免 N+1
  • 軟刪除支持

4. 安全特性

  • JWT 簽名驗證
  • 令牌黑名單
  • 設備指紋追蹤
  • 循環依賴檢測
  • 管理員權限特殊處理

🏗️ 架構設計

本模組遵循 Clean Architecture 設計原則:

pkg/permission/
├── domain/          # 領域層(核心業務邏輯)
│   ├── entity/      # 實體定義
│   ├── repository/  # Repository 介面
│   ├── usecase/     # UseCase 介面
│   ├── config/      # 配置定義
│   ├── permission/  # 權限類型定義
│   └── token/       # 令牌類型定義
├── usecase/         # UseCase 實現層
│   ├── token.go                    # 令牌業務邏輯
│   ├── permission_usecase.go       # 權限業務邏輯
│   ├── role_usecase.go             # 角色業務邏輯
│   ├── role_permission_usecase.go  # 角色權限業務邏輯
│   ├── user_role_usecase.go        # 使用者角色業務邏輯
│   └── permission_tree.go          # 權限樹結構
├── repository/      # Repository 實現層(數據訪問)
│   ├── token_model.go         # Redis 令牌存儲
│   ├── permission.go          # MongoDB 權限存儲
│   ├── role.go                # MongoDB 角色存儲
│   ├── role_permission.go     # MongoDB 角色權限存儲
│   └── user_role.go           # MongoDB 使用者角色存儲
└── mock/            # Mock 實現(測試用)

依賴關係圖

┌─────────────────┐
│   HTTP Handler  │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│    UseCase      │  ◄─── 業務邏輯層
│  - Token        │
│  - Permission   │
│  - Role         │
│  - UserRole     │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   Repository    │  ◄─── 數據訪問層
│  - Redis        │
│  - MongoDB      │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  Storage        │  ◄─── 存儲層
│  - Redis        │
│  - MongoDB      │
└─────────────────┘

📁 目錄結構

domain/ - 領域層

entity/ - 實體定義

  • token.go - 令牌實體Token, Ticket, Blacklist
  • permission.go - 權限實體
  • role.go - 角色實體
  • role_permission.go - 角色權限關聯實體
  • user_role.go - 使用者角色實體

repository/ - Repository 介面

定義數據訪問接口,與具體實現解耦

usecase/ - UseCase 介面

定義業務邏輯接口,規範業務操作

config/ - 配置定義

  • config.go - 模組配置結構
  • errors.go - 配置錯誤定義

permission/ - 權限類型

  • types.go - 權限狀態、類型、集合定義

token/ - 令牌類型

  • token_type.go - 令牌類型常量
  • grant_type.go - 授權類型定義

usecase/ - 業務邏輯實現

令牌管理

  • token.go - JWT 令牌生成、驗證、刷新
  • token_jwt.go - JWT 編解碼
  • token_claims.go - JWT Claims 處理

RBAC 管理

  • permission_usecase.go - 權限管理
  • role_usecase.go - 角色管理
  • role_permission_usecase.go - 角色權限管理
  • user_role_usecase.go - 使用者角色管理
  • permission_tree.go - 權限樹結構與操作

repository/ - 數據訪問實現

  • token_model.go - Redis 令牌存儲實現
  • permission.go - MongoDB 權限存儲實現
  • role.go - MongoDB 角色存儲實現
  • role_permission.go - MongoDB 角色權限存儲實現
  • user_role.go - MongoDB 使用者角色存儲實現

mock/ - Mock 實現

自動生成的 Mock 實現,用於單元測試

🚀 快速開始

1. 安裝依賴

go get backend/pkg/permission

2. 配置

import (
    "backend/pkg/permission/domain/config"
    "backend/pkg/permission/usecase"
    "backend/pkg/permission/repository"
)

// 配置
cfg := &config.Config{
    Token: config.TokenConfig{
        Secret: "your-secret-key",
        Expired: config.ExpiredConfig{
            Seconds: 900, // 15 分鐘
        },
        RefreshExpires: config.ExpiredConfig{
            Seconds: 604800, // 7 天
        },
        Issuer:             "playone-backend",
        MaxTokensPerUser:   10,
        MaxTokensPerDevice: 5,
    },
    Role: config.RoleConfig{
        AdminRoleUID: "ADMIN",
        UIDPrefix:    "ROLE",
        UIDLength:    10,
    },
}

3. 初始化 Repository

// Redis 令牌存儲
redisClient := redis.NewClient(&redis.Options{
    Addr: "localhost:6379",
})

tokenRepo := repository.NewTokenRepository(repository.TokenRepositoryParam{
    Redis: redisClient,
})

// MongoDB 存儲
mongoConf := &mongo.Conf{
    Host:     "localhost:27017",
    Database: "permission",
    User:     "admin",
    Password: "password",
}

permRepo := repository.NewPermissionRepository(repository.PermissionRepositoryParam{
    Conf:      mongoConf,
    CacheConf: cache.CacheConf{},
})

roleRepo := repository.NewRoleRepository(repository.RoleRepositoryParam{
    Conf:      mongoConf,
    CacheConf: cache.CacheConf{},
})

rolePermRepo := repository.NewRolePermissionRepository(repository.RolePermissionRepositoryParam{
    Conf:      mongoConf,
    CacheConf: cache.CacheConf{},
})

userRoleRepo := repository.NewUserRoleRepository(repository.UserRoleRepositoryParam{
    Conf:      mongoConf,
    CacheConf: cache.CacheConf{},
})

4. 初始化 UseCase

// 權限 UseCase
permUC := usecase.NewPermissionUseCase(usecase.PermissionUseCaseParam{
    PermRepo:     permRepo,
    RolePermRepo: rolePermRepo,
    RoleRepo:     roleRepo,
    UserRoleRepo: userRoleRepo,
})

// 角色權限 UseCase
rolePermUC := usecase.NewRolePermissionUseCase(usecase.RolePermissionUseCaseParam{
    PermRepo:     permRepo,
    RolePermRepo: rolePermRepo,
    RoleRepo:     roleRepo,
    UserRoleRepo: userRoleRepo,
    PermUseCase:  permUC,
    AdminRoleUID: cfg.Role.AdminRoleUID,
})

// 角色 UseCase
roleUC := usecase.NewRoleUseCase(usecase.RoleUseCaseParam{
    RoleRepo:        roleRepo,
    UserRoleRepo:    userRoleRepo,
    RolePermUseCase: rolePermUC,
    Config: usecase.RoleUseCaseConfig{
        AdminRoleUID: cfg.Role.AdminRoleUID,
        UIDPrefix:    cfg.Role.UIDPrefix,
        UIDLength:    cfg.Role.UIDLength,
    },
})

// 使用者角色 UseCase
userRoleUC := usecase.NewUserRoleUseCase(usecase.UserRoleUseCaseParam{
    UserRoleRepo: userRoleRepo,
    RoleRepo:     roleRepo,
})

// 令牌 UseCase
tokenUC := usecase.NewTokenUseCase(usecase.TokenUseCaseParam{
    TokenRepo: tokenRepo,
    Config:    cfg,
})

📚 API 文檔

令牌管理 API

生成令牌

req := entity.AuthorizationReq{
    GrantType: token.PasswordCredentials.ToString(),
    Data: map[string]string{
        "uid":  "user123",
        "role": "admin",
    },
    DeviceID: "device123",
}

tokenResp, err := tokenUC.NewToken(ctx, req)
// tokenResp.AccessToken
// tokenResp.RefreshToken
// tokenResp.ExpiresIn

刷新令牌

req := entity.RefreshTokenReq{
    RefreshToken: "old-refresh-token",
    DeviceID:     "device123",
}

tokenResp, err := tokenUC.RefreshToken(ctx, req)

驗證令牌

isValid := tokenUC.IsAccessTokenValid(ctx, accessToken)

取消令牌

err := tokenUC.CancelToken(ctx, tokenID)

黑名單令牌

err := tokenUC.BlacklistToken(ctx, entity.BlacklistTokenReq{
    TokenID: tokenID,
    Reason:  "User logout",
})

權限管理 API

獲取所有權限

permissions, err := permUC.GetAll(ctx)

獲取權限樹

tree, err := permUC.GetTree(ctx)

根據 HTTP 路徑獲取權限

perm, err := permUC.GetByHTTP(ctx, "/api/users", "GET")

展開權限(包含父權限)

perms := permission.Permissions{
    "user.list": permission.Open,
}
expanded, err := permUC.ExpandPermissions(ctx, perms)
// expanded 將包含 "user" 和 "user.list"

角色管理 API

創建角色

req := usecase.CreateRoleRequest{
    ClientID: 1,
    Name:     "管理員",
    Permissions: permission.Permissions{
        "user.list":   permission.Open,
        "user.create": permission.Open,
    },
}

role, err := roleUC.Create(ctx, req)

更新角色

name := "高級管理員"
req := usecase.UpdateRoleRequest{
    Name: &name,
    Permissions: permission.Permissions{
        "user.list":   permission.Open,
        "user.create": permission.Open,
        "user.delete": permission.Open,
    },
}

role, err := roleUC.Update(ctx, "ROLE0000000001", req)

刪除角色

err := roleUC.Delete(ctx, "ROLE0000000001")

分頁查詢角色

filter := usecase.RoleFilterRequest{
    ClientID: 1,
    Name:     "管理",
}

page, err := roleUC.Page(ctx, filter, 1, 10)
// page.List - 角色列表(含使用者數量)
// page.Total - 總數

角色權限管理 API

獲取角色權限

perms, err := rolePermUC.GetByRoleUID(ctx, "ROLE0000000001")

獲取使用者權限

userPerms, err := rolePermUC.GetByUserUID(ctx, "user123")
// userPerms.RoleUID
// userPerms.RoleName
// userPerms.Permissions

更新角色權限

perms := permission.Permissions{
    "user.list":   permission.Open,
    "user.create": permission.Open,
}

err := rolePermUC.UpdateRolePermissions(ctx, "ROLE0000000001", perms)

檢查權限

result, err := rolePermUC.CheckPermission(ctx, "ROLE0000000001", "/api/users", "GET")
// result.Allowed - 是否允許
// result.PermissionName - 權限名稱
// result.PlainCode - 是否有 plain_code 權限

使用者角色管理 API

指派角色給使用者

req := usecase.AssignRoleRequest{
    UserUID: "user123",
    RoleUID: "ROLE0000000001",
    Brand:   "brand1",
}

userRole, err := userRoleUC.Assign(ctx, req)

更新使用者角色

userRole, err := userRoleUC.Update(ctx, "user123", "ROLE0000000002")

移除使用者角色

err := userRoleUC.Remove(ctx, "user123")

獲取角色的所有使用者

userRoles, err := userRoleUC.GetByRole(ctx, "ROLE0000000001")

⚙️ 配置說明

Token 配置

token:
  secret: "your-jwt-secret-key"           # JWT 簽名密鑰
  expired:
    seconds: 900                           # Access Token 過期時間(秒)
  refresh_expires:
    seconds: 604800                        # Refresh Token 過期時間(秒)
  issuer: "playone-backend"               # 發行者
  max_tokens_per_user: 10                 # 每個使用者最大令牌數
  max_tokens_per_device: 5                # 每個設備最大令牌數
  enable_device_tracking: true            # 是否啟用設備追蹤

Role 配置

role:
  admin_role_uid: "ADMIN"                 # 管理員角色 UID
  uid_prefix: "ROLE"                      # 角色 UID 前綴
  uid_length: 10                          # UID 數字長度

RBAC 配置

rbac:
  enable_cache: true                      # 是否啟用快取
  cache_ttl: 3600                         # 快取過期時間(秒)

🧪 測試

運行所有測試

go test ./pkg/permission/... -v

運行特定測試

# 令牌測試
go test ./pkg/permission/usecase -run TestToken -v

# 權限樹測試
go test ./pkg/permission/usecase -run TestPermissionTree -v

# Repository 測試
go test ./pkg/permission/repository -v

測試覆蓋率

go test ./pkg/permission/... -cover

生成覆蓋率報告

go test ./pkg/permission/... -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html

💡 最佳實踐

1. 令牌管理

推薦做法

// 使用 Refresh Token 自動刷新
if tokenUC.IsAccessTokenValid(ctx, accessToken) {
    // 使用令牌
} else {
    // 嘗試刷新
    newToken, err := tokenUC.RefreshToken(ctx, refreshReq)
    if err != nil {
        // 要求重新登錄
    }
}

避免做法

// 不要在每次請求都生成新令牌
// 不要將令牌存儲在不安全的地方
// 不要在客戶端解析 Refresh Token

2. 權限檢查

推薦做法

// 使用權限檢查中間件
func AuthMiddleware(rolePermUC usecase.RolePermissionUseCase) gin.HandlerFunc {
    return func(c *gin.Context) {
        roleUID := c.GetString("role_uid")
        result, err := rolePermUC.CheckPermission(
            c.Request.Context(),
            roleUID,
            c.Request.URL.Path,
            c.Request.Method,
        )
        
        if err != nil || !result.Allowed {
            c.AbortWithStatus(http.StatusForbidden)
            return
        }
        
        c.Next()
    }
}

避免做法

// 不要在每個 Handler 中重複權限檢查代碼
// 不要硬編碼權限名稱
// 不要跳過權限檢查

3. 權限設計

推薦做法

// 使用層級式權限結構
// 例如:
// - user
//   - user.list
//   - user.create
//   - user.update
//   - user.delete

// 父權限會自動包含,只需設置子權限即可
perms := permission.Permissions{
    "user.list": permission.Open,
}
// 展開後會自動包含 "user"

避免做法

// 不要創建過深的權限層級(建議 ≤ 3 層)
// 不要使用過於細緻的權限粒度
// 不要創建循環依賴的權限

4. 角色管理

推薦做法

// 使用預定義角色
const (
    RoleAdmin     = "ADMIN"
    RoleManager   = "MANAGER"
    RoleEmployee  = "EMPLOYEE"
)

// 定期檢查無使用者的角色
roles, _ := roleUC.Page(ctx, filter, 1, 100)
for _, role := range roles.List {
    if role.UserCount == 0 {
        // 考慮刪除或停用
    }
}

避免做法

// 不要創建過多的角色
// 不要刪除有使用者的角色
// 不要頻繁修改角色權限

🔐 安全建議

  1. JWT 密鑰管理

    • 使用強密鑰(至少 32 字元)
    • 定期輪換密鑰
    • 不要將密鑰硬編碼在代碼中
  2. 令牌過期設置

    • Access Token: 15 分鐘 - 1 小時
    • Refresh Token: 7 - 30 天
    • One-Time Token: 5 - 10 分鐘
  3. 設備追蹤

    • 啟用設備指紋追蹤
    • 限制每個設備的令牌數量
    • 檢測異常登錄行為
  4. 權限檢查

    • 在所有 API 端點進行權限檢查
    • 使用白名單而非黑名單
    • 記錄權限拒絕事件

📝 資料庫設計

MongoDB Collections

permissions

{
  "_id": ObjectId,
  "parent_id": ObjectId,
  "name": "user.list",
  "http_method": "GET",
  "http_path": "/api/users",
  "status": 1,
  "type": 1,
  "create_time": 1234567890,
  "update_time": 1234567890
}

roles

{
  "_id": ObjectId,
  "client_id": 1,
  "uid": "ROLE0000000001",
  "name": "管理員",
  "status": 1,
  "create_time": 1234567890,
  "update_time": 1234567890
}

role_permissions

{
  "_id": ObjectId,
  "role_id": ObjectId,
  "permission_id": ObjectId,
  "create_time": 1234567890,
  "update_time": 1234567890
}

user_roles

{
  "_id": ObjectId,
  "brand": "brand1",
  "uid": "user123",
  "role_id": "ROLE0000000001",
  "status": 1,
  "create_time": 1234567890,
  "update_time": 1234567890
}

Redis Keys

permission:access_token:{token_id}         # Access Token
permission:refresh_token:{token_id}        # Refresh Token
permission:device_token:{device_id}        # Device Token List
permission:uid_token:{uid}                 # User Token List
permission:ticket:{ticket}                 # One-Time Token
permission:blacklist:{token_id}            # Token Blacklist