软删除机制
7878字约26分钟
2025-07-15
介绍一下 Gorm 的软删除技术,讲解一下与 Mybatis 的一个区别,和传统 MySQL 的差异。
🧾 GORM 软删除详解
在数据库开发中,软删除(Soft Delete) 是一种逻辑上“删除”数据的方式,而不是物理上从表中移除记录。GORM 原生支持软删除功能,通过 gorm.DeletedAt
字段实现。
🔍 什么是软删除?
软删除的本质是在数据表中添加一个字段(如 deleted_at TIMESTAMP
),当执行删除操作时,并不是真正地将记录从数据库中删除,而是设置该字段为当前时间戳。后续查询时自动过滤掉这些标记为已删除的记录。
软删除常用于需要保留历史数据、审计日志或防止误删的场景。
💡 GORM 中的软删除机制
1. 定义软删除字段
在结构体中加入 DeletedAt
字段:
type User struct {
ID uint
Name string
DeletedAt gorm.DeletedAt `gorm:"index"` // 启用软删除
}
GORM 使用 *time.Time
或 gorm.DeletedAt
类型来识别是否启用软删除功能。
2. 删除操作
使用标准的 Delete()
方法即可进行软删除:
db.Delete(&user)
// 实际 SQL: UPDATE users SET deleted_at = NOW() WHERE id = ?
3. 查询忽略软删除记录
默认情况下,GORM 的 Find
, First
等方法会自动忽略 deleted_at IS NOT NULL
的记录。
4. 查询软删除记录
如果需要查看被软删除的数据,可以使用 .Unscoped()
:
var user User
db.Unscoped().Where("id = ?", 1).First(&user)
5. 恢复软删除记录
同样使用 .Unscoped()
修改状态:
db.Unscoped().Model(&user).Update("deleted_at", nil)
⚖️ GORM 软删除 vs MyBatis 软删除
特性 | GORM | MyBatis |
---|---|---|
内置支持 | ✅ 原生支持 | ❌ 需手动实现 |
语法简洁程度 | 高(结构体+标签) | 低(需手写 SQL 判断 is_deleted ) |
自动过滤逻辑删除 | ✅ 默认过滤 | ❌ 需手动加条件 |
恢复逻辑删除 | 简单(更新 DeletedAt 为 nil) | 手动修改字段值 |
性能影响 | 小(索引优化 + 单一字段) | 可能高(依赖字段判断) |
📌 结论:
- GORM 对软删除是封装完整的 ORM 层解决方案。
- MyBatis 更灵活但需要开发者自行维护逻辑删除字段和查询条件。
🔄 GORM 软删除与传统 MySQL 物理删除对比
维度 | 软删除(GORM) | 物理删除(MySQL DELETE) |
---|---|---|
数据是否丢失 | ❌ 不丢失 | ✅ 数据永久丢失 |
可恢复性 | ✅ 可以通过设置 deleted_at = NULL 恢复 | ❌ 很难恢复(除非有备份/binlog) |
查询效率 | 可接受(建立索引后) | 更快 |
存储占用 | 有冗余数据 | 不占空间 |
适合场景 | 日志、审计、用户注销等 | 临时数据清理、垃圾回收等 |
🛠 示例:软删除代码片段(GORM)
type Product struct {
ID uint
Name string
Price float64
DeletedAt gorm.DeletedAt `gorm:"index"`
}
// 创建产品
db.Create(&Product{Name: "iPhone", Price: 999})
// 软删除产品
var product Product
db.First(&product, 1)
db.Delete(&product)
// 查看所有产品(含软删除)
var products []Product
db.Unscoped().Find(&products) // 返回包括 deleted_at != null 的数据
// 恢复软删除
db.Unscoped().Model(&product).Update("deleted_at", nil)
📌 总结表格
对比维度 | GORM 软删除 | MyBatis 软删除 | MySQL 物理删除 |
---|---|---|---|
是否原生支持 | ✅ 是 | ❌ 否 | ✅ 是 |
实现复杂度 | 低 | 高 | 极低 |
数据可恢复性 | ✅ 可恢复 | ✅(取决于实现) | ❌ 几乎不可逆 |
查询效率 | 中等(带过滤条件) | 中等(手写条件) | 高 |
存储开销 | 有冗余 | 有冗余 | 无 |
场景适用 | 审计、用户管理、日志系统等 | 自定义需求强的项目 | 无需保留数据的场景 |
✅ 推荐实践
- 在涉及重要业务数据的模型中启用软删除(如用户、订单、文章等);
- 结合
.Unscoped()
方法实现恢复、统计等功能; - 使用钩子函数(如
BeforeDelete
)进行权限检查或日志记录; - 为
deleted_at
添加索引以提高查询性能; - 定期清理长期未使用的软删除数据(归档或物理删除)。
如果你还想了解如何结合 Gin 框架实现 RESTful API 的软删除接口、或者 GORM 的全局钩子做统一软删除处理策略,我也可以为你扩展讲解 👇
结合 Gin 框架实现 RESTful API 的软删除接口、或者 GORM 的全局钩子做统一软删除处理策略, Gorm-SQLite
好的!我们将结合 GORM + SQLite + Gin 框架,实现一个完整的 RESTful API 接口系统,支持:
- 用户注册、登录(带权限区分)
- 用户列表获取
- 软删除功能(逻辑删除)
- 使用 GORM 的钩子函数统一处理软删除逻辑和日志记录
🧱 技术栈
工具/库 | 版本/用途说明 |
---|---|
Go | 1.20+ |
GORM | ORM 操作数据库 |
SQLite | 轻量级数据库用于本地测试 |
Gin | 构建 RESTful API |
bcrypt | 加密用户密码 |
gorm.DeletedAt | 实现软删除 |
📁 目录结构(简化)
.
├── main.go
├── go.mod
├── models/
│ └── user.go
├── handlers/
│ └── user.go
└── middlewares/
└── auth.go
🔧 第一步:初始化项目 & 安装依赖
go mod init your_project_name
go get -u gorm.io/gorm gorm.io/driver/sqlite github.com/gin-gonic/gin golang.org/x/crypto/bcrypt
🗂️ models/user.go
定义用户模型与软删除字段。
package models
import (
"gorm.io/gorm"
)
type Role string
const (
RoleUser Role = "user"
RoleAdmin Role = "admin"
)
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Username string `json:"username" gorm:"unique"`
Password string `json:"-"`
Role Role `json:"role"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 启用软删除
}
🪝 钩子函数:models/hooks.go
我们可以添加一个全局钩子,在删除操作前做权限检查或日志记录。
package models
import (
"fmt"
"gorm.io/gorm"
)
func RegisterHooks(db *gorm.DB) {
db.Callback().Delete().Before("gorm:delete").Register("before_delete_log", func(tx *gorm.DB) {
fmt.Println("准备执行删除操作...")
})
db.Callback().Delete().After("gorm:delete").Register("after_delete_log", func(tx *gorm.DB) {
fmt.Println("删除操作已完成。")
})
}
📡 handlers/user.go
编写 Gin 处理接口。
package handlers
import (
"net/http"
"your_project_name/models"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
var db *gorm.DB
func SetDB(database *gorm.DB) {
db = database
}
// 注册用户
func Register(c *gin.Context) {
var user models.User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
user.Password = string(hashedPassword)
user.Role = models.RoleUser
if err := db.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "注册失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "注册成功"})
}
// 登录接口
func Login(c *gin.Context) {
var input struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user models.User
if err := db.Where("username = ?", input.Username).First(&user).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "登录成功", "role": user.Role})
}
// 获取所有用户(排除已软删除)
func GetUsers(c *gin.Context) {
var users []models.User
db.Find(&users)
c.JSON(http.StatusOK, users)
}
// 删除用户(软删除)
func DeleteUser(c *gin.Context) {
id := c.Param("id")
if err := db.Delete(&models.User{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}
// 查看被软删除的用户
func GetDeletedUsers(c *gin.Context) {
var users []models.User
db.Unscoped().Where("deleted_at IS NOT NULL", "").Find(&users)
c.JSON(http.StatusOK, users)
}
🧭 main.go
整合 Gin 和 GORM。
package main
import (
"fmt"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"github.com/gin-gonic/gin"
"your_project_name/handlers"
"your_project_name/models"
)
func main() {
// 初始化 SQLite 数据库
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
panic("连接数据库失败")
}
// 自动迁移
db.AutoMigrate(&models.User{})
// 注册钩子
models.RegisterHooks(db)
// 初始化 handlers DB
handlers.SetDB(db)
// 初始化 Gin
r := gin.Default()
// 路由组
api := r.Group("/api")
{
api.POST("/register", handlers.Register)
api.POST("/login", handlers.Login)
api.GET("/users", handlers.GetUsers)
api.DELETE("/users/:id", handlers.DeleteUser)
api.GET("/deleted-users", handlers.GetDeletedUsers)
}
fmt.Println("服务启动于 http://localhost:8080")
r.Run(":8080")
}
✅ 测试接口
使用 Postman 或 curl 进行测试:
注册用户
POST /api/register
{
"username": "alice",
"password": "123456"
}
登录
POST /api/login
{
"username": "alice",
"password": "123456"
}
获取用户列表(不包括软删除)
GET /api/users
获取被软删除的用户
GET /api/deleted-users
删除用户(软删除)
DELETE /api/users/1
🔄 补充:恢复软删除用户
你可以扩展接口实现恢复软删除功能:
func RestoreUser(c *gin.Context) {
id := c.Param("id")
db.Unscoped().Model(&models.User{}).Where("id = ?", id).Update("deleted_at", nil)
c.JSON(http.StatusOK, gin.H{"message": "恢复成功"})
}
然后添加路由:
api.PUT("/restore/users/:id", handlers.RestoreUser)
🎯 总结
功能 | 是否支持 | 说明 |
---|---|---|
软删除 | ✅ | 使用 gorm.DeletedAt |
钩子函数 | ✅ | 在删除前后输出日志 |
权限控制(示例中简单) | ✅(可扩展) | 可以基于 role 字段判断是否管理员 |
RESTful API | ✅ | 使用 Gin 构建接口 |
用户管理 CRUD | ✅ | 支持注册、查询、软删除、恢复 |
数据库引擎 | SQLite | 简单轻量,适合开发测试 |
🚀 下一步建议
- JWT Token 登录认证
- 中间件权限校验
- 使用 Redis 缓存用户信息
- 部署 MySQL 替换 SQLite
- 加入 Swagger 文档
- 单元测试覆盖
如果你希望我继续为你封装 JWT 认证模块、或者构建完整的 RBAC 权限系统(角色-权限关联表),也可以继续提问 👇
- JWT Token 登录认证
- 中间件权限校验
- MySQL 替换 SQLite
- 优化一下整个处理流程,稍微封装一下整个响应请求。
好的!我们将对当前项目进行 功能增强和结构优化,包括以下四个核心扩展:
✅ 目标
- 🧾 使用 JWT 实现登录认证
- 🔐 添加中间件实现权限控制(仅管理员可删除)
- 🛠 将数据库从 SQLite 替换为 MySQL
- 📦 封装统一的 API 响应格式
🗂️ 优化后的目录结构
.
├── main.go
├── go.mod
├── config/
│ └── db.go
├── models/
│ ├── user.go
│ └── hooks.go
├── handlers/
│ ├── auth.go
│ └── user.go
├── middlewares/
│ ├── auth.go
│ └── role.go
├── utils/
│ └── response.go
└── services/
└── jwt.go
1️⃣ 配置数据库连接(MySQL)
config/db.go
package config
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func ConnectDB() *gorm.DB {
dsn := "root:password@tcp(127.0.0.1:3306)/mydb?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("连接数据库失败")
}
return db
}
⚠️ 替换
dsn
中的用户名、密码、数据库名为你自己的 MySQL 配置。
2️⃣ 用户模型与软删除字段
models/user.go
package models
import (
"gorm.io/gorm"
)
type Role string
const (
RoleUser Role = "user"
RoleAdmin Role = "admin"
)
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Username string `json:"username" gorm:"unique"`
Password string `json:"-"`
Role Role `json:"role"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
3️⃣ GORM 软删除钩子函数
models/hooks.go
package models
import (
"fmt"
"gorm.io/gorm"
)
func RegisterHooks(db *gorm.DB) {
db.Callback().Delete().Before("gorm:delete").Register("before_delete_log", func(tx *gorm.DB) {
fmt.Println("准备执行删除操作...")
})
db.Callback().Delete().After("gorm:delete").Register("after_delete_log", func(tx *gorm.DB) {
fmt.Println("删除操作已完成。")
})
}
4️⃣ JWT 登录认证服务
services/jwt.go
package services
import (
"github.com/dgrijalva/jwt-go"
"time"
)
var jwtKey = []byte("your-secret-key")
type Claims struct {
Username string `json:"username"`
Role string `json:"role"`
jwt.StandardClaims
}
func GenerateToken(username, role string) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour)
claims := &Claims{
Username: username,
Role: role,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtKey)
}
5️⃣ 登录接口处理
handlers/auth.go
package handlers
import (
"net/http"
"your_project_name/models"
"your_project_name/services"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
var db *gorm.DB
func SetDB(database *gorm.DB) {
db = database
}
// 登录生成 Token
func Login(c *gin.Context) {
var input struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&input); err != nil {
utils.ErrorResponse(c, http.StatusBadRequest, err.Error())
return
}
var user models.User
if err := db.Where("username = ?", input.Username).First(&user).Error; err != nil {
utils.ErrorResponse(c, http.StatusUnauthorized, "用户名或密码错误")
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)); err != nil {
utils.ErrorResponse(c, http.StatusUnauthorized, "用户名或密码错误")
return
}
token, _ := services.GenerateToken(user.Username, string(user.Role))
utils.SuccessResponse(c, gin.H{"token": token})
}
6️⃣ 中间件:JWT 认证和角色校验
middlewares/auth.go
package middlewares
import (
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"net/http"
"strings"
"your_project_name/services"
)
func AuthMiddleware(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "缺少授权头"})
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
claims := &services.Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return services.JwtKey, nil
})
if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "无效的 token"})
return
}
c.Set("username", claims.Username)
c.Set("role", claims.Role)
c.Next()
}
middlewares/role.go
package middlewares
import "github.com/gin-gonic/gin"
func AdminOnly(c *gin.Context) {
role, _ := c.Get("role")
if role != "admin" {
c.AbortWithStatusJSON(403, gin.H{"error": "无权限访问"})
return
}
c.Next()
}
7️⃣ 统一响应封装
utils/response.go
package utils
import "github.com/gin-gonic/gin"
func SuccessResponse(c *gin.Context, data interface{}) {
c.JSON(200, gin.H{"success": true, "data": data})
}
func ErrorResponse(c *gin.Context, code int, message string) {
c.JSON(code, gin.H{"success": false, "message": message})
}
8️⃣ 优化用户处理逻辑
handlers/user.go
package handlers
import (
"net/http"
"your_project_name/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
var db *gorm.DB
func SetDB(database *gorm.DB) {
db = database
}
func GetUsers(c *gin.Context) {
var users []models.User
db.Find(&users)
utils.SuccessResponse(c, users)
}
func DeleteUser(c *gin.Context) {
id := c.Param("id")
db.Delete(&models.User{}, id)
utils.SuccessResponse(c, gin.H{"message": "删除成功"})
}
func RestoreUser(c *gin.Context) {
id := c.Param("id")
db.Unscoped().Model(&models.User{}).Where("id = ?", id).Update("deleted_at", nil)
utils.SuccessResponse(c, gin.H{"message": "恢复成功"})
}
9️⃣ 启动主程序 main.go
package main
import (
"your_project_name/config"
"your_project_name/handlers"
"your_project_name/middlewares"
"your_project_name/models"
"github.com/gin-gonic/gin"
)
func main() {
db := config.ConnectDB()
models.RegisterHooks(db)
handlers.SetDB(db)
r := gin.Default()
api := r.Group("/api")
{
api.POST("/login", handlers.Login)
// 受保护的路由
protected := api.Group("")
protected.Use(middlewares.AuthMiddleware)
{
protected.GET("/users", handlers.GetUsers)
admin := protected.Group("")
admin.Use(middlewares.AdminOnly)
{
admin.DELETE("/users/:id", handlers.DeleteUser)
admin.PUT("/restore/users/:id", handlers.RestoreUser)
}
}
}
r.Run(":8080")
}
✅ 请求示例
登录获取 Token
POST /api/login
{
"username": "alice",
"password": "123456"
}
返回:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6..."
}
获取用户列表(所有角色都可访问)
GET /api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
删除用户(仅限管理员)
DELETE /api/users/1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
🎯 总结
功能 | 描述 |
---|---|
数据库 | 改为使用 MySQL |
登录认证 | 使用 JWT 实现 |
权限控制 | 使用 Gin 中间件限制只有 admin 可删除 |
响应统一封装 | 使用 utils.Response 返回统一格式 |
结构清晰 | 模块化设计,易于维护和扩展 |
🚀 下一步建议
- 🧪 添加单元测试(使用 Testify + GORM 测试模式)
- 📄 加入 Swagger 接口文档支持
- 📦 使用 Redis 缓存 Token 黑名单
- 🧭 构建完整的 RBAC 系统(角色-权限关联)
如果你希望我继续帮你添加这些高级模块,欢迎随时告诉我 👇
实现[测试、缓存、RBAC]
- 🧪 添加单元测试(使用 Testify + GORM 测试模式)
- 📦 使用 Redis 缓存 Token 黑名单
- 🧭 构建完整的 RBAC 系统(角色-权限关联)
非常棒!你已经打下了坚实的基础,现在我们继续为这个项目添加 单元测试、Redis 缓存 Token 黑名单 和 完整的 RBAC 权限系统(角色-权限关联) 。我们将依次讲解每个模块,并提供完整的示例代码。
✅ 第一部分:添加单元测试(使用 Testify + GORM 测试模式)
我们将使用以下工具:
-
testing
标准库 -
testify
的require/assert
做断言 - GORM 的内存数据库(SQLite 内存模式)做隔离测试
1. 安装依赖
go get github.com/stretchr/testify
2. 示例测试文件 handlers/user_test.go
package handlers
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func setupTestDB() *gorm.DB {
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
db.AutoMigrate(&models.User{})
return db
}
func TestGetUsers(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.Default()
// 初始化测试 DB
testDB := setupTestDB()
SetDB(testDB)
// 插入测试数据
testDB.Create(&models.User{Username: "alice", Password: "123456", Role: models.RoleUser})
testDB.Create(&models.User{Username: "bob", Password: "abcdef", Role: models.RoleAdmin})
r.GET("/api/users", GetUsers)
req, _ := http.NewRequest(http.MethodGet, "/api/users", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
_ = json.Unmarshal(w.Body.Bytes(), &resp)
data := resp["data"].([]interface{})
assert.Len(t, data, 2)
}
3. 示例登录测试 handlers/auth_test.go
func TestLoginSuccess(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.Default()
testDB := setupTestDB()
SetDB(testDB)
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
testDB.Create(&models.User{
Username: "alice",
Password: string(hashedPassword),
Role: models.RoleUser,
})
r.POST("/api/login", Login)
body := bytes.NewBufferString(`{"username":"alice","password":"123456"}`)
req, _ := http.NewRequest(http.MethodPost, "/api/login", body)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "token")
}
✅ 第二部分:使用 Redis 缓存 Token 黑名单
当用户注销时,我们需要将 Token 加入黑名单,防止其继续被使用。
1. 安装 Redis 包
go get github.com/go-redis/redis/v8
2. 创建 Redis 工具包 utils/redis.go
package utils
import (
"context"
"time"
"github.com/go-redis/redis/v8"
)
var RedisClient *redis.Client
func InitRedis() {
RedisClient = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
}
func AddToBlacklist(token string, exp time.Duration) error {
ctx := context.Background()
return RedisClient.Set(ctx, "blacklist:"+token, "1", exp).Err()
}
func IsTokenBlacklisted(token string) bool {
ctx := context.Background()
val, _ := RedisClient.Get(ctx, "blacklist:"+token).Result()
return val == "1"
}
3. 修改 JWT 登出接口 handlers/auth.go
// 新增登出接口
func Logout(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
_, err := jwt.ParseWithClaims(tokenString, &services.Claims{}, func(token *jwt.Token) (interface{}, error) {
return services.JwtKey, nil
})
if err == nil {
utils.AddToBlacklist(tokenString, 24*time.Hour)
utils.SuccessResponse(c, gin.H{"message": "已登出"})
return
}
utils.ErrorResponse(c, http.StatusUnauthorized, "无效的 token")
}
4. 修改中间件检查是否在黑名单中 middlewares/auth.go
if utils.IsTokenBlacklisted(tokenString) {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "token 已失效,请重新登录"})
return
}
✅ 第三部分:构建完整的 RBAC 系统(角色-权限关联)
我们将实现:
- 角色表 (
roles
) - 权限表 (
permissions
) - 角色-权限关联表 (
role_permissions
) - 用户-角色表 (
user_roles
) - 接口控制基于权限名(如
can_delete_user
,can_edit_content
)
1. 数据模型定义
models/role.go
type Role struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"unique"`
Permissions []Permission `json:"permissions" gorm:"many2many:role_permissions;"`
DeletedAt gorm.DeletedAt `json:"-"`
}
models/permission.go
type Permission struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"unique"` // 如 can_delete_user, can_edit_post
}
2. 自动迁移与初始化权限
main.go
db.AutoMigrate(&models.User{}, &models.Role{}, &models.Permission{})
// 初始化默认权限
db.FirstOrCreate(&models.Permission{Name: "can_delete_user"}, models.Permission{Name: "can_delete_user"})
db.FirstOrCreate(&models.Permission{Name: "can_edit_post"}, models.Permission{Name: "can_edit_post"})
// 初始化角色
var adminRole models.Role
db.Where("name = ?", "admin").FirstOrCreate(&adminRole, models.Role{Name: "admin"})
db.Model(&adminRole).Association("Permissions").Append([]models.Permission{
{Name: "can_delete_user"},
{Name: "can_edit_post"},
})
3. 中间件:权限校验
middlewares/permission.go
func HasPermission(permissionName string) gin.HandlerFunc {
return func(c *gin.Context) {
username, _ := c.Get("username")
var user models.User
db.Where("username = ?", username).Preload("Roles.Permissions").First(&user)
for _, role := range user.Roles {
for _, perm := range role.Permissions {
if perm.Name == permissionName {
c.Next()
return
}
}
}
c.AbortWithStatusJSON(403, gin.H{"error": "无权限操作"})
}
}
4. 控制器中使用权限控制
handlers/user.go
protected.POST("/delete/:id", middlewares.HasPermission("can_delete_user"), handlers.DeleteUser)
🎯 总结功能扩展
模块 | 功能描述 |
---|---|
单元测试 | 使用 testify + SQLite in-memory 进行隔离测试 |
Redis Token 黑名单 | 登出后 Token 不再可用 |
RBAC 系统 | 支持角色-权限多对多,细粒度权限控制 |
🚀 下一步建议
- 📄 添加 Swagger 文档支持,使用
swaggo/swag
- 🧪 添加集成测试,测试整个 API 链路
- 🔐 引入 HTTPS + TLS 配置
- 📦 构建 Docker 容器部署
- 📊 添加 Prometheus 监控指标
如果你希望我帮你实现其中任意一个模块(比如 Swagger 接口文档),欢迎随时告诉我👇 我会继续为你扩展完整代码和配置说明。
🔍 代码解析:
db.Where("username = ?", username).Preload("Roles.Permissions").First(&user)
分解说明:
部分 | 含义 |
---|---|
db | GORM 的数据库连接对象,代表当前与数据库的会话。 |
.Where("username = ?", username) | 添加查询条件,查找用户名等于传入的 username 的用户记录。 |
.Preload("Roles.Permissions") | 预加载用户拥有的角色以及这些角色所拥有的权限。这一步是为了避免后续访问时发生 N+1 查询问题。 |
.First(&user) | 查找第一条匹配结果并赋值给变量 user 。 |
🧱 假设模型结构如下(简化版):
type User struct {
ID uint `gorm:"primaryKey"`
Username string `gorm:"unique"`
Password string
Roles []Role `gorm:"many2many:user_roles;"`
}
type Role struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"unique"`
Permissions []Permission `gorm:"many2many:role_permissions;"`
}
type Permission struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"unique"` // 如 "can_delete_user", "can_edit_post"
}
💡 举例说明:
假设数据库中有以下数据:
用户表 (users
)
id | username |
---|---|
1 | alice |
角色表 (roles
)
id | name |
---|---|
1 | admin |
权限表 (permissions
)
id | name |
---|---|
1 | can_delete_user |
2 | can_edit_content |
角色-权限关联表 (role_permissions
)
role_id | permission_id |
---|---|
1 | 1 |
1 | 2 |
用户-角色关联表 (user_roles
)
user_id | role_id |
---|---|
1 | 1 |
✅ 执行效果:
当执行:
var user User
db.Where("username = ?", "alice").Preload("Roles.Permissions").First(&user)
GORM 会:
- 先查出
alice
这个用户; - 然后根据其关联的
Roles
表(即admin
角色); - 再查出该角色的所有
Permissions
(即can_delete_user
,can_edit_content
);
- 先查出
最终,你可以在代码中直接访问用户的权限:
for _, role := range user.Roles {
for _, perm := range role.Permissions {
fmt.Println(perm.Name) // 输出:can_delete_user, can_edit_content
}
}
⚠️ 注意事项:
- 如果没有
.Preload("Roles.Permissions")
,在访问user.Roles
或user.Roles.Permissions
时会发生懒加载(Lazy Loading),导致额外的 SQL 查询,影响性能。 - 使用
.Preload()
可以一次性将所需的数据全部加载进来,提高效率,适用于 RBAC、菜单树等嵌套结构场景。
🛠 使用场景示例(结合中间件做权限判断)
func HasPermission(permissionName string) gin.HandlerFunc {
return func(c *gin.Context) {
username, _ := c.Get("username")
var user User
db.Where("username = ?", username).
Preload("Roles.Permissions").
First(&user)
hasPerm := false
for _, role := range user.Roles {
for _, perm := range role.Permissions {
if perm.Name == permissionName {
hasPerm = true
break
}
}
}
if !hasPerm {
c.AbortWithStatusJSON(403, gin.H{"error": "无权限操作"})
return
}
c.Next()
}
}
然后你可以这样使用它:
protected.DELETE("/users/:id", middlewares.HasPermission("can_delete_user"), handlers.DeleteUser)
📌 总结:
内容 | 说明 |
---|---|
.Where(...) | 查询指定用户名的用户 |
.Preload("Roles.Permissions") | 一次性加载角色和权限,避免 N+1 查询 |
.First(&user) | 获取第一个匹配的用户记录 |
用途 | 实现 RBAC 权限验证中的权限校验逻辑 |
如果你还想了解如何用 Swagger 自动生成接口文档、或者用 Redis 缓存权限信息提升性能,我也可以继续为你讲解 👇
缓存权限信息,可以做些什么?配合 RBAC
在 RBAC(基于角色的访问控制) 系统中,缓存权限信息可以显著提升性能和用户体验。特别是在高并发场景下,频繁地从数据库查询用户的权限可能会导致延迟、资源浪费甚至成为瓶颈。
🧠 缓存权限信息可以做什么?
将用户或角色的权限信息缓存到内存或 Redis 中,主要有以下作用:
功能/优势 | 说明 |
---|---|
❌ 减少数据库查询 | 用户每次请求时不再需要查 DB 获取权限,提高响应速度 |
⚡ 提升性能 | 尤其适用于高频接口调用、权限校验密集的系统 |
🔁 实现快速权限刷新机制 | 当权限变更时主动更新缓存,而不是等待过期 |
🔄 支持 RBAC 权限热更新 | 修改权限后无需重启服务即可生效 |
🧱 降低数据库压力 | 减轻数据库负担,提高整体系统稳定性 |
💡 使用场景举例
- ✅ 用户登录后,将其所有权限缓存到 Redis 或内存中;
- 🔐 每次接口请求时,直接从缓存获取权限进行校验;
- 🔄 管理员修改某个角色的权限后,自动清理该角色的缓存;
- 🕒 设置合理的缓存过期时间(如 5 分钟),防止数据陈旧。
🛠️ 如何实现:配合 RBAC 系统缓存权限
我们以使用 Redis 缓存为例,介绍如何实现一个高效的权限缓存系统。
1. 安装依赖
go get github.com/go-redis/redis/v8
2. 初始化 Redis 客户端(utils/redis.go
)
package utils
import (
"context"
"github.com/go-redis/redis/v8"
)
var RedisClient *redis.Client
var Ctx = context.Background()
func InitRedis() {
RedisClient = redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Redis 地址
Password: "", // 密码(无则留空)
DB: 0, // 默认数据库
})
}
3. 缓存用户权限逻辑(services/permission_cache.go
)
package services
import (
"context"
"encoding/json"
"fmt"
"time"
"your_project_name/models"
"your_project_name/utils"
)
// 缓存 Key 格式:permissions:<username>
const permissionCacheKey = "permissions:%s"
// 缓存权限到 Redis
func CacheUserPermissions(username string, perms []string) error {
key := fmt.Sprintf(permissionCacheKey, username)
data, _ := json.Marshal(perms)
return utils.RedisClient.Set(utils.Ctx, key, data, 5*time.Minute).Err()
}
// 从 Redis 获取权限
func GetUserPermissionsFromCache(username string) ([]string, bool) {
key := fmt.Sprintf(permissionCacheKey, username)
val, err := utils.RedisClient.Get(utils.Ctx, key).Result()
if err != nil {
return nil, false
}
var perms []string
_ = json.Unmarshal([]byte(val), &perms)
return perms, true
}
// 清除用户的权限缓存
func ClearUserPermissionCache(username string) {
key := fmt.Sprintf(permissionCacheKey, username)
utils.RedisClient.Del(utils.Ctx, key)
}
4. 登录时缓存权限(handlers/auth.go
)
// 用户登录成功后,缓存权限
var user models.User
db.Where("username = ?", input.Username).
Preload("Roles.Permissions").
First(&user)
// 提取权限名列表
var permissions []string
for _, role := range user.Roles {
for _, perm := range role.Permissions {
permissions = append(permissions, perm.Name)
}
}
// 缓存权限
services.CacheUserPermissions(user.Username, permissions)
5. 中间件读取缓存权限(middlewares/permission.go
)
func HasPermission(requiredPerm string) gin.HandlerFunc {
return func(c *gin.Context) {
username, _ := c.Get("username")
perms, ok := services.GetUserPermissionsFromCache(username.(string))
if !ok {
c.AbortWithStatusJSON(401, gin.H{"error": "权限缓存未命中,请重新登录"})
return
}
hasPerm := false
for _, p := range perms {
if p == requiredPerm {
hasPerm = true
break
}
}
if !hasPerm {
c.AbortWithStatusJSON(403, gin.H{"error": "无权限操作"})
return
}
c.Next()
}
}
6. 角色权限变更时清除缓存(handlers/role.go
)
func UpdateRolePermissions(c *gin.Context) {
roleName := c.Param("name")
// ... 更新角色权限逻辑 ...
// 假设我们知道哪些用户属于这个角色,批量清除他们的权限缓存
var users []models.User
db.Where("role = ?", roleName).Find(&users)
for _, user := range users {
services.ClearUserPermissionCache(user.Username)
}
c.JSON(200, gin.H{"message": "权限已更新,缓存已清除"})
}
🎯 总结:缓存权限 + RBAC 的优势
优点 | 描述 |
---|---|
✅ 高性能 | 不再每次都去数据库查询权限 |
✅ 可控性 | 可设定缓存时间、主动刷新 |
✅ 高可用 | 即使数据库暂时不可用,仍可继续验证缓存中的权限 |
✅ 易扩展 | 后续可接入分布式缓存(如 Redis 集群)、支持更多权限维度 |
🚀 下一步建议
你可以进一步优化权限缓存系统:
功能 | 说明 |
---|---|
🧪 编写单元测试验证缓存是否生效 | 使用 testify 测试缓存命中与失效 |
🧩 添加权限标签树结构 | 支持按模块(如 user: 、post: )做通配符匹配 |
📊 接入 Prometheus 监控缓存命中率 | 统计 hit / miss 次数 |
📦 支持多级缓存(内存 + Redis) | 本地 LRU + Redis 两级缓存架构 |
📈 用布隆过滤器减少无效查询 | 防止大量不存在用户的无效缓存查询 |
如果你希望我继续为你添加这些高级功能(比如:多级缓存、通配符权限匹配、Prometheus 监控等),欢迎随时告诉我 👇 我会继续为你编写完整代码和配置!
📦 支持多级缓存(内存 + Redis) | 本地 LRU + Redis 两级缓存架构 |
---|---|
📈 用布隆过滤器减少无效查询 | 防止大量不存在用户的无效缓存查询 |
非常好!我们来一起实现 多级缓存架构(内存 LRU + Redis) 和 使用布隆过滤器减少无效查询。这是现代高性能权限系统中常见的做法,既能保证访问速度,又能降低后端压力。
🧩 一、支持多级缓存:内存 LRU + Redis
🎯 目标:
- 使用本地内存缓存(LRU)做一级缓存,提升访问速度;
- 使用 Redis 做二级缓存,防止服务重启数据丢失;
- 查询时优先查内存 → 查 Redis → 回退数据库;
- 更新时同步清理本地缓存并更新 Redis。
📦 安装依赖:
go get github.com/hashicorp/golang-lru/v2
🔧 缓存结构设计
我们将构建一个 PermissionCache
结构体,封装以下逻辑:
层级 | 类型 | 特点 |
---|---|---|
一级缓存 | LRU 内存缓存 | 快速响应、低延迟 |
二级缓存 | Redis | 持久化、分布式共享 |
最终回退 | 数据库 | 确保最终一致性 |
📁 文件结构(新增)
utils/
├── redis.go
├── lru_cache.go
services/
└── permission_cache.go
📄 1. 实现 LRU 本地缓存(utils/lru_cache.go
)
package utils
import (
"sync"
"github.com/hashicorp/golang-lru/v2"
)
const LocalCacheSize = 1024 // 可调整大小
var (
localCache *lru.Cache[string, []string]
mu sync.RWMutex
)
func InitLocalCache() {
var err error
localCache, err = lru.NewWithEvict[string, []string](LocalCacheSize, func(key string, value []string) {
// 可选的淘汰回调(如写入日志或后台持久化)
})
if err != nil {
panic("无法初始化本地 LRU 缓存")
}
}
📄 2. 多级缓存封装(services/permission_cache.go
)
package services
import (
"context"
"encoding/json"
"fmt"
"time"
"your_project_name/utils"
)
// Redis Key 格式
const permissionRedisKey = "permissions:%s"
// 获取用户权限(先查内存 → 再查 Redis → 最后查 DB)
func GetUserPermissions(username string) ([]string, bool) {
// 先查本地 LRU 缓存
if perms, ok := utils.LocalCache.Get(username); ok {
return perms, true
}
// 再查 Redis
val, err := utils.RedisClient.Get(utils.Ctx, fmt.Sprintf(permissionRedisKey, username)).Result()
if err == nil {
var perms []string
_ = json.Unmarshal([]byte(val), &perms)
utils.LocalCache.Add(username, perms) // 加入本地缓存
return perms, true
}
return nil, false
}
// 设置权限(同时写入 Redis 和本地缓存)
func SetUserPermissions(username string, perms []string) error {
data, _ := json.Marshal(perms)
utils.LocalCache.Add(username, perms)
return utils.RedisClient.Set(utils.Ctx, fmt.Sprintf(permissionRedisKey, username), data, 5*time.Minute).Err()
}
// 清除缓存(用于角色权限变更时)
func ClearUserPermissionCache(username string) {
utils.LocalCache.Remove(username)
utils.RedisClient.Del(utils.Ctx, fmt.Sprintf(permissionRedisKey, username))
}
✨ 中间件使用方式不变(仍调用 GetUserPermissions
)
func HasPermission(requiredPerm string) gin.HandlerFunc {
return func(c *gin.Context) {
username, _ := c.Get("username")
perms, ok := services.GetUserPermissions(username.(string))
if !ok {
c.AbortWithStatusJSON(401, gin.H{"error": "未授权,请重新登录"})
return
}
for _, p := range perms {
if p == requiredPerm {
c.Next()
return
}
}
c.AbortWithStatusJSON(403, gin.H{"error": "无权限操作"})
}
}
🧺 二、使用布隆过滤器减少无效查询
🎯 目标:
避免大量不存在用户的请求频繁穿透到 Redis 或数据库,造成缓存击穿和资源浪费。
📦 安装 Bloom Filter:
go get github.com/tylertreat/BoomFilters
📁 文件结构扩展:
utils/
├── bloom_filter.go
📄 1. 初始化布隆过滤器(utils/bloom_filter.go
)
package utils
import (
"github.com/tylertreat/BoomFilters"
)
var UserExistsFilter *bloomfilter.ScalableBloomFilter
func InitBloomFilter() {
UserExistsFilter = bloomfilter.NewDefaultScalableBloomFilter(1000000, 0.01)
}
📄 2. 在用户注册、登录时加入布隆过滤器判断
用户注册时加入布隆过滤器:
// 注册成功后加入布隆过滤器
utils.UserExistsFilter.Add([]byte(username))
登录时提前拦截无效用户名:
if !utils.UserExistsFilter.Test([]byte(input.Username)) {
utils.ErrorResponse(c, http.StatusUnauthorized, "用户名或密码错误")
return
}
🔄 同步布隆过滤器与真实数据库状态
你可以定期从数据库中加载所有用户名,重建布隆过滤器,确保其准确度:
func SyncBloomFilterFromDB(db *gorm.DB) {
var usernames []string
db.Model(&models.User{}).Pluck("username", &usernames)
filter := bloomfilter.NewDefaultScalableBloomFilter(uint(len(usernames)*2), 0.01)
for _, u := range usernames {
filter.Add([]byte(u))
}
utils.UserExistsFilter = filter
}
你可以在项目启动时执行一次,或定时任务每小时更新一次。
🎯 总结:多级缓存 + 布隆过滤器的优势
功能 | 优势 |
---|---|
🧩 多级缓存 | 提升性能、容灾能力强、服务重启不丢数据 |
⏱️ 本地 LRU 缓存 | 极低延迟、高命中率 |
📡 Redis 分布式缓存 | 支持集群部署、缓存共享 |
🧹 布隆过滤器 | 防止缓存穿透、拒绝无效用户名查询 |
🔁 自动刷新机制 | 权限变化后缓存自动失效,不影响业务逻辑 |
🚀 下一步建议
你可以继续优化这个系统:
方向 | 描述 |
---|---|
🧪 单元测试 | 对 LRU、Redis、Bloom Filter 做 mock 测试 |
📊 Prometheus 监控 | 统计命中率、穿透率、缓存时间等指标 |
🧠 通配符权限匹配 | 如 post:* , user:delete ,提高灵活性 |
🛡️ 限流熔断 | 防止恶意攻击导致缓存过载 |
📦 多租户支持 | 每个租户独立缓存空间、隔离权限体系 |
如果你希望我继续为你添加这些高级功能(例如 Prometheus 监控模块、通配符权限解析),欢迎随时告诉我 👇 我会继续为你完善整个权限缓存系统!
📊 一、使用 Prometheus 统计缓存命中率等指标
✅ 功能目标:
- 记录缓存命中数(LRU / Redis)
- 记录缓存穿透数(未命中且查 DB)
- 暴露
/metrics
接口供 Prometheus 抓取
🔧 实现步骤:
1. 安装 Prometheus Go Client:
go get github.com/prometheus/client_golang
2. 创建监控指标文件 utils/metrics.go
package utils
import (
"github.com/prometheus/client_golang/prometheus"
)
var (
cacheHits = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "permission_cache_hits_total",
Help: "Number of permission cache hits by layer (lru/redis)",
},
[]string{"layer"},
)
cacheMisses = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "permission_cache_misses_total",
Help: "Total number of permission cache misses",
},
)
cachePenetrations = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "permission_cache_penetration_total",
Help: "Number of cache penetration attempts (invalid user)",
},
)
)
func InitMetrics() {
prometheus.MustRegister(cacheHits)
prometheus.MustRegister(cacheMisses)
prometheus.MustRegister(cachePenetrations)
}
func IncCacheHit(layer string) {
cacheHits.WithLabelValues(layer).Inc()
}
func IncCacheMiss() {
cacheMisses.Inc()
}
func IncPenetration() {
cachePenetrations.Inc()
}
3. 在 main.go
中注册 Prometheus 路由:
import (
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// ...
r.GET("/metrics", gin.WrapH(promhttp.Handler()))
4. 修改权限缓存服务逻辑,加入监控统计(services/permission_cache.go
)
func GetUserPermissions(username string) ([]string, bool) {
if perms, ok := utils.LocalCache.Get(username); ok {
utils.IncCacheHit("lru")
return perms, true
}
val, err := utils.RedisClient.Get(utils.Ctx, fmt.Sprintf(permissionRedisKey, username)).Result()
if err == nil {
var perms []string
_ = json.Unmarshal([]byte(val), &perms)
utils.LocalCache.Add(username, perms)
utils.IncCacheHit("redis")
return perms, true
}
utils.IncCacheMiss()
return nil, false
}
func ClearUserPermissionCache(username string) {
utils.LocalCache.Remove(username)
utils.RedisClient.Del(utils.Ctx, fmt.Sprintf(permissionRedisKey, username))
}
5. 登录时拦截无效用户名并记录穿透次数:
if !utils.UserExistsFilter.Test([]byte(input.Username)) {
utils.IncPenetration()
utils.ErrorResponse(c, http.StatusUnauthorized, "用户名或密码错误")
return
}
✅ 效果:访问 /metrics
可查看如下指标
# HELP permission_cache_hits_total Number of permission cache hits by layer (lru/redis)
# TYPE permission_cache_hits_total counter
permission_cache_hits_total{layer="lru"} 100
permission_cache_hits_total{layer="redis"} 50
# HELP permission_cache_misses_total Total number of permission cache misses
# TYPE permission_cache_misses_total counter
permission_cache_misses_total 10
# HELP permission_cache_penetration_total Number of cache penetration attempts (invalid user)
# TYPE permission_cache_penetration_total counter
permission_cache_penetration_total 3
🧠 二、通配符权限匹配(Wildcard Permission Match)
✅ 功能目标:
- 支持类似
post:*
,user:delete
的权限结构 - 提高权限表达能力,简化角色配置
🔧 实现代码:
文件:services/permission_matcher.go
package services
import (
"strings"
)
// 匹配通配符权限,如 can(post:create, post:*) => true
func HasPermission(requiredPerm string, userPerms []string) bool {
for _, p := range userPerms {
if p == requiredPerm {
return true
}
// 处理 wildcard,如 user:* 匹配 user:read, user:write
if strings.HasSuffix(p, ":*") {
prefix := p[:len(p)-1] // 去掉 *
if strings.HasPrefix(requiredPerm, prefix) {
return true
}
}
}
return false
}
✅ 示例用法:
if !services.HasPermission("post:edit", userPerms) {
c.AbortWithStatusJSON(403, gin.H{"error": "无权限操作"})
return
}
若用户拥有
post:*
权限,则可以访问post:create
,post:edit
,post:delete
等所有子权限。
🛡️ 三、限流熔断机制(Rate Limiting + Circuit Breaker)
✅ 功能目标:
- 防止恶意请求频繁穿透缓存和数据库
- 设置单位时间内最大请求数
- 使用滑动窗口算法实现平滑限流
🔧 实现方式:使用 gRPC
或 Gin
插件,这里我们使用 Gin + uber-go/ratelimit
1. 安装依赖:
go get go.uber.org/ratelimit
2. 创建中间件 middlewares/rate_limit.go
package middlewares
import (
"time"
"go.uber.org/ratelimit"
"github.com/gin-gonic/gin"
)
// 创建一个每秒最多 100 次请求的限流器
var limiter = ratelimit.New(100, ratelimit.WithSlidingWindow(1*time.Second))
func RateLimit() gin.HandlerFunc {
return func(c *gin.Context) {
if limiter.Take() == zeroTime {
c.AbortWithStatusJSON(429, gin.H{"error": "请求过于频繁,请稍后再试"})
return
}
c.Next()
}
}
var zeroTime time.Time
3. 在路由中启用限流:
r.Use(middlewares.RateLimit())
📦 四、多租户支持(可选高级功能)
如果你有多个客户/组织,可以为每个租户建立独立缓存空间:
const permissionRedisKey = "permissions:%s:%s" // tenantID:username
在中间件中读取租户 ID,并将权限缓存键加上前缀。这能保证不同租户之间权限隔离。
🎯 总结:新增功能一览表
功能 | 描述 |
---|---|
📊 Prometheus 监控 | 记录缓存命中率、穿透率、缓存刷新时间等 |
🧠 通配符权限匹配 | 如 post:* 匹配 post:create 、post:delete |
🛡️ 限流熔断机制 | 防止高频攻击、缓存击穿 |
📦 多租户权限隔离 | 不同租户缓存和权限互不干扰 |
🚀 下一步建议
可以继续:
- 📌 添加 JWT 刷新 Token 机制;
- 📁 将权限模型从 GORM 移植到 Casbin(更强大的 RBAC 库);
- 📋 添加审计日志(Audit Log),记录谁在什么时间执行了哪些权限操作;
- 🧱 实现完整的多级权限树,支持前端动态菜单渲染;
- 🧪 编写完整的单元测试套件验证缓存命中率、穿透检测等功能;