跳到主要内容

CmsKit 权限模型详解

本文档详细说明CmsKit中的多层权限控制机制,包括内容可见性、操作权限、角色权限等


📊 权限架构概览

CmsKit采用多层权限模型,共4层权限叠加:

┌─────────────────────────────────────────┐
│ 第1层: 全局权限 (GlobalPermission) │ ← 用户账户级别
│ 示例: 是否被封禁、是否可发布内容 │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│ 第2层: 内容状态权限 (AuditStatus) │ ← 内容审核级别
│ 示例: 待审核/已通过/已拒绝/拉黑 │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│ 第3层: 隐私权限 (PrivacyType) │ ← 内容创建者设置
│ 示例: 公开/仅自己可见 │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│ 第4层: 操作权限 (CommentType等) │ ← 特定操作权限
│ 示例: 谁可以评论、谁可以点赞 │
└─────────────────────────────────────────┘

🔐 第1层: 全局权限 (Global Permission)

账户状态

状态权限说明
正常可发布、互动、浏览
禁言不可发表新内容,可浏览
禁浏览不可浏览任何内容
已封禁所有操作都不可用

权限检查实现

public class GlobalPermissionValidator
{
public async Task<bool> CanCreateContentAsync(Guid userId)
{
var user = await _userRepository.GetAsync(userId);

// 检查全局权限
if (user.IsBlocked)
throw new ForbiddenException("账户已被封禁");

if (user.IsMuted)
throw new ForbiddenException("账户已被禁言");

return true;
}
}

🏷️ 第2层: 内容状态权限 (AuditStatus)

枚举值

名称说明可见性可编辑可删除
0Draft草稿仅作者
1Pending待审核仅作者/管理员
2Approved已通过所有(受PrivacyType影响)✅*
3Rejected已拒绝仅作者/管理员
4Black拉黑不可见✅**

*: 管理员可编辑,作者需重新审核
**: 仅管理员可删除

可见性矩阵

观看者角色 Draft Pending Approved Rejected Black
─────────────────────────────────────────────────────────
内容作者 ✅ ✅ ✅ ✅ ✅
审核管理员 ❌ ✅ ✅ ✅ ✅
普通用户 ❌ ❌ ✅ ❌ ❌
管理员(全局) ✅ ✅ ✅ ✅ ✅

权限检查流程

public async Task<Article> GetArticleAsync(Guid articleId, Guid? userId)
{
var article = await _articleRepository.GetAsync(articleId);

// 流程1: 检查是否被软删除
if (article.IsDeleted)
throw new NotFoundException("文章不存在");

// 流程2: 根据AuditStatus检查可见性
switch (article.AuditStatus)
{
case AuditStatus.Approved:
// 流程3: 检查PrivacyType
if (article.PrivacyType == PrivacyType.VisibleOnlyMySelf)
{
if (userId != article.CreateUserId)
throw new ForbiddenException("无权限访问");
}
break;

case AuditStatus.Pending:
// 待审核: 仅作者和管理员可见
if (userId != article.CreateUserId && !await IsReviewerAsync(userId))
throw new ForbiddenException("内容待审核");
break;

case AuditStatus.Rejected:
case AuditStatus.Black:
// 已拒绝/拉黑: 仅作者和管理员可见
if (userId != article.CreateUserId && !await IsAdminAsync(userId))
throw new ForbiddenException("内容不可见");
break;

case AuditStatus.Draft:
// 草稿: 仅作者可见
if (userId != article.CreateUserId)
throw new ForbiddenException("无权限访问");
break;
}

return article;
}

🔓 第3层: 隐私权限 (PrivacyType)

枚举值

名称说明可见人群
0Public公开所有用户
1VisibleOnlyMySelf仅自己可见仅作者本人

隐私规则

规则1: 已发布内容的隐私

Article (AuditStatus = Approved)
├─ PrivacyType = Public
│ └─ 任何用户都可见(在热排/推荐/搜索中可见)
└─ PrivacyType = VisibleOnlyMySelf
└─ 仅作者本人可见(不在热排/推荐/搜索中)

规则2: 书签集的隐私

Bookmark
├─ PrivacyType = Public
│ └─ 任何用户都可查看书签集及其项目
└─ PrivacyType = VisibleOnlyMySelf
└─ 仅所有者可查看

隐私权限检查

public async Task<IEnumerable<Article>> QueryPublicArticlesAsync()
{
return await _articleRepository
.Where(a => a.AuditStatus == AuditStatus.Approved
&& a.PrivacyType == PrivacyType.Public
&& !a.IsDeleted)
.OrderByDescending(a => a.HotIndex)
.ToListAsync();
}

public async Task<Article> GetPrivateArticleAsync(Guid articleId, Guid userId)
{
var article = await _articleRepository.GetAsync(articleId);

// 私密文章检查
if (article.PrivacyType == PrivacyType.VisibleOnlyMySelf)
{
if (article.CreateUserId != userId)
throw new ForbiddenException("无权限访问私密内容");
}

return article;
}

💬 第4层: 操作权限 (CommentType等)

CommentType - 评论权限

名称说明谁可以评论
10Public公开评论任何已登录用户
20FansOnly仅粉丝关注者
30FollowedMoreThan3Days3天以上粉丝关注超过3天的用户
40FilteredComments显示筛选评论所有人可发,但管理员控制显示
50Disabled禁止评论任何人都不可评论

CommentType 权限矩阵

评论权限设置 陌生用户 粉丝(1天) 粉丝(4天) 管理员
────────────────────────────────────────────────
Public ✅ ✅ ✅ ✅
FansOnly ❌ ✅ ✅ ✅
Followed3Days ❌ ❌ ✅ ✅
Disabled ❌ ❌ ❌ ✅*

*: 管理员可查看被禁评论,但普通用户不可评

评论权限检查实现

public class CommentPermissionValidator
{
public async Task ValidateCanCommentAsync(Guid articleId, Guid? userId)
{
var article = await _articleRepository.GetAsync(articleId);

// 检查评论权限
switch (article.CommentType)
{
case CommentType.Disabled:
throw new ForbiddenException("该内容已禁止评论");

case CommentType.FansOnly:
if (!await IsFollowerAsync(article.CreateUserId, userId))
throw new ForbiddenException("仅粉丝可评论");
break;

case CommentType.FollowedMoreThan3Days:
if (!await IsFollowerAsync(article.CreateUserId, userId))
throw new ForbiddenException("仅粉丝可评论");

var followTime = await GetFollowTimeAsync(article.CreateUserId, userId);
if (DateTime.UtcNow - followTime < TimeSpan.FromDays(3))
throw new ForbiddenException("需要关注3天以上");
break;

case CommentType.Public:
default:
// 任何人都可评论
break;
}
}
}

其他操作权限

操作权限类型说明
点赞无限制任何已登录用户都可点赞
书签无限制任何已登录用户都可书签
删除评论作者/管理员评论作者或管理员可删除
编辑内容作者/管理员编辑后需重新审核
设置为精选管理员设置 EditorIndex

🎭 第5层: 角色权限 (Role-Based)

俱乐部角色权限

角色权限集合说明
Owner (圈主)所有权限
  • ✅ 管理成员角色
  • ✅ 删除成员
  • ✅ 删除内容
  • ✅ 设置圈子信息
  • ✅ 转移圈主
Admin (管理员)管理权限
  • ✅ 删除违规内容
  • ✅ 管理评论
  • ✅ 设置精选
  • ❌ 管理角色
  • ❌ 删除成员
Guest (嘉宾)特殊权限
  • ✅ 内容展示加亮
  • ✅ 优先推荐
  • ❌ 管理权限
Member (成员)基础权限
  • ✅ 发布内容
  • ✅ 参与评论
  • ❌ 删除他人内容

管理员(全局)权限

权限项说明
审核内容审批/拒绝待审核的文章、沸点、评论
拉黑内容将内容标记为拉黑,对所有用户隐藏
删除内容永久删除任何内容
管理用户禁言、封禁、解禁用户
处理举报审核用户举报
管理权限修改用户或圈子的权限配置

🔀 权限决策树

场景1: 查看文章

用户请求查看文章
├─ [否] 文章已被软删除? → 错误: 不存在
├─ [是] 用户是管理员? → 允许查看

└─ [否] 检查AuditStatus:
├─ Draft: [否] 是作者? → 拒绝: 无权限
├─ Pending: [否] 是审核员? → 拒绝: 待审核
├─ Rejected: [否] 是作者或管理员? → 拒绝: 内容被拒绝
├─ Black: → 拒绝: 内容被拉黑
└─ Approved:
└─ [否] PrivacyType=VisibleOnlyMySelf && 不是作者?
→ 拒绝: 私密内容
└─ 允许查看

场景2: 发表评论

用户请求发表评论
├─ [否] 已登录? → 拒绝: 需登录
├─ [否] 账户正常(未禁言/封禁)? → 拒绝: 账户异常
├─ [否] 可查看该文章? → 拒绝: 无权限

└─ 检查CommentType:
├─ Disabled → 拒绝: 禁止评论
├─ FansOnly:
│ └─ [否] 是粉丝? → 拒绝: 仅粉丝可评论
├─ FollowedMoreThan3Days:
│ ├─ [否] 是粉丝? → 拒绝: 仅粉丝可评论
│ └─ [否] 关注超过3天? → 拒绝: 需关注3天以上
└─ Public → 允许评论

场景3: 编辑内容

用户请求编辑内容
├─ [否] 是作者或管理员? → 拒绝: 无权限
├─ [是管理员] 允许编辑,内容状态不变
└─ [是作者]:
├─ [否] AuditStatus=Draft或Approved? → 拒绝: 状态不支持编辑
└─ 允许编辑
└─ 若已发布(Approved) → 编辑后状态变为Pending(待重新审核)

🛡️ 权限检查中间件

全局权限拦截器

public class PermissionCheckMiddleware
{
public async Task InvokeAsync(HttpContext context)
{
var endpoint = context.GetEndpoint();
var requireAuth = endpoint?.Metadata.GetMetadata<RequireAuthAttribute>();

if (requireAuth != null)
{
var userId = context.User.FindFirst("sub")?.Value;

if (string.IsNullOrEmpty(userId))
{
context.Response.StatusCode = 401;
return;
}

// 检查全局权限
var user = await _userService.GetAsync(Guid.Parse(userId));
if (user.IsBlocked)
{
context.Response.StatusCode = 403;
await context.Response.WriteAsync("账户已被封禁");
return;
}
}

await _next(context);
}
}

基于属性的权限检查

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class RequirePermissionAttribute : Attribute
{
public string Permission { get; set; }

public RequirePermissionAttribute(string permission)
{
Permission = permission;
}
}

public class PermissionCheckFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
var requirePerm = context.ActionDescriptor
.EndpointMetadata
.OfType<RequirePermissionAttribute>()
.FirstOrDefault();

if (requirePerm != null)
{
var userId = context.HttpContext.User.FindFirst("sub")?.Value;
var hasPermission = await _permissionService
.HasPermissionAsync(Guid.Parse(userId), requirePerm.Permission);

if (!hasPermission)
{
context.Result = new ForbiddenResult();
return;
}
}

await next();
}
}

📋 权限查询 SQL

查询用户的权限清单

-- 用户全局权限
SELECT
Id,
UserName,
IsBlocked,
IsMuted,
CASE
WHEN IsBlocked = 1 THEN '已封禁'
WHEN IsMuted = 1 THEN '已禁言'
ELSE '正常'
END AS GlobalStatus
FROM cms_user
WHERE Id = @UserId

-- 用户俱乐部角色
SELECT
cmr.Id,
c.Name AS ClubName,
cmr.Role,
CASE
WHEN cmr.Role = 1 THEN '圈主'
WHEN cmr.Role = 2 THEN '管理员'
WHEN cmr.Role = 3 THEN '嘉宾'
ELSE '成员'
END AS RoleName,
cmr.ExpireTime
FROM cms_club_member_role cmr
JOIN cms_club c ON cmr.ClubId = c.Id
WHERE cmr.UserId = @UserId AND cmr.IsEnabled = 1

查询内容的权限配置

SELECT
Id,
Title,
AuditStatus,
PrivacyType,
CommentType,
CASE
WHEN AuditStatus = 0 THEN '草稿'
WHEN AuditStatus = 1 THEN '待审核'
WHEN AuditStatus = 2 THEN '已通过'
WHEN AuditStatus = 3 THEN '已拒绝'
WHEN AuditStatus = 4 THEN '已拉黑'
END AS AuditStatusName,
CASE
WHEN PrivacyType = 0 THEN '公开'
WHEN PrivacyType = 1 THEN '仅自己'
END AS PrivacyTypeName,
CASE
WHEN CommentType = 10 THEN '任何人'
WHEN CommentType = 20 THEN '仅粉丝'
WHEN CommentType = 30 THEN '3天以上粉丝'
WHEN CommentType = 40 THEN '筛选评论'
WHEN CommentType = 50 THEN '禁止评论'
END AS CommentTypeName
FROM cms_article
WHERE Id = @ArticleId

🎯 最佳实践

原则1: 白名单机制

// ✅ 推荐: 显式允许
if (allowedRoles.Contains(userRole))
{
// 执行操作
}

// ❌ 不推荐: 显式拒绝
if (forbiddenRoles.Contains(userRole))
{
throw new Exception();
}

原则2: 最少权限原则

// ✅ 用户默认无权限,只在必要时赋予
// ❌ 用户默认有权限,需要时撤销

原则3: 缓存权限决策

// ✅ 缓存权限检查结果
var cacheKey = $"permission:{userId}:{permission}";
var hasPermission = await _cache.GetAsync<bool>(cacheKey)
?? await _permissionService.CheckAsync(userId, permission);
await _cache.SetAsync(cacheKey, hasPermission, TimeSpan.FromHours(1));

// ❌ 每次都检查数据库

🔄 权限变更日志

所有权限变更应记录在 AuditLog 表中:

public async Task UpdateUserPermissionAsync(Guid userId, string permission, bool grant)
{
// 执行权限变更

// 记录审计日志
await _auditLogService.LogAsync(new AuditLog
{
UserId = CurrentUserId,
EntityType = "User",
EntityId = userId,
Action = grant ? "GrantPermission" : "RevokePermission",
OldValue = "无",
NewValue = permission,
ChangeTime = DateTime.UtcNow,
IpAddress = HttpContext.Connection.RemoteIpAddress.ToString()
});
}

📚 相关文档


最后更新: 2026-05-10