Day 1 - 角色与权限
建议用时:180-210 分钟
你将学会什么
- 认证和授权有什么区别
- 角色和权限有什么区别
- 为什么隐藏按钮不等于权限控制
- 怎么定义稳定的权限名
- 怎么在服务层检查权限
- 没有权限时怎么返回失败结果
本页重点是:权限判断必须放在真正执行动作之前。界面可以根据权限隐藏按钮,但服务层仍然要再次检查。否则只要绕过界面,关键动作仍然可能被执行。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“复盘与巩固”,今天继续学习“角色与权限”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“角色与权限”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。权限的核心不是“谁能看到按钮”,而是“谁能真正执行动作”。
认证和授权的区别
| 名词 | 解决的问题 | 例子 |
|---|---|---|
| 认证 | 你是谁 | Alice 已登录 |
| 授权 | 你能做什么 | Alice 能不能删除商品 |
登录成功只说明系统知道你是谁。
能不能删除商品,还要看权限。
角色和权限的区别
角色是一组权限的集合。
权限是具体动作。
例如:
| 角色 | 权限 |
|---|---|
| Viewer | Product.Read |
| Editor | Product.Read、Product.Create、Product.Update |
| Admin | Product.Read、Product.Create、Product.Update、Product.Delete |
这样设计的好处是:
- 用户只需要分配角色。
- 权限可以统一维护。
- 新增权限时不需要改每个用户。
为什么权限名要稳定
权限名最好集中定义:
public static class PermissionNames
{
public const string ProductDelete = "Product.Delete";
}不要到处手写字符串:
"Product.Delete"原因是字符串写错不会自动报错,例如写成:
"Product.Delet"程序还能编译,但权限判断会失败。
隐藏按钮不等于权限控制
界面可以这样做:
CanDelete = permissionService.HasPermission(user, PermissionNames.ProductDelete);然后根据 CanDelete 显示或隐藏删除按钮。
但这只是界面体验。
真正保护动作的是服务层:
if (!permissionService.HasPermission(user, PermissionNames.ProductDelete))
{
return OperationResult.Fail("没有删除权限。");
}因为按钮隐藏了,不代表删除方法不会被调用。
为什么权限检查放在服务层
删除商品这个动作属于业务动作。
服务层负责业务规则,所以删除前必须在服务层检查权限。
数据层只负责删除数据,不应该判断谁能删。
界面层只负责显示按钮,不应该成为唯一防线。
权限失败应该怎么处理
权限失败是可预期的业务失败。
本页不抛异常,而是返回:
OperationResult.Fail("Alice 没有删除权限。")这样界面可以显示提示,程序也能继续运行。
真正意外的问题,例如文件损坏、数据库连接失败,才适合异常和日志。
角色与权限常用写法速查
| 需求 | 写法 | 说明 |
|---|---|---|
| 定义角色 | enum UserRole | Viewer、Editor、Admin |
| 定义权限名 | static class PermissionNames | 避免魔法字符串 |
| 判断角色 | user.Role == UserRole.Admin | 简单规则 |
| 判断权限 | Can(user, permission) | 集中权限逻辑 |
| 权限集合 | HashSet<string> | 判断是否包含权限 |
| 拒绝操作 | 返回失败结果 | 不继续执行业务 |
权限判断流程:
先确认当前用户
-> 再确认要做的动作
-> 查角色是否拥有权限
-> 没权限直接返回失败
-> 有权限才执行业务第二部分:把知识组合成完整例子
今天做一个角色权限控制程序。
它会验证:
Viewer只能查看,不能删除。Editor可以新增和编辑,但不能删除。Admin可以删除。- 删除商品时,服务层会检查
Product.Delete权限。 - 没有权限时返回失败,不执行删除。
完整 Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
var repository = new ProductRepository();
var permissionService = new PermissionService();
var productService = new ProductService(repository, permissionService);
var viewer = new User("Alice", Role.Viewer);
var editor = new User("Bob", Role.Editor);
var admin = new User("Cindy", Role.Admin);
void Print(OperationResult result)
{
Console.WriteLine($"{result.Success} - {result.Message}");
}
Print(productService.DeleteProduct(viewer, 1));
Print(productService.DeleteProduct(editor, 1));
Print(productService.DeleteProduct(admin, 1));
Print(productService.DeleteProduct(admin, 99));
Console.WriteLine($"剩余商品数量:{repository.Count}");
public enum Role
{
Viewer,
Editor,
Admin
}
public sealed record User(string Name, Role Role);
public static class PermissionNames
{
public const string ProductRead = "Product.Read";
public const string ProductCreate = "Product.Create";
public const string ProductUpdate = "Product.Update";
public const string ProductDelete = "Product.Delete";
}
public sealed class PermissionService
{
private static readonly Dictionary<Role, HashSet<string>> RolePermissions = new()
{
[Role.Viewer] = new HashSet<string>
{
PermissionNames.ProductRead
},
[Role.Editor] = new HashSet<string>
{
PermissionNames.ProductRead,
PermissionNames.ProductCreate,
PermissionNames.ProductUpdate
},
[Role.Admin] = new HashSet<string>
{
PermissionNames.ProductRead,
PermissionNames.ProductCreate,
PermissionNames.ProductUpdate,
PermissionNames.ProductDelete
}
};
public bool HasPermission(User user, string permission)
{
return RolePermissions.TryGetValue(user.Role, out HashSet<string>? permissions)
&& permissions.Contains(permission);
}
}
public sealed class ProductService
{
private readonly ProductRepository repository;
private readonly PermissionService permissionService;
public ProductService(ProductRepository repository, PermissionService permissionService)
{
this.repository = repository;
this.permissionService = permissionService;
}
public OperationResult DeleteProduct(User user, int productId)
{
if (!permissionService.HasPermission(user, PermissionNames.ProductDelete))
{
return OperationResult.Fail($"{user.Name} 没有删除权限。");
}
if (!repository.Exists(productId))
{
return OperationResult.Fail($"商品 {productId} 不存在。");
}
repository.Delete(productId);
return OperationResult.Ok($"{user.Name} 删除商品 {productId} 成功。");
}
}
public sealed class ProductRepository
{
private readonly List<Product> products = new()
{
new Product(1, "Keyboard"),
new Product(2, "Mouse")
};
public int Count => products.Count;
public bool Exists(int id)
{
return products.Any(product => product.Id == id);
}
public void Delete(int id)
{
products.RemoveAll(product => product.Id == id);
}
}
public sealed record Product(int Id, string Name);
public sealed class OperationResult
{
public bool Success { get; init; }
public string Message { get; init; } = "";
public static OperationResult Ok(string message)
{
return new OperationResult
{
Success = true,
Message = message
};
}
public static OperationResult Fail(string message)
{
return new OperationResult
{
Success = false,
Message = message
};
}
}正常运行结果
False - Alice 没有删除权限。
False - Bob 没有删除权限。
True - Cindy 删除商品 1 成功。
False - 商品 99 不存在。
剩余商品数量:1这段程序说明:权限检查不是界面装饰,而是服务层执行动作前的必要判断。
第三部分:跟着敲代码
从这里开始动手。先用控制台项目验证权限规则。
第 1 步:创建控制台项目
dotnet new console -n RolePermissionDemo
cd RolePermissionDemo打开 Program.cs,清空原内容。
第 2 步:定义角色
public enum Role
{
Viewer,
Editor,
Admin
}解释:
Viewer只查看。Editor能新增和编辑。Admin能执行全部动作。
第 3 步:定义用户
public sealed record User(string Name, Role Role);解释:
- 用户有名字和角色。
- 这里先不做登录,只模拟当前用户。
第 4 步:定义权限名
public static class PermissionNames
{
public const string ProductRead = "Product.Read";
public const string ProductCreate = "Product.Create";
public const string ProductUpdate = "Product.Update";
public const string ProductDelete = "Product.Delete";
}解释:
- 权限名集中管理。
- 后面检查权限时不要散落字符串。
第 5 步:定义角色权限表
private static readonly Dictionary<Role, HashSet<string>> RolePermissions = new()
{
[Role.Viewer] = new HashSet<string>
{
PermissionNames.ProductRead
},
[Role.Editor] = new HashSet<string>
{
PermissionNames.ProductRead,
PermissionNames.ProductCreate,
PermissionNames.ProductUpdate
},
[Role.Admin] = new HashSet<string>
{
PermissionNames.ProductRead,
PermissionNames.ProductCreate,
PermissionNames.ProductUpdate,
PermissionNames.ProductDelete
}
};解释:
- 每个角色对应一组权限。
HashSet适合判断是否包含某个权限。
第 6 步:写权限检查方法
public bool HasPermission(User user, string permission)
{
return RolePermissions.TryGetValue(user.Role, out HashSet<string>? permissions)
&& permissions.Contains(permission);
}解释:
- 先找到用户角色对应的权限集合。
- 再判断集合里有没有目标权限。
第 7 步:写 ProductRepository
public sealed class ProductRepository
{
private readonly List<Product> products = new()
{
new Product(1, "Keyboard"),
new Product(2, "Mouse")
};
public int Count => products.Count;
public bool Exists(int id)
{
return products.Any(product => product.Id == id);
}
public void Delete(int id)
{
products.RemoveAll(product => product.Id == id);
}
}解释:
- Repository 只管数据。
- 它不知道谁有权限。
第 8 步:写 ProductService 删除方法
public OperationResult DeleteProduct(User user, int productId)
{
if (!permissionService.HasPermission(user, PermissionNames.ProductDelete))
{
return OperationResult.Fail($"{user.Name} 没有删除权限。");
}
if (!repository.Exists(productId))
{
return OperationResult.Fail($"商品 {productId} 不存在。");
}
repository.Delete(productId);
return OperationResult.Ok($"{user.Name} 删除商品 {productId} 成功。");
}解释:
- 先检查权限。
- 再检查商品是否存在。
- 最后才真正删除。
第 9 步:写验证流程
var viewer = new User("Alice", Role.Viewer);
var editor = new User("Bob", Role.Editor);
var admin = new User("Cindy", Role.Admin);
Print(productService.DeleteProduct(viewer, 1));
Print(productService.DeleteProduct(editor, 1));
Print(productService.DeleteProduct(admin, 1));
Print(productService.DeleteProduct(admin, 99));解释:
- Viewer 删除失败。
- Editor 删除失败。
- Admin 删除成功。
- Admin 删除不存在商品失败。
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
| 只在按钮上隐藏功能 | 只是界面看不到,逻辑仍可能被调用 | 服务层也要检查权限 |
| 角色名到处写字符串 | 拼错后很难发现 | 用常量或枚举集中管理 |
| 权限判断分散在每个页面 | 后期难维护 | 写 PermissionService 统一判断 |
| 没有默认低权限角色 | 新账号可能权限过大 | 默认使用普通角色,只给必要权限 |
| 提示只写“失败” | 不知道是权限问题还是系统错误 | 权限不足时明确提示“没有权限执行此操作” |
小白重复敲写训练
权限先练纯 C# 判断,再绑定到按钮。
训练 1:按角色判断
bool CanDelete(string role)
{
return role == "Admin";
}
Console.WriteLine(CanDelete("Admin"));
Console.WriteLine(CanDelete("Viewer"));训练 2:使用枚举角色
enum UserRole { Viewer, Editor, Admin }
bool CanEdit(UserRole role)
{
return role is UserRole.Editor or UserRole.Admin;
}遍历三个角色并输出结果。
训练 3:绑定按钮权限
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanDelete))]
private UserRole _currentRole;
public bool CanDelete => CurrentRole == UserRole.Admin;<Button Content="删除" IsEnabled="{Binding CanDelete}" />第三遍切换角色,确认按钮状态变化。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:角色与权限。今天至少要掌握的 3 个点是:
1. 认证和授权有什么区别
2. 角色和权限有什么区别
3. 为什么隐藏按钮不等于权限控制答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:认证和授权有什么区别。
上位机专项练习
操作员、工程师和管理员权限不同。权限控制要放在命令和服务层,不能只把按钮藏起来。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:定义角色
public enum UserRole
{
Operator,
Engineer,
Administrator
}运行结果或界面效果:
三种角色有明确名称改动任务: 写出每种角色可执行的操作。
专项例子 2:检查修改权限
public bool CanEditConfiguration(User user)
{
return user.Role is UserRole.Engineer or UserRole.Administrator;
}运行结果或界面效果:
操作员不能修改配置改动任务: 增加只有管理员能管理用户的规则。
专项例子 3:界面绑定权限
<Button Content="修改设备配置"
IsVisible="{Binding CanEditConfiguration}"
Command="{Binding EditConfigurationCommand}" />运行结果或界面效果:
无权限时入口不可见改动任务: 命令执行处也再次检查权限。
第四部分:作业完整答案
作业要求:写一个角色权限控制程序,包含 Viewer、Editor、Admin,删除商品前必须检查 Product.Delete 权限。
答案文件:Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
var repository = new ProductRepository();
var permissionService = new PermissionService();
var productService = new ProductService(repository, permissionService);
var viewer = new User("Alice", Role.Viewer);
var editor = new User("Bob", Role.Editor);
var admin = new User("Cindy", Role.Admin);
void Print(OperationResult result)
{
Console.WriteLine($"{result.Success} - {result.Message}");
}
Print(productService.DeleteProduct(viewer, 1));
Print(productService.DeleteProduct(editor, 1));
Print(productService.DeleteProduct(admin, 1));
Print(productService.DeleteProduct(admin, 99));
Console.WriteLine($"剩余商品数量:{repository.Count}");
public enum Role
{
Viewer,
Editor,
Admin
}
public sealed record User(string Name, Role Role);
public static class PermissionNames
{
public const string ProductRead = "Product.Read";
public const string ProductCreate = "Product.Create";
public const string ProductUpdate = "Product.Update";
public const string ProductDelete = "Product.Delete";
}
public sealed class PermissionService
{
private static readonly Dictionary<Role, HashSet<string>> RolePermissions = new()
{
[Role.Viewer] = new HashSet<string>
{
PermissionNames.ProductRead
},
[Role.Editor] = new HashSet<string>
{
PermissionNames.ProductRead,
PermissionNames.ProductCreate,
PermissionNames.ProductUpdate
},
[Role.Admin] = new HashSet<string>
{
PermissionNames.ProductRead,
PermissionNames.ProductCreate,
PermissionNames.ProductUpdate,
PermissionNames.ProductDelete
}
};
public bool HasPermission(User user, string permission)
{
return RolePermissions.TryGetValue(user.Role, out HashSet<string>? permissions)
&& permissions.Contains(permission);
}
}
public sealed class ProductService
{
private readonly ProductRepository repository;
private readonly PermissionService permissionService;
public ProductService(ProductRepository repository, PermissionService permissionService)
{
this.repository = repository;
this.permissionService = permissionService;
}
public OperationResult DeleteProduct(User user, int productId)
{
if (!permissionService.HasPermission(user, PermissionNames.ProductDelete))
{
return OperationResult.Fail($"{user.Name} 没有删除权限。");
}
if (!repository.Exists(productId))
{
return OperationResult.Fail($"商品 {productId} 不存在。");
}
repository.Delete(productId);
return OperationResult.Ok($"{user.Name} 删除商品 {productId} 成功。");
}
}
public sealed class ProductRepository
{
private readonly List<Product> products = new()
{
new Product(1, "Keyboard"),
new Product(2, "Mouse")
};
public int Count => products.Count;
public bool Exists(int id)
{
return products.Any(product => product.Id == id);
}
public void Delete(int id)
{
products.RemoveAll(product => product.Id == id);
}
}
public sealed record Product(int Id, string Name);
public sealed class OperationResult
{
public bool Success { get; init; }
public string Message { get; init; } = "";
public static OperationResult Ok(string message)
{
return new OperationResult
{
Success = true,
Message = message
};
}
public static OperationResult Fail(string message)
{
return new OperationResult
{
Success = false,
Message = message
};
}
}验收结果
运行后必须看到:
Viewer删除失败。Editor删除失败。Admin删除成功。- 删除不存在商品失败。
- 最后剩余商品数量是 1。
为什么这个答案是对的
这个答案把权限控制拆成了清楚的层:
| 层 | 代码 | 作用 |
|---|---|---|
| 用户 | User | 表示当前操作的人 |
| 角色 | Role | 表示用户属于哪一类 |
| 权限名 | PermissionNames | 稳定定义动作名称 |
| 权限表 | PermissionService | 判断角色是否拥有权限 |
| 业务动作 | ProductService.DeleteProduct | 删除前再次检查权限 |
| 数据层 | ProductRepository | 真正删除数据 |
权限控制的关键是:界面可以隐藏按钮,但服务层必须检查权限;Repository 只执行数据操作,不决定谁能操作。