这篇是安全专题的总览,把散落在各篇里的机制汇总成一张"攻击面 → 防护 → 证据"的对照表。目标不是制造紧张感,而是让每一类风险都能指到具体代码。
一、先明确威胁模型
后台系统面对的主要是三类人:
| 攻击者 | 手里有什么 | 想干什么 |
|---|---|---|
| 未登录的外部人员 | 只知道后台地址 | 撞库、爆破、探测接口 |
| 已登录的低权限用户 | 合法账号(比如普通员工) | 越权看/改不该碰的数据 |
| 内网中的其它系统 | 网络可达性 | 通过 SSRF 等入口横向移动 |
下面所有防护都对应这三类。
二、总览:攻击面 → 防护 → 证据
| 攻击面 | 防护机制 | 源码 | 测试 |
|---|---|---|---|
| 会话伪造 / 篡改 | Data Protection 加密登录票据 + 统一 purpose | AdminContext.Auth.cs、EasyAdminLoginTicketAuthenticationHandler.cs |
AuthenticationTests |
| 后台入口探测 | AdminRouteSecret(中间件 404 + 组件二次校验) |
AdminExtensions.cs、AdminContext.Auth.cs |
------ |
| 暴力破解 | 登录失败锁定 + 验证码 | SecurityOptions.LoginFailureLimit / LoginLockout、EnableLoginCaptcha |
------ |
| CSRF | Antiforgery 过滤器(写方法校验) | EasyAdminAntiforgeryFilter.cs |
CsrfTests |
| 路由越权 | AuthPath(路由 + 导航拦截) |
AdminContext.Authorization.cs |
------ |
| 操作越权 | AuthButton + AdminButtonAttribute |
AdminTable.razor、AdminButtonAttribute.cs |
AuthorizationTests |
| 数据越权(查询/改/删/导入) | ApplyDataPermission + FilterAuthorizedAsync |
FreeSqlExtensions.cs |
DataPermissionTests |
| 文件上传绕过 | 扩展名白/黑名单 + Magic Number + ImageSharp | FileSecurityValidator.cs、FileService.cs |
FileSecurityValidatorTests、FileUploadSecurityTests |
| 路径穿越 | SafePathService(拒绝 ../盘符/UNC) |
SafePathService.cs |
PathTraversalTests |
| SSRF | 协议白名单 + 逐跳校验 + 连接前锁定 IP | RemoteContentDownloader.cs、FileSecurityValidator.cs |
SsrfTests |
| 越权订阅推送 | Hub 不接受客户端 userId | NotificationHub.cs |
NotificationHubTests |
| 越权改消息状态 | 只取 Id + 当前用户条件 | AdminMessageService.cs |
MessageReadTests |
| 密码泄露 | PBKDF2 + Salt + 固定时间比较 | PBKDF2Encrypt.cs |
Pbkdf2UpgradeTests |
| 匿名接口滥用 | 限流 + 参数校验 + 不泄露异常 | WeChatAppController.cs 等 |
AuthorizationTests |
| 多租户串数据 | 独立库 + 文件目录 + 缓存/锁前缀 | MultiTenantService.cs、AdminContext.Tenant.cs |
TenantIsolationTests、TenantCacheIsolationTests |
| 异常信息泄露 | 通用提示 + TraceId | ApprovalService.GenericFailure |
ApprovalConcurrencyTests |
三、逐项展开
1. 会话:票据是加密的,且不能跨用途复用
登录成功后写的是加密票据,内容是 userId|loginTime:
csharp
var encryptedData = _loginTicketProtector.Protect($"{user.Id}|{user.LoginTime.ToString(LoginTimeFormat, CultureInfo.InvariantCulture)}");
_loginTicketProtector 来自 IDataProtectionProvider.CreateProtector("EasyAdminBlazor.LoginTicket.v1")。测试锁定了两条:
csharp
[Fact] public void LoginTicket_InvalidPurpose_CannotBeUnprotected() // 换个 purpose 解不开
[Fact] public void EphemeralDataProtector_RoundTripsLoginTicket()
还有一条跨文件一致性断言,防止以后有人改了 purpose 导致 Hub 认证失效:
csharp
[Theory]
[InlineData("EasyAdminBlazor/Hubs/NotificationHub.cs")]
[InlineData("Extensions/EasyAdminBlazor.Chat/ChatHub.cs")]
[InlineData("EasyAdminBlazor/AdminContext.cs")]
[InlineData("EasyAdminBlazor/Components/Layout/Widget.razor")]
public void LoginTicketProtectorPurpose_IsConsistent(string relativePath)
{
var source = ReadSource(relativePath);
source.Should().Contain("EasyAdminBlazor.LoginTicket.v1");
}
2. CSRF:只拦写方法,失败返回 400
csharp
[Theory]
[InlineData("GET")] [InlineData("HEAD")] [InlineData("OPTIONS")] [InlineData("TRACE")]
public async Task SafeHttpMethods_AreNotValidated(string method) // 安全方法放行
[Theory]
[InlineData("POST")] [InlineData("PUT")] [InlineData("PATCH")] [InlineData("DELETE")]
public async Task WriteHttpMethods_AreValidated(string method) // 写方法必须校验
[Fact]
public async Task PostWithoutToken_IsRejectedWith400() // 失败是 400,不是 500
三条约束都很关键:
- 只拦写方法:如果连 GET 都要求令牌,缓存和预取会出问题;
- 失败返回 400 而不是 500:令牌无效是客户端问题,不该记成服务端错误;
AntiforgeryDisabled时可整体关闭 (默认开启,SecurityOptions.AntiforgeryEnabled = true)。
3. 授权:四个层次缺一不可
这一块第 08、09、24 篇讲透了,这里只列结论:
text
路由级 AuthPath 能不能进页面
操作级 AuthButton 能不能点按钮
方法级 AdminButtonAttribute 方法能不能执行
数据级 FilterAuthorizedAsync 能操作哪些行
4. 文件与路径:两套独立的校验
内容防伪(第 23 篇):
csharp
if (FileSignatures.TryGetValue(extension, out var signatures))
{
return await HasAnyPrefixAsync(input, signatures, cancellationToken);
}
return false; // 未知扩展名 fail-closed
路径防穿越 (SafePathService):
csharp
private readonly SafePathService _service = SafePathService.Instance;
[Theory]
[InlineData("..")]
[InlineData("../../etc/passwd")]
[InlineData("a/../../b")]
[InlineData("..\\..\\windows\\system32")]
[InlineData("C:/windows/system32")]
[InlineData("//server/share/file")]
public void NormalizeRelative_TraversalAttempts_Throw(string input)
{
var act = () => _service.NormalizeRelative(input, allowRoot: false);
act.Should().Throw<UnauthorizedAccessException>();
}
还有一个容易漏掉的用例:
csharp
[Fact]
public void IsContained_SiblingDirectoryWithSamePrefix_ReturnsFalse()
{
var root = Path.Combine(Path.GetTempPath(), "eab-root");
// eab-root-evil 以 eab-root 为前缀,但不是子目录
var sibling = Path.Combine(Path.GetTempPath(), "eab-root-evil", "file.txt");
_service.IsContained(root, sibling).Should().BeFalse();
}
如果只用 StartsWith(root) 判断包含关系,eab-root-evil 就能绕过。 这是路径校验的经典坑。
5. SSRF:出站请求也要校验目标
第 11 篇讲透了七层防护。核心两条:
- 每跳重定向都重新校验(禁止自动重定向);
ConnectCallback在建立 TCP 连接前再次校验实际 IP,关闭 DNS Rebinding 的 TOCTOU 窗口。
6. 多租户:数据库隔离 ≠ 系统隔离
第 13、15 篇讲过:数据库、文件目录、缓存键、分布式锁都要带租户维度。测试里有一组专门的候选键清单:
csharp
"tenant:main:SysConfig:",
"tenant:main:UserRoles:",
"tenant:main:Dict:",
"tenant:main:chat_messages:",
"tenant:main:save_messages_lock",
7. 信息泄露:错误信息要"对外模糊、对内精确"
csharp
private string GenericFailure(Exception ex)
{
var traceId = Guid.NewGuid().ToString("N")[..16];
_logger.LogError(ex, "审批处理失败,TraceId:{TraceId}", traceId);
return LF("审批处理失败,请稍后重试。TraceId:{0}", traceId);
}
用户拿到的是通用提示 + TraceId;数据库异常、表名、SQL 片段只进日志。AuthorizationTests 里还有一条约束:控制器源码里不得出现 ex.Message。
DatabaseLogger 落库时也做了截断(消息 2000 / 异常 12000),并且写日志失败不会影响业务(第 27 篇)。
四、框架已做 vs 需要自己补
| 项目 | 框架已做 | 需要你补 |
|---|---|---|
| 密码哈希 | PBKDF2 + 平滑升级 | 密码强度策略、历史密码限制 |
| 登录防护 | 失败锁定、验证码 | 二次验证(2FA)、异地登录提醒 |
| 授权 | 路由/按钮/方法/数据四层 | 权限矩阵的正确配置与验收 |
| 文件 | 类型/内容/路径/大小 | 病毒扫描、CDR、独立域名下发 |
| SSRF | 出站请求完整防护 | 代理环境下的额外确认 |
| CSRF | 写方法令牌校验 | 自定义接口注册到 MVC 过滤器链 |
| 多租户 | 库/文件/缓存/锁隔离 | 跨租户统计的权限控制 |
| 审计 | 操作日志 + 登录日志 + 错误日志 | 日志保留期限、归档与合规要求 |
| 传输 | ------ | HTTPS、证书、HSTS |
| 运维 | ------ | 备份、WAF/限流、端口暴露面、最小权限账号 |
这张表的右列不是框架的缺陷,而是产品与运维决策。 比如"是否要求 2FA"取决于客户合规要求,框架不该替业务决定。
五、上线安全清单
结合 SecurityOptions 和部署文档(第 28 篇),上线前逐条确认:
| 项 | 默认值 | 上线动作 |
|---|---|---|
AdminRouteSecret |
i4ByMAX4 |
换成自定义值 |
AesKey |
示例值 | 换成长随机串 |
| 默认管理员密码 | 123yyq |
首次登录强制改密,确认流程可用 |
MaxUploadSize |
10MB | 按业务调整,不要无限放大 |
MaxRemoteRedirects |
3 | 一般不用改 |
LoginFailureLimit / LoginLockout |
5 次 / 15 分钟 | 按业务调整 |
AntiforgeryEnabled |
true | 不要关 |
RateLimitEnabled |
true | 确认匿名接口都有限流 |
ExternalApiTimeout |
10 秒 | 按外部服务调整 |
UseAutoSyncStructure |
开发 true | 生产关闭 |
| HTTPS | ------ | 必须启用 |
wwwroot/uploads 权限 |
------ | 应用账号可写,不可执行 |
| Redis | ------ | 设密码、不暴露公网端口 |
六、小结
后台安全可以拆成六句话:
- 身份:票据加密、不能跨用途复用、多端登录可踢。
- 入口:路由安全码混淆探测成本,但真正的边界是登录与授权。
- 请求:写操作校验 CSRF,匿名接口限流 + 校验参数。
- 授权 :路由、操作、方法、数据四层,前端隐藏只是体验。
- 内容:上传看内容不看声明,出站请求校验目标,路径拒绝上跳。
- 信息:对外模糊(通用提示 + TraceId),对内精确(日志含租户/用户/路径)。
这些机制在仓库里都有对应源码和测试;真正需要你判断的是:权限矩阵怎么配、上线参数怎么改、业务上还有哪些特殊约束。
如果你正在用 .NET 10 + Blazor 做企业后台,可以按这篇的对照表检查自己的项目。EasyAdminBlazor 的安全组件与测试都在仓库里,能直接复用,也能作为自研时的参考清单。