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)
枚举值
| 值 | 名称 | 说明 | 可见性 | 可编辑 | 可删除 |
|---|---|---|---|---|---|
| 0 | Draft | 草稿 | 仅作者 | ✅ | ✅ |
| 1 | Pending | 待审核 | 仅作者/管理员 | ❌ | ❌ |
| 2 | Approved | 已通过 | 所有(受PrivacyType影响) | ✅* | ✅ |
| 3 | Rejected | 已拒绝 | 仅作者/管理员 | ✅ | ✅ |
| 4 | Black | 拉黑 | 不可见 | ❌ | ✅** |
*: 管理员可编辑,作者需重新审核
**: 仅管理员可删除
可见性矩阵
观看者角色 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)
枚举值
| 值 | 名称 | 说明 | 可见人群 |
|---|---|---|---|
| 0 | Public | 公开 | 所有用户 |
| 1 | VisibleOnlyMySelf | 仅自己可见 | 仅作者本人 |
隐私规则
规则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 - 评论权限
| 值 | 名称 | 说明 | 谁可以评论 |
|---|---|---|---|
| 10 | Public | 公开评论 | 任何已登录用户 |
| 20 | FansOnly | 仅粉丝 | 关注者 |
| 30 | FollowedMoreThan3Days | 3天以上粉丝 | 关注超过3天的用户 |
| 40 | FilteredComments | 显示筛选评论 | 所有人可发,但管理员控制显示 |
| 50 | Disabled | 禁止评论 | 任何人都不可评论 |
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()
});
}
📚 相关文档
- API参考
- 审计日志:参考本页"权限变更日志"与 SystemArchitecture
- 角色管理:参考 新功能使用文档
最后更新: 2026-05-10