backend/pkg/permission/README.md

729 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Permission Module
一個完整的 Go 權限管理模組,提供 JWT 令牌管理、RBAC基於角色的訪問控制以及權限樹管理功能。
## 📋 目錄
- [功能特性](#功能特性)
- [架構設計](#架構設計)
- [目錄結構](#目錄結構)
- [快速開始](#快速開始)
- [API 文檔](#api-文檔)
- [配置說明](#配置說明)
- [測試](#測試)
- [最佳實踐](#最佳實踐)
## 🎯 功能特性
### 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. 安裝依賴
```bash
go get backend/pkg/permission
```
### 2. 配置
```go
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
```go
// 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
```go
// 權限 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
#### 生成令牌
```go
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
```
#### 刷新令牌
```go
req := entity.RefreshTokenReq{
RefreshToken: "old-refresh-token",
DeviceID: "device123",
}
tokenResp, err := tokenUC.RefreshToken(ctx, req)
```
#### 驗證令牌
```go
isValid := tokenUC.IsAccessTokenValid(ctx, accessToken)
```
#### 取消令牌
```go
err := tokenUC.CancelToken(ctx, tokenID)
```
#### 黑名單令牌
```go
err := tokenUC.BlacklistToken(ctx, entity.BlacklistTokenReq{
TokenID: tokenID,
Reason: "User logout",
})
```
### 權限管理 API
#### 獲取所有權限
```go
permissions, err := permUC.GetAll(ctx)
```
#### 獲取權限樹
```go
tree, err := permUC.GetTree(ctx)
```
#### 根據 HTTP 路徑獲取權限
```go
perm, err := permUC.GetByHTTP(ctx, "/api/users", "GET")
```
#### 展開權限(包含父權限)
```go
perms := permission.Permissions{
"user.list": permission.Open,
}
expanded, err := permUC.ExpandPermissions(ctx, perms)
// expanded 將包含 "user" 和 "user.list"
```
### 角色管理 API
#### 創建角色
```go
req := usecase.CreateRoleRequest{
ClientID: 1,
Name: "管理員",
Permissions: permission.Permissions{
"user.list": permission.Open,
"user.create": permission.Open,
},
}
role, err := roleUC.Create(ctx, req)
```
#### 更新角色
```go
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)
```
#### 刪除角色
```go
err := roleUC.Delete(ctx, "ROLE0000000001")
```
#### 分頁查詢角色
```go
filter := usecase.RoleFilterRequest{
ClientID: 1,
Name: "管理",
}
page, err := roleUC.Page(ctx, filter, 1, 10)
// page.List - 角色列表(含使用者數量)
// page.Total - 總數
```
### 角色權限管理 API
#### 獲取角色權限
```go
perms, err := rolePermUC.GetByRoleUID(ctx, "ROLE0000000001")
```
#### 獲取使用者權限
```go
userPerms, err := rolePermUC.GetByUserUID(ctx, "user123")
// userPerms.RoleUID
// userPerms.RoleName
// userPerms.Permissions
```
#### 更新角色權限
```go
perms := permission.Permissions{
"user.list": permission.Open,
"user.create": permission.Open,
}
err := rolePermUC.UpdateRolePermissions(ctx, "ROLE0000000001", perms)
```
#### 檢查權限
```go
result, err := rolePermUC.CheckPermission(ctx, "ROLE0000000001", "/api/users", "GET")
// result.Allowed - 是否允許
// result.PermissionName - 權限名稱
// result.PlainCode - 是否有 plain_code 權限
```
### 使用者角色管理 API
#### 指派角色給使用者
```go
req := usecase.AssignRoleRequest{
UserUID: "user123",
RoleUID: "ROLE0000000001",
Brand: "brand1",
}
userRole, err := userRoleUC.Assign(ctx, req)
```
#### 更新使用者角色
```go
userRole, err := userRoleUC.Update(ctx, "user123", "ROLE0000000002")
```
#### 移除使用者角色
```go
err := userRoleUC.Remove(ctx, "user123")
```
#### 獲取角色的所有使用者
```go
userRoles, err := userRoleUC.GetByRole(ctx, "ROLE0000000001")
```
## ⚙️ 配置說明
### Token 配置
```yaml
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 配置
```yaml
role:
admin_role_uid: "ADMIN" # 管理員角色 UID
uid_prefix: "ROLE" # 角色 UID 前綴
uid_length: 10 # UID 數字長度
```
### RBAC 配置
```yaml
rbac:
enable_cache: true # 是否啟用快取
cache_ttl: 3600 # 快取過期時間(秒)
```
## 🧪 測試
### 運行所有測試
```bash
go test ./pkg/permission/... -v
```
### 運行特定測試
```bash
# 令牌測試
go test ./pkg/permission/usecase -run TestToken -v
# 權限樹測試
go test ./pkg/permission/usecase -run TestPermissionTree -v
# Repository 測試
go test ./pkg/permission/repository -v
```
### 測試覆蓋率
```bash
go test ./pkg/permission/... -cover
```
### 生成覆蓋率報告
```bash
go test ./pkg/permission/... -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html
```
## 💡 最佳實踐
### 1. 令牌管理
#### ✅ 推薦做法
```go
// 使用 Refresh Token 自動刷新
if tokenUC.IsAccessTokenValid(ctx, accessToken) {
// 使用令牌
} else {
// 嘗試刷新
newToken, err := tokenUC.RefreshToken(ctx, refreshReq)
if err != nil {
// 要求重新登錄
}
}
```
#### ❌ 避免做法
```go
// 不要在每次請求都生成新令牌
// 不要將令牌存儲在不安全的地方
// 不要在客戶端解析 Refresh Token
```
### 2. 權限檢查
#### ✅ 推薦做法
```go
// 使用權限檢查中間件
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()
}
}
```
#### ❌ 避免做法
```go
// 不要在每個 Handler 中重複權限檢查代碼
// 不要硬編碼權限名稱
// 不要跳過權限檢查
```
### 3. 權限設計
#### ✅ 推薦做法
```go
// 使用層級式權限結構
// 例如:
// - user
// - user.list
// - user.create
// - user.update
// - user.delete
// 父權限會自動包含,只需設置子權限即可
perms := permission.Permissions{
"user.list": permission.Open,
}
// 展開後會自動包含 "user"
```
#### ❌ 避免做法
```go
// 不要創建過深的權限層級(建議 ≤ 3 層)
// 不要使用過於細緻的權限粒度
// 不要創建循環依賴的權限
```
### 4. 角色管理
#### ✅ 推薦做法
```go
// 使用預定義角色
const (
RoleAdmin = "ADMIN"
RoleManager = "MANAGER"
RoleEmployee = "EMPLOYEE"
)
// 定期檢查無使用者的角色
roles, _ := roleUC.Page(ctx, filter, 1, 100)
for _, role := range roles.List {
if role.UserCount == 0 {
// 考慮刪除或停用
}
}
```
#### ❌ 避免做法
```go
// 不要創建過多的角色
// 不要刪除有使用者的角色
// 不要頻繁修改角色權限
```
## 🔐 安全建議
1. **JWT 密鑰管理**
- 使用強密鑰(至少 32 字元)
- 定期輪換密鑰
- 不要將密鑰硬編碼在代碼中
2. **令牌過期設置**
- Access Token: 15 分鐘 - 1 小時
- Refresh Token: 7 - 30 天
- One-Time Token: 5 - 10 分鐘
3. **設備追蹤**
- 啟用設備指紋追蹤
- 限制每個設備的令牌數量
- 檢測異常登錄行為
4. **權限檢查**
- 在所有 API 端點進行權限檢查
- 使用白名單而非黑名單
- 記錄權限拒絕事件
## 📝 資料庫設計
### MongoDB Collections
#### permissions
```json
{
"_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
```json
{
"_id": ObjectId,
"client_id": 1,
"uid": "ROLE0000000001",
"name": "管理員",
"status": 1,
"create_time": 1234567890,
"update_time": 1234567890
}
```
#### role_permissions
```json
{
"_id": ObjectId,
"role_id": ObjectId,
"permission_id": ObjectId,
"create_time": 1234567890,
"update_time": 1234567890
}
```
#### user_roles
```json
{
"_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
```