feat(auth): implement session-based token management with revocation support
Add Session model, SessionService, and SessionRepository to track user login sessions and enable token revocation on auth-critical events. - Introduce explicit TokenType (access/refresh) in JWT claims to prevent refresh token misuse via access token endpoints - Add SessionID field to JWT claims, enabling stateless JWT validation against revoked sessions - Replace legacy Auth middleware with RequireAuth/OptionalAuth pipeline that validates token type, account status, and session validity - Implement session revocation on password change, reset, user ban/inactive, and explicit logout - Add Principal cache with active invalidation for banned/role-changed users - Fix IDOR vulnerability: GetMessagesByCursor now validates currentUserID is conversation participant via GetParticipantStrict - Add group member visibility checks: announcements, group info, member list now require group membership - Simplify Casbin policy: remove g grouping, use r.sub == p.sub matcher with globMatch; user_roles table is single source of truth for roles - Add migration logic to clean legacy casbin g rules and migrate old p rules from path-style to admin/<domain> resource naming
This commit is contained in:
@@ -234,6 +234,9 @@ func autoMigrate(db *gorm.DB) error {
|
||||
|
||||
// 文件上传记录(用于聊天文件 TTL 过期清理)
|
||||
&model.UploadedFile{},
|
||||
|
||||
// 会话(令牌撤销支持)
|
||||
&model.Session{},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -254,6 +257,12 @@ func autoMigrate(db *gorm.DB) error {
|
||||
return fmt.Errorf("failed to seed permissions: %w", err)
|
||||
}
|
||||
|
||||
// 清理历史 Casbin g 分组规则(真源已统一为 user_roles 表,Casbin 仅保留 p 策略)。
|
||||
// 幂等:仅在 casbin_rule 中存在 ptype='g' 记录时删除。
|
||||
if err := cleanupLegacyCasbinGRules(db); err != nil {
|
||||
return fmt.Errorf("failed to cleanup legacy casbin g rules: %w", err)
|
||||
}
|
||||
|
||||
// 初始化频道配置种子数据
|
||||
if err := seedChannels(db); err != nil {
|
||||
return fmt.Errorf("failed to seed channels: %w", err)
|
||||
@@ -285,6 +294,27 @@ func dropPostsHotScoreColumnIfExists(db *gorm.DB) error {
|
||||
return m.DropColumn(&postWithHotScore{}, "HotScore")
|
||||
}
|
||||
|
||||
// cleanupLegacyCasbinGRules 清理历史 Casbin g 分组规则。
|
||||
//
|
||||
// 真源策略变更后:user_roles 表为用户-角色唯一真源,Casbin 仅保留 p, role, resource, action 策略。
|
||||
// 旧的 g, user, role 记录不再被 matcher 使用(model.conf 已移除 g 依赖),保留只会造成管理后台误解。
|
||||
// 幂等:仅删除 ptype='g' 的记录;若不存在则无操作。
|
||||
func cleanupLegacyCasbinGRules(db *gorm.DB) error {
|
||||
if !db.Migrator().HasTable(&model.CasbinRule{}) {
|
||||
return nil
|
||||
}
|
||||
result := db.Where("ptype = ?", "g").Delete(&model.CasbinRule{})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected > 0 {
|
||||
zap.L().Info("Cleaned up legacy casbin g rules",
|
||||
zap.Int64("count", result.RowsAffected),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// seedRoles 初始化角色种子数据
|
||||
func seedRoles(db *gorm.DB) error {
|
||||
// 检查是否已有角色数据
|
||||
@@ -344,6 +374,14 @@ func seedRoles(db *gorm.DB) error {
|
||||
}
|
||||
|
||||
// seedPermissions 初始化权限策略种子数据
|
||||
//
|
||||
// 真源策略:user_roles 表为用户-角色真源,Casbin 仅维护 p, role, resource, action 策略。
|
||||
// 资源命名约定:admin/<domain>[/<sub>](路径式,配合 globMatch 中 * 不跨 /、** 跨 / 的语义)。
|
||||
//
|
||||
// 升级兼容:旧版本以 URL 路径式资源(如 /api/v1/admin/*、/*)作为 Casbin 策略,
|
||||
// 新版本改为 admin/<domain> 资源命名后,存量部署需要迁移。本函数检测到任何 v1 以 '/' 开头的
|
||||
// 旧策略时,将其视为旧版本部署:清空所有 p 策略并按新约定重新 seed。
|
||||
// 新部署无此条件不触发,幂等(admin/<domain> 不以 '/' 开头,不会误判为新旧迁移)。
|
||||
func seedPermissions(db *gorm.DB) error {
|
||||
// 检查是否已有权限策略数据
|
||||
var count int64
|
||||
@@ -351,43 +389,79 @@ func seedPermissions(db *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果已有数据,跳过种子初始化
|
||||
if count > 0 {
|
||||
return nil
|
||||
// 检测是否存在旧版本路径式策略(v1 以 '/' 开头)。
|
||||
// 旧策略例如:{p, super_admin, /*, *} / {p, admin, /api/v1/admin/*, *} 等。
|
||||
// 新约定资源形如 admin/users(不以 '/' 开头),不会被本条件误命中。
|
||||
// 旧策略在新 model.conf 的 admin/<domain> 命名下永远不会匹配,保留只会让管理后台权限全断。
|
||||
var legacyCount int64
|
||||
if err := db.Model(&model.CasbinRule{}).
|
||||
Where("ptype = ? AND v1 LIKE '/%'", "p").
|
||||
Count(&legacyCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if legacyCount == 0 {
|
||||
// 已是新版本策略,无需重复 seed。
|
||||
return nil
|
||||
}
|
||||
|
||||
// 命中旧策略:迁移。先 dump 待删除策略到日志便于回查,再删除全部 p 策略。
|
||||
zap.L().Info("Migrating legacy path-based casbin p rules to admin/<domain> resource naming",
|
||||
zap.Int64("legacy_count", legacyCount),
|
||||
zap.Int64("total_p_count", count),
|
||||
)
|
||||
var legacyRules []model.CasbinRule
|
||||
if err := db.Where("ptype = ?", "p").Find(&legacyRules).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range legacyRules {
|
||||
zap.L().Debug("legacy casbin p rule will be removed",
|
||||
zap.String("v0", r.V0),
|
||||
zap.String("v1", r.V1),
|
||||
zap.String("v2", r.V2),
|
||||
)
|
||||
}
|
||||
if err := db.Where("ptype = ?", "p").Delete(&model.CasbinRule{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 注意:count > 0 分支到此已清空所有 p 策略,下面统一重新 seed。
|
||||
}
|
||||
|
||||
// 预定义权限策略数据
|
||||
// p = sub, obj, act (角色, 资源, 操作)
|
||||
//
|
||||
// 资源命名约定:admin/<domain>[/<sub>](路径式),动作:read / write / *。
|
||||
// super_admin 通过 admin/** 通配获得所有管理能力(globMatch 中 ** 跨 '/');
|
||||
// admin 仅获得业务管理能力,角色与权限管理(admin/roles/*、admin/users/roles/*)
|
||||
// 与日志导出(admin/logs/export)仅 super_admin 拥有(admin/logs 单层匹配不跨 /)。
|
||||
permissions := []model.CasbinRule{
|
||||
// 超级管理员 - 拥有所有权限
|
||||
{Ptype: "p", V0: model.RoleSuperAdmin, V1: "/*", V2: "*"},
|
||||
// 超级管理员 - 通配所有 admin 资源(globMatch:** 跨 '/' 分隔的多级资源)
|
||||
{Ptype: "p", V0: model.RoleSuperAdmin, V1: "admin/**", V2: "*"},
|
||||
|
||||
// 管理员 - 拥有管理权限
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "/api/v1/admin/*", V2: "*"},
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "/api/v1/users/*", V2: "GET"},
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "/api/v1/posts/*", V2: "*"},
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "/api/v1/comments/*", V2: "*"},
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "/api/v1/groups/*", V2: "*"},
|
||||
// 管理员 - 业务管理能力(不含角色/权限管理与日志导出)
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "admin/users", V2: "read"},
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "admin/users/status", V2: "write"},
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "admin/users/devices", V2: "read"},
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "admin/posts", V2: "*"},
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "admin/comments", V2: "*"},
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "admin/groups", V2: "*"},
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "admin/channels", V2: "*"},
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "admin/dashboard", V2: "read"},
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "admin/reports", V2: "*"},
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "admin/verifications", V2: "*"},
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "admin/profile_audits", V2: "*"},
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "admin/materials", V2: "*"},
|
||||
{Ptype: "p", V0: model.RoleAdmin, V1: "admin/logs", V2: "read"},
|
||||
|
||||
// 版主 - 拥有内容审核权限
|
||||
{Ptype: "p", V0: model.RoleModerator, V1: "/api/v1/posts/*", V2: "GET"},
|
||||
{Ptype: "p", V0: model.RoleModerator, V1: "/api/v1/posts/*", V2: "DELETE"},
|
||||
{Ptype: "p", V0: model.RoleModerator, V1: "/api/v1/comments/*", V2: "GET"},
|
||||
{Ptype: "p", V0: model.RoleModerator, V1: "/api/v1/comments/*", V2: "DELETE"},
|
||||
{Ptype: "p", V0: model.RoleModerator, V1: "/api/v1/groups/*", V2: "GET"},
|
||||
// 版主 - 内容审核
|
||||
{Ptype: "p", V0: model.RoleModerator, V1: "admin/posts", V2: "read"},
|
||||
{Ptype: "p", V0: model.RoleModerator, V1: "admin/posts", V2: "write"},
|
||||
{Ptype: "p", V0: model.RoleModerator, V1: "admin/comments", V2: "read"},
|
||||
{Ptype: "p", V0: model.RoleModerator, V1: "admin/comments", V2: "write"},
|
||||
{Ptype: "p", V0: model.RoleModerator, V1: "admin/reports", V2: "read"},
|
||||
{Ptype: "p", V0: model.RoleModerator, V1: "admin/reports", V2: "write"},
|
||||
|
||||
// 普通用户 - 拥有基本权限
|
||||
{Ptype: "p", V0: model.RoleUser, V1: "/api/v1/posts", V2: "GET"},
|
||||
{Ptype: "p", V0: model.RoleUser, V1: "/api/v1/posts/*", V2: "GET"},
|
||||
{Ptype: "p", V0: model.RoleUser, V1: "/api/v1/posts", V2: "POST"},
|
||||
{Ptype: "p", V0: model.RoleUser, V1: "/api/v1/comments/*", V2: "GET"},
|
||||
{Ptype: "p", V0: model.RoleUser, V1: "/api/v1/comments", V2: "POST"},
|
||||
{Ptype: "p", V0: model.RoleUser, V1: "/api/v1/users/me", V2: "GET"},
|
||||
{Ptype: "p", V0: model.RoleUser, V1: "/api/v1/users/me", V2: "PUT"},
|
||||
{Ptype: "p", V0: model.RoleUser, V1: "/api/v1/groups", V2: "GET"},
|
||||
{Ptype: "p", V0: model.RoleUser, V1: "/api/v1/groups/*", V2: "GET"},
|
||||
|
||||
// 被封禁用户 - 无权限
|
||||
// 普通用户与被封禁用户:admin 路由外的资源由路由层中间件保障,Casbin 不维护。
|
||||
}
|
||||
|
||||
// 批量插入权限策略
|
||||
|
||||
Reference in New Issue
Block a user