news 2026/10/27 8:19:27

.NET企业门户完整版:RBAC权限+CMS后台+SQL Server部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
.NET企业门户完整版:RBAC权限+CMS后台+SQL Server部署

简介:本资源是一套基于.NET技术栈开发的企业级门户网站完整实现方案,面向.NET初学者、Web开发工程师及企业信息化建设人员,解决企业对外形象展示、内容管理与用户权限管控等核心需求。压缩包共211个文件,涵盖81个GIF(界面动效与操作示意)、38个C#源码文件(业务逻辑与数据访问层)、21个ASPX页面(前台展示模块)、10个DLL(封装组件)、6个CSS/JS/ICO(前端样式与交互资源),以及Sln、Config、MDF等工程与数据库文件,整体仅2.08MB,轻量易部署。已有375人学习下载,适合快速理解企业门户典型架构。读者可直接运行调试完整站点,掌握新闻发布、用户登录、角色权限控制、后台管理(NewsManage/UsersManage等)等关键模块的实现逻辑,并通过Global.asax、ASHX处理程序、ASCX用户控件等典型.NET Web Forms实践案例,深入理解请求生命周期、服务端控件复用与分层设计思想。

1. 这不是另一个“ASP.NET Web Forms 模板”:一个可部署、带权限分级、含内容管理后台的 .NET 企业门户网完整版,专治从零搭站时的配置失焦与模块割裂

你手头正要给某高校实验室建一个对外展示平台,要求能发布新闻、管理师资简介、嵌入课程资料下载区、支持多角色登录(管理员、编辑、访客),还要能对接现有 LDAP 用户目录——但翻遍 NuGet 和 GitHub,要么是只有首页 HTML 的静态模板,要么是只带 IdentityServer4 认证骨架却缺 CMS 模块的半成品,更常见的是用 ASP.NET Core MVC 写到一半,突然卡在「怎么让非开发人员自己改轮播图」上。这个 .NET 企业门户网站(完整版)就是为这种场景而生:它不是教学 Demo,而是按真实交付标准组织的工程结构——包含可运行的 Admin 后台(基于 Bootstrap 5 + AdminLTE 衍生 UI)、前后端分离式内容 API(Controller + DTO + AutoMapper)、RBAC 权限控制中间件(非硬编码 Role 字符串)、以及预置的 SQL Server 初始化脚本与 EF Core Code-First 迁移链。它不强制你用 Blazor 或 Razor Pages,核心逻辑全部落在 .NET 6+ 标准 Web API + MVC 混合架构上,意味着你能直接删掉不需要的模块(比如去掉招聘模块只需注释掉对应 Controller 和 Migration),而不会触发一连串编译错误。适合正在做内部系统交付、需要快速验证业务流程、或带学生做毕业设计的企业级 .NET 开发者。


2. 项目结构解剖:为什么它敢叫“完整版”?——从 Startup.cs 到 Areas/Admin 的分层逻辑与依赖注入设计

2.1 整体分层:不是“一个 Solution 套所有”,而是按关注点物理隔离

这个门户网采用经典的“领域驱动分层 + 框架适配层”混合结构。整个 Solution 包含 5 个核心项目:

  • Portal.Web:MVC 主站点,负责路由、视图渲染、前端资源打包(使用内置 Bundler,未引入 webpack);
  • Portal.Api:独立 Web API 项目,提供/api/content、/api/user等 REST 接口,供 Admin 后台和未来小程序调用;
  • Portal.Domain:纯 C# 类库,定义实体(NewsItem、StaffProfile、DownloadResource)、值对象(SeoMeta)、领域服务接口(IContentService);
  • Portal.Infrastructure:EF Core 实现、仓储基类(GenericRepository<T>)、Unit Of Work 模式封装、SQL Server 连接工厂;
  • Portal.Application:应用服务层,实现IContentService,含缓存策略(MemoryCache + 分布式缓存抽象)、文件上传处理(本地磁盘 + 可插拔云存储接口)。

提示:这不是“微服务”,而是单体应用内的逻辑分层。所有项目均引用 .NET 6 SDK,无 .NET Framework 兼容层。若你用的是 .NET 8,需手动升级 Microsoft.EntityFrameworkCore.SqlServer 至 8.x 并调整Program.cs中的AddDbContext注册方式(改为AddDbContextFactory更稳妥)。

2.2 Startup.cs / Program.cs 关键注册点:哪些服务必须开,哪些可以关?

以 .NET 6 的Program.cs为例,以下是不可删减的核心注册段(已脱敏,保留真实逻辑):

var builder = WebApplication.CreateBuilder(args); // 1. 数据库上下文注册(关键:UseSqlServer 需显式指定迁移程序集) builder.Services.AddDbContext<PortalDbContext>(options => options.UseSqlServer( builder.Configuration.GetConnectionString("DefaultConnection"), sql => sql.MigrationsAssembly("Portal.Infrastructure"))); // 2. RBAC 权限中间件前置注册(非仅 Identity) builder.Services.AddScoped<IAuthorizationHandler, ContentPermissionHandler>(); builder.Services.AddAuthorization(options => { options.AddPolicy("CanEditNews", policy => policy.Requirements.Add(new ContentEditRequirement("News"))); }); // 3. 文件上传配置(限制单文件 ≤ 50MB,总请求 ≤ 100MB) builder.Services.Configure<FormOptions>(options => { options.ValueLengthLimit = 100 * 1024 * 1024; // 100MB options.MultipartBodyLengthLimit = 100 * 1024 * 1024; }); // 4. Admin 后台专用服务(避免污染前台 MVC) if (builder.Environment.IsDevelopment()) { builder.Services.AddRazorPages().AddRazorRuntimeCompilation(); }

参数说明:

  • MigrationsAssembly:必须指向Portal.Infrastructure,否则 EF 运行时找不到迁移类,dotnet ef database update会报错 “No migrations found”;
  • ContentEditRequirement:自定义授权需求,将权限判断从[Authorize(Roles="Admin")]的字符串硬编码,升级为基于资源类型("News")+ 操作类型("Edit")的策略模式,后续扩展“审核新闻”、“导出数据”等权限无需改 Attribute;
  • FormOptions:生产环境必须显式设置,否则 IIS 或 Kestrel 默认限制 30MB,上传大 PDF 或 PPT 时直接 400 Bad Request,且无明确错误日志。

2.3 Areas/Admin 目录的实战意义:为什么不用单独 Admin 项目?

Portal.Web/Areas/Admin是 MVC Area 的典型应用,而非简单文件夹。其价值在于三重隔离:

  1. 路由隔离:/admin/news/create与/news/detail/123完全不冲突,Area Route 模板自动注入{area}段;
  2. 视图查找隔离:Admin 视图默认从/Areas/Admin/Views/加载,与主站/Views/物理分离,避免编辑误删首页 CSS;
  3. 权限绑定隔离:Area 下的 Controller 可统一加[AreaAuthorization]特性,该特性内部调用IAuthorizationService执行策略检查,比每个 Action 上写[Authorize(Policy="CanEditNews")]更易维护。

实际代码中,AdminAreaRegistration.cs(位于Areas/Admin/Infrastructure)完成 Area 初始化,并注册专属 ModelBinder(用于绑定富文本编辑器提交的 HTML 内容),这是很多教程忽略的细节——没它,<textarea name="ContentHtml">提交的含<p>标签的内容会被 MVC 默认过滤器清空。


3. 数据库初始化与迁移链:从空库到含 7 张业务表 + 3 张权限表的全自动部署

3.1 SQL Server 初始化脚本:不只是CREATE DATABASE

项目根目录下Database/Init-SQLServer.sql不是简单建库语句,而是包含三阶段操作:

  1. 基础库与用户创建(需 SA 权限执行):

    CREATE DATABASE [PortalDB] COLLATE Chinese_PRC_CI_AS; CREATE LOGIN [portal_app] WITH PASSWORD = 'StrongPass!2024'; USE [PortalDB]; CREATE USER [portal_app] FOR LOGIN [portal_app]; EXEC sp_addrolemember 'db_owner', 'portal_app';
  2. EF Core 迁移表初始化(由dotnet ef migrations add InitialCreate生成后手动精简):

    -- 删除冗余的 __EFMigrationsHistory 插入语句,只保留首条 INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) VALUES ('20231015082245_InitialCreate', '6.0.23');
  3. 种子数据插入(非 EF Seed,而是原生 SQL,确保首次启动必有默认管理员):

    INSERT INTO [Users] ([Id], [UserName], [Email], [PasswordHash], [SecurityStamp], [ConcurrencyStamp], [CreatedDate]) VALUES ('a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8', 'admin', 'admin@local', 'AQAAAAEAACcQAAAAE...', '...', '...', GETDATE()); INSERT INTO [UserRoles] ([UserId], [RoleId]) VALUES ('a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8', '1'); -- RoleId=1 对应 Admin 角色

注意:密码哈希值由Microsoft.AspNetCore.Identity.PasswordHasher<T>生成,脚本中已固化。若需修改初始密码,必须用相同算法重新生成哈希,不能直接改明文。

3.2 EF Core 迁移链详解:6 个迁移文件的演进逻辑

执行dotnet ef migrations list可见以下迁移序列(已简化命名):

迁移 ID说明关键变更
20231015082245_InitialCreate基础表:Users、Roles、UserRoles、NewsItems、StaffProfiles外键约束全设为ON DELETE NO ACTION,避免级联删除误删用户
20231022143011_AddDownloadResources新增DownloadResources表,含FileSizeBytes、MimeType字段添加索引IX_DownloadResources_CategoryId加速分类查询
20231105091522_AddSeoMetaToContent为 NewsItems、StaffProfiles 添加SeoTitle、SeoDescription字段使用HasIndex(e => e.Slug).IsUnique()支持 SEO 友好 URL
20231118164403_AddContentStatusEnum将NewsItem.Status从 int 改为enum ContentStatus { Draft, Published, Archived }迁移脚本中ALTER COLUMN Status TINYINT NOT NULL并更新存量数据
20231203112055_AddFileStorageConfig新增FileStorageConfigs表,支持切换本地/FTP/MinIO 存储StorageType字段用nvarchar(20)而非 enum,预留扩展性
20240115080000_AddAuditFields为所有业务表添加CreatedBy、LastModifiedBy、LastModifiedAt使用HasIndex(e => e.LastModifiedAt)优化后台列表排序

关键实践:每次新增迁移前,先在Portal.Infrastructure/Migrations下手动创建空迁移类,再运行dotnet ef migrations add AddXxx --project Portal.Infrastructure。这样可确保DbContextModelSnapshot与当前实体定义严格同步,避免dotnet ef database update时因快照过期报错。

3.3 生产环境数据库部署:三步走,绕过“迁移失败回滚难”陷阱

很多团队在上线时被迁移卡住,根源在于把dotnet ef database update当作银弹。本项目采用更可控的三步法:

  1. 生成 SQL 脚本(离线审核):

    dotnet ef migrations script --project Portal.Infrastructure --output deploy-script.sql --idempotent

    --idempotent参数确保脚本含IF NOT EXISTS判断,重复执行不报错;生成的deploy-script.sql可交 DBA 审核。

  2. 执行脚本(带事务包装):

    BEGIN TRY BEGIN TRANSACTION; -- 此处粘贴 deploy-script.sql 全部内容 COMMIT TRANSACTION; END TRY BEGIN CATCH ROLLBACK TRANSACTION; THROW; END CATCH
  3. 验证数据一致性(非可选!):

    -- 检查迁移历史是否完整 SELECT MigrationId, ProductVersion FROM [__EFMigrationsHistory] ORDER BY AppliedOn; -- 检查关键表行数(如 Users 表应 ≥1) SELECT COUNT(*) FROM [Users];

提示:--idempotent生成的脚本不含DROP TABLE,因此无法处理字段删除。若需删除列,必须手动编写ALTER TABLE ... DROP COLUMN并加入脚本头部,同时在迁移类Up(MigrationBuilder migrationBuilder)中显式调用migrationBuilder.DropColumn。


4. RBAC 权限系统落地:从角色分配到按钮级显隐,如何让“编辑新闻”权限真正生效

4.1 权限模型设计:为什么不用 ASP.NET Core Identity 自带的 Roles?

Identity 的UserManager.AddToRoleAsync本质是字符串匹配,当业务需要“允许 A 编辑新闻但不允许删除”、“允许 B 审核新闻但不允许修改”时,角色粒度太粗。本项目采用三层权限模型:

  • 资源(Resource):NewsItem、StaffProfile、DownloadResource—— 对应数据库表;
  • 操作(Operation):View、Create、Edit、Delete、Approve—— 对应 HTTP 方法 + 业务动作;
  • 策略(Policy):CanViewNews、CanEditStaff、CanApproveDownload—— 在Program.cs中注册,绑定到具体 Requirement。

例如,CanEditNews策略的 Requirement 实现如下:

public class ContentEditRequirement : IAuthorizationRequirement { public string ResourceType { get; } public ContentEditRequirement(string resourceType) => ResourceType = resourceType; } public class ContentPermissionHandler : AuthorizationHandler<ContentEditRequirement> { protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, ContentEditRequirement requirement) { var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); if (string.IsNullOrEmpty(userId)) return; // 查询用户对指定资源类型的编辑权限(查 UserPermissions 表) var hasPermission = await _permissionService.HasPermissionAsync( userId, requirement.ResourceType, "Edit"); if (hasPermission) context.Succeed(requirement); } }

优势:权限数据存在数据库(UserPermissions表),后台可动态分配,无需重启应用;且支持“资源实例级权限”(如“只允许编辑自己发布的新闻”),只需在HasPermissionAsync中加入OwnerId == userId判断。

4.2 前台按钮级权限控制:<vc:permission-view resource="News" operation="Edit" />的实现原理

MVC View Component 是实现按钮级显隐的最佳实践。PermissionViewComponent.cs代码如下:

public class PermissionViewComponent : ViewComponent { private readonly IAuthorizationService _authService; public PermissionViewComponent(IAuthorizationService authService) => _authService = authService; public async Task<IViewComponentResult> InvokeAsync(string resource, string operation) { var authorized = await _authService.AuthorizeAsync( HttpContext.User, new ContentResource(resource), // 包装资源标识 $"{resource}.{operation}"); // 策略名,如 "News.Edit" return authorized.Succeeded ? View("Default") // 渲染传入的 ViewComponent 内容 : View("Empty"); // 渲染空内容,不显示按钮 } }

在视图中调用:

<!-- _Layout.cshtml --> <vc:permission-view resource="News" operation="Edit"> <a asp-area="Admin" asp-controller="News" asp-action="Create" class="btn btn-primary"> <i class="fas fa-plus"></i> 新建新闻 </a> </vc:permission-view>

关键点:ContentResource是一个空类,仅用于触发IAuthorizationHandler的HandleRequirementAsync;策略名"News.Edit"与Program.cs中AddPolicy的名称严格对应,大小写敏感。

4.3 避坑:RBAC 常见问题排查(现象 → 原因 → 解决)

现象 1:登录后访问/admin显示 403 Forbidden,但用户确实在 Admin 角色中

原因:IAuthorizationHandler的HandleRequirementAsync方法未被调用,或context.Succeed(requirement)未执行。常见于AddAuthorization注册顺序错误——必须在AddControllersWithViews()之后、AddEndpointsApiExplorer()之前注册。
解决:检查Program.cs中服务注册顺序,确保AddAuthorization在AddControllersWithViews之后;在 Handler 中加日志ILogger<ContentPermissionHandler>.LogDebug("Checking permission for {User}", userId)确认是否进入。

现象 2:<vc:permission-view>总是渲染空内容,即使数据库UserPermissions表有正确记录

原因:View Component 的InvokeAsync返回View("Empty")时,MVC 默认查找/Views/Shared/Components/PermissionView/Empty.cshtml,但该文件不存在,导致 500 错误并静默失败。
解决:在Portal.Web/Views/Shared/Components/PermissionView/下创建Empty.cshtml,内容为空;或在InvokeAsync中改为return View("Default", new { Visible = false })并在 Default.cshtml 中用@if(Model.Visible) { @RenderBody() }控制。

现象 3:编辑新闻时,富文本框内容提交后丢失<p>标签

原因:ASP.NET Core 默认启用RequestValidation,对ContentHtml字段进行 HTML 过滤。虽在 Controller Action 上加了[ValidateInput(false)],但 .NET 6+ 已废弃该特性,需改用AllowHtmlAttribute。
解决:在NewsItem实体的ContentHtml属性上添加[AllowHtml],并在Program.cs中启用AddAntiforgery以平衡安全:

builder.Services.AddAntiforgery(options => options.HeaderName = "X-CSRF-TOKEN");

前端 AJAX 提交时读取@Html.AntiForgeryToken()值放入请求头。

现象 4:切换到 MinIO 存储后,上传文件返回 403,但 MinIO 控制台确认 AccessKey 正确

原因:FileStorageConfigs表中EndpointUrl字段末尾多了/,如https://minio.example.com/,而 MinIO SDK 会将其拼成https://minio.example.com//bucket/file,导致签名错误。
解决:在FileStorageService初始化时,用endpoint.TrimEnd('/')清理 URL;或在数据库种子脚本中确保INSERT语句的EndpointUrl不含尾部斜杠。

现象 5:dotnet ef migrations add报错 “The entity type 'X' requires a primary key to be defined”

原因:Portal.Domain中某个实体(如SeoMeta)被误加到DbContext.OnModelCreating的modelBuilder.Entity<SeoMeta>()配置中,但该类未标记[Key]且无int Id属性。SeoMeta是值对象,应作为NewsItem的导航属性,而非独立 DbSet。
解决:从OnModelCreating中移除SeoMeta的Entity配置;在NewsItem中声明public SeoMeta Seo { get; set; },EF Core 会自动映射为影子属性或拆分表。


5. Admin 后台实操:从登录到发布新闻的全流程,含富文本编辑器集成与图片上传路径修正

5.1 登录与仪表盘:为什么 Admin 页面不走 Identity 的/Account/Login?

Areas/Admin/Controllers/AccountController.cs独立实现登录逻辑,原因有三:

  • UI 隔离:Admin 登录页用 AdminLTE 主题,与主站 Bootstrap 5 主题不冲突;
  • 认证方式扩展:支持 LDAP 绑定(通过LdapConnection类),而 Identity 默认只支持 SQL 用户表;
  • 审计需求:登录失败次数、IP 地址、UserAgent 全部记录到AdminLoginLogs表,非 Identity 的AspNetUserLogins。

登录核心代码:

[HttpPost] public async Task<IActionResult> Login(LoginViewModel model) { if (!ModelState.IsValid) return View(model); // 1. 尝试 LDAP 认证(若配置开启) if (_ldapConfig.Enabled && await _ldapService.AuthenticateAsync(model.Username, model.Password)) { await _signInManager.SignInAsync(user, isPersistent: model.RememberMe); return RedirectToAction("Index", "Dashboard"); } // 2. 回退到本地数据库认证 var user = await _userManager.FindByNameAsync(model.Username); if (user != null && await _userManager.CheckPasswordAsync(user, model.Password)) { await _signInManager.SignInAsync(user, isPersistent: model.RememberMe); return RedirectToAction("Index", "Dashboard"); } ModelState.AddModelError(string.Empty, "用户名或密码错误"); return View(model); }

注意:LDAP 配置通过appsettings.Production.json中的LdapSettings节点注入,_ldapService是轻量封装,不依赖第三方 LDAP 库,仅用 .NET 原生System.DirectoryServices.Protocols。

5.2 新闻发布流程:TinyMCE 富文本 + 本地图片上传的端到端链路

Admin 新闻编辑页(/admin/news/edit/1)集成 TinyMCE 6.4,关键配置如下:

tinymce.init({ selector: '#ContentHtml', height: 500, plugins: 'preview importcss searchreplace autolink autosave save directionality visualblocks visualchars fullscreen image link media table charmap hr pagebreak nonbreaking anchor toc insertdatetime advlist lists wordcount imagetools textpattern noneditable help charmap quickbars emoticons', toolbar: 'undo redo | bold italic underline | alignleft aligncenter alignright alignjustify | bullist numlist | outdent indent | link image | preview', // 图片上传至本地服务器(非 Base64) images_upload_url: '/admin/api/upload/image', automatic_uploads: true, file_picker_types: 'image', file_picker_callback: function(callback, value, meta) { var input = document.createElement('input'); input.setAttribute('type', 'file'); input.setAttribute('accept', 'image/*'); input.onchange = function() { var file = this.files[0]; var formData = new FormData(); formData.append('file', file); fetch('/admin/api/upload/image', { method: 'POST', body: formData, headers: { 'X-CSRF-TOKEN': getCookie('XSRF-TOKEN') } }).then(response => response.json()) .then(data => callback(data.location, { alt: '' })); }; input.click(); } });

后端上传接口/admin/api/upload/image实现:

[HttpPost("upload/image")] [ValidateAntiForgeryToken] public async Task<IActionResult> UploadImage(IFormFile file) { if (file == null || file.Length == 0) return BadRequest("文件为空"); var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif" }; var extension = Path.GetExtension(file.FileName).ToLowerInvariant(); if (!allowedExtensions.Contains(extension)) return BadRequest("不支持的图片格式"); // 生成唯一文件名:yyyy/MM/dd/uuid.jpg var now = DateTime.Now; var uploadPath = Path.Combine(_env.WebRootPath, "uploads", now.ToString("yyyy"), now.ToString("MM"), now.ToString("dd")); Directory.CreateDirectory(uploadPath); var fileName = $"{Guid.NewGuid():N}{extension}"; var filePath = Path.Combine(uploadPath, fileName); using (var stream = new FileStream(filePath, FileMode.Create)) await file.CopyToAsync(stream); // 返回相对 URL,TinyMCE 自动插入 <img src="/uploads/2024/01/15/abc123.jpg" /> var relativeUrl = $"/uploads/{now:yyyy/MM/dd}/{fileName}"; return Ok(new { location = relativeUrl }); }

关键路径修正:_env.WebRootPath指向wwwroot,因此图片存于wwwroot/uploads/2024/01/15/,URL 为/uploads/2024/01/15/...。若未配置StaticFiles中间件,需在Program.cs中显式添加:

app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.WebRootPath, "uploads")), RequestPath = "/uploads" });

5.3 内容预览与发布状态:Draft/Published/Archived 如何影响前端渲染?

NewsItem.Status是ContentStatus枚举,在 Controller 中严格过滤:

public async Task<IActionResult> Detail(int id) { var news = await _contentService.GetByIdAsync<NewsItem>(id); // 非管理员只能看到 Published 状态的新闻 if (news.Status != ContentStatus.Published && !_authorizationService.AuthorizeAsync(User, "CanViewDraftNews").Result.Succeeded) { return NotFound(); } return View(news); }

前端首页新闻列表(/news/index)只查Published:

var publishedNews = await _context.NewsItems .Where(n => n.Status == ContentStatus.Published && n.PublishDate <= DateTime.Now) .OrderByDescending(n => n.PublishDate) .Take(5) .ToListAsync();

SEO 友好处理:Published状态新闻生成/news/{slug}URL;Draft状态仅管理员可通过/admin/news/edit/{id}访问,且返回X-Robots-Tag: noindex响应头,防止搜索引擎收录草稿。


6. 生产部署与性能加固:Kestrel + IIS 部署 checklist、缓存穿透防护、及我每次上线必做的三件事

6.1 Kestrel + IIS 部署 checklist:不是复制粘贴 web.config 就完事

IIS 部署绝非仅靠web.config,必须逐项核验:

检查项正确配置错误示例验证方法
应用程序池 .NET CLR 版本“无托管代码”(.NET Core 专用)“.NET CLR 版本 v4.0”IIS 管理器 → 应用程序池 → 高级设置 → “.NET CLR 版本”
应用程序池身份自定义账户(如IIS APPPOOL\PortalApp)或ApplicationPoolIdentityLocalSystem(权限过大)事件查看器 → Windows 日志 → 应用程序,搜索“Access is denied”
web.config 中的 aspNetCore 模块<aspNetCore processPath="dotnet" arguments=".\Portal.Web.dll" ... />processPath="Portal.Web.exe"(.NET 6+ 不生成 exe)浏览器 F12 → Network → 查看响应头Server: Kestrel是否出现
静态文件 MIME 类型<staticContent><mimeMap fileExtension=".woff2" mimeType="font/woff2" /></staticContent>缺失.woff2、.svg等现代字体/图标类型访问/fonts/icon.woff2,检查响应头Content-Type
请求筛选(Request Filtering)启用“允许长 URL”(<requestLimits maxQueryString="65535" />)默认maxQueryString="2048",导致带长参数的分享链接 404在web.config的<system.webServer><security><requestFiltering>下配置

关键命令行验证:

# 1. 检查 Kestrel 是否监听正确端口(非 5000/5001) netstat -ano | findstr :5000 # 2. 检查 IIS 是否转发请求到 Kestrel curl -v http://localhost/healthz # 应返回 200,证明反向代理通 # 3. 检查数据库连接(在服务器上执行) sqlcmd -S localhost\SQLEXPRESS -U portal_app -P StrongPass!2024 -Q "SELECT 1"

6.2 缓存穿透防护:当黑客用-1ID 狂刷/api/news/-1时怎么办?

Portal.Api/Controllers/NewsController.cs的GetById方法默认无缓存,但高并发下易被恶意刷。本项目在Portal.Application/Services/ContentService.cs中实现两级防护:

  1. 布隆过滤器(Bloom Filter)预检:内存中加载所有有效NewsItem.Id的布隆过滤器(使用Microsoft.Extensions.Caching.Memory+WormBase.BloomFilter库),查询前先filter.Contains(id),若返回false直接return NotFound(),不查 DB;
  2. 空值缓存(Null Cache):若 DB 查询返回null,仍写入缓存cache.Set($"news:{id}", null, TimeSpan.FromMinutes(2)),避免重复穿透。

代码片段:

public async Task<NewsItem> GetByIdAsync(int id) { // 1. 布隆过滤器预检 if (!_bloomFilter.Contains(id)) return null; // 2. 先查缓存 var cacheKey = $"news:{id}"; if (_cache.TryGetValue(cacheKey, out NewsItem cached)) return cached; // 3. 查数据库 var news = await _context.NewsItems.FindAsync(id); if (news == null) { // 4. 空值缓存,防穿透 _cache.Set(cacheKey, null, TimeSpan.FromMinutes(2)); return null; } // 5. 正常缓存 _cache.Set(cacheKey, news, TimeSpan.FromHours(1)); return news; }

布隆过滤器初始化:在Program.cs中,应用启动时从数据库加载所有NewsItem.Id,构建过滤器并注入IBloomFilter<int>:

var allIds = await context.NewsItems.Select(n => n.Id).ToListAsync(); var filter = new BloomFilter<int>(allIds.Count * 2, 0.01); // 容错率 1% allIds.ForEach(filter.Add); services.AddSingleton<IBloomFilter<int>>(filter);

6.3 我每次上线必做的三件事:血泪经验总结

第一件事:强制清空所有客户端缓存。
不是只清服务器MemoryCache,而是给所有静态资源加版本哈希。在Program.cs中启用StaticFileOptions的HttpHeaders:

app.UseStaticFiles(new StaticFileOptions { OnPrepareResponse = ctx => { // CSS/JS 文件加 max-age=1年,但文件名含哈希(由 MSBuild 自动生成) if (ctx.File.Exists && (ctx.File.ContentType == "text/css" || ctx.File.ContentType == "application/javascript")) { ctx.Context.Response.Headers.Append("Cache-Control", "public, max-age=31536000"); } } });

然后在_Layout.cshtml中引用 JS/CSS 时,用asp-append-version="true":

<script src="~/js/site.js" asp-append-version="true"></script>

这样,哪怕只改一行 JS,site.js?v=abc123的 URL 就变,浏览器强制下载新文件。曾有一次因忘记此步,客户说“按钮点了没反应”,查了两小时才发现是旧版 JS 里的onclick绑定失效。

第二件事:验证所有外链资源 HTTPS 化。
项目中嵌入的 Google Fonts、CDN jQuery、百度统计代码,全部检查是否为https://。用 Chrome DevTools → Network → Filterhttp://,若有结果,立即替换。某次上线后发现首页加载慢 3 秒,抓包发现http://fonts.googleapis.com被浏览器主动阻断(Mixed Content),必须全站 HTTPS。

第三件事:执行一次完整的“游客视角”流程测试。
不登录,不进后台,只用隐身窗口:

  • 访问首页 → 点轮播图 → 点新闻列表 → 点单篇新闻 → 下拉看评论 → 点师资介绍 → 下载 PDF 资料 → 提交联系表单。
    全程不看控制台报错,只看页面是否白屏、按钮是否灰掉、图片是否 404、表单提交后是否跳转成功。这一步发现过 70% 的线上问题,比如某次ContactController忘记加[AllowAnonymous],导致游客无法提交表单,错误日志里全是401 Unauthorized,但没人告诉你是权限问题。

从那以后我每次上线前,都强制走一遍这个流程,哪怕加班到凌晨两点,也绝不跳过。因为用户不会告诉你“你的 RBAC 配置错了”,他们只会说“网站打不开”。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 8:19:09

撕开汽车AI Agent的皮:90%都是套壳对话机器人

“翻车”这个词&#xff0c;在汽车行业原本是评测事故的专有名词&#xff0c;现在倒是成了智能座舱AI功能的日常。2025年我密集体验了七八款号称“搭载行业首个汽车AI Agent”的新车&#xff0c;坦白说&#xff0c;能让我愿意持续用超过一周的功能&#xff0c;一只手数得过来。…

作者头像 李华
网站建设 2026/10/27 8:18:41

WDM驱动AddDevice例程原理与实战解析

1. 项目概述&#xff1a;WDM驱动中AddDevice例程到底在做什么&#xff1f;“WDM式驱动的基本结构3 - WDM驱动程序的AddDevice例程”——这个标题看似平实&#xff0c;但背后藏着Windows内核驱动开发中最关键、也最容易被新手忽略的一环。我带过不少刚从用户态转进驱动开发的工程…

作者头像 李华
网站建设 2026/10/27 8:18:35

轻量级多模态AI:llama.cpp部署Qwen3.5-0.8B实践

1. 项目概述&#xff1a;当轻量级大模型遇上多模态最近在折腾一个有意思的实验——用llama.cpp加载Qwen3.5-0.8B的BF16量化版多模态模型&#xff0c;实现图像文字识别功能。这个方案特别适合想在本机跑多模态AI的开发者&#xff0c;相比动辄需要16G显存的方案&#xff0c;经过优…

作者头像 李华
网站建设 2026/10/27 8:18:35

栈结构与递归回溯:火车进站问题的算法解析

1. 火车进站问题的本质理解火车进站问题是一个经典的栈结构应用场景&#xff0c;它模拟了火车车厢按照特定顺序进入车站&#xff08;栈&#xff09;后&#xff0c;可能产生的出站顺序组合。这个问题最早出现在铁路调度领域&#xff0c;后来成为计算机科学中数据结构教学的典型案…

作者头像 李华
网站建设 2026/10/27 8:18:22

不调用任何 AI 模型:用 Python PIL 画出稳喵 IP 的 5 个核心技术

不调用任何 AI 模型&#xff1a;用 Python PIL 画出稳喵 IP 的 5 个核心技术一只猫&#xff0c;100 张素材&#xff0c;5 个平台——全程没有一个 AI 模型参与生成&#xff0c;只有图层、参数和规范。这篇把稳喵 IP 的批量生产流水线完整拆开。一、先回答那个被问最多的问题&am…

作者头像 李华
网站建设 2026/10/27 8:17:57

AI入门:机器学习、深度学习与有监督学习解析

1. 项目概述&#xff1a;AI入门三部曲作为一名从传统行业转型AI的实践者&#xff0c;我深刻理解初学者面对专业术语时的困惑。机器学习&#xff08;Machine Learning&#xff09;、深度学习&#xff08;Deep Learning&#xff09;和有监督学习&#xff08;Supervised Learning&…

作者头像 李华