
做过几个后台管理系统之后你会发现一个规律无论业务多简单“登录登出”这一关永远绕不开。我第一次在实际项目里用 ASP.NET Core 的 Identity 框架时其实纠结了很久——网上资料多但多数只教你“点几下就生成好了”真正讲到框架内部怎么流转、为什么这么设计的内容很少。这篇文章以 ASP.NET Core 7 为背景把自己从项目搭建到线上踩坑的完整过程整理出来尤其是那些文档不会写、只有真的做过一遍才踩得到的细节。我接手过一个内部培训管理系统需求调研会上老板拍板“登录注册给我做出来一周上线”。我一听还把用户表、密码加密、Session 都列了个计划最后被同事劝住“直接用 Identity 吧别自己造轮子了。”后来证明这句话至少帮我省了一周。如果你也正卡在“该不该用 Identity”或者“用了之后怎么改”这个路口这篇文章应该能给你省下不少试错时间。1. 先搞清楚Identity到底做了哪些事1.1 为什么“自己写一个登录”不划算很多人第一反应是登录系统不就是一张用户表加一个密码比对吗我刚开始也这么干过直到发现要补的东西越来越多。存密码不能存明文吧那得做哈希加盐。用户忘记密码要重置光生成重置链接就能写一整天。连续输错密码要锁账号防止暴力破解。还要记住密码、记住登录状态、支持第三方登录、支持角色权限控制、让管理员能封禁某个用户……这些东西单拆开来都不难但合在一起就是一个完整的身份认证体系任何一个环节考虑不周上线之后都可能变成安全事故。ASP.NET Core 7 内置的 Identity 框架恰好把这些标准功能全部做出来了。它解决了两个核心问题一是“你是谁”——通过用户名、邮箱、密码登录来确认身份二是“你能干什么”——通过角色Role和声明Claim控制系统里的各种权限。你不需要从零设计用户表结构不需要自己写密码哈希逻辑也不需要担心 Cookie 认证流程出错框架已经帮你处理好了。1.2 Identity的组成结构User、Role、Claim刚开始接触 Identity 时我最晕的就是“User、Role、Claim、Token”这几个概念的关系。后来我自己画了个图才理顺这里用大白话给你解释。User 就是用户本身对应一张 Users 表。默认字段包含用户名、Email、手机号、密码哈希、锁定信息等。Role 是角色比如“管理员”“编辑”“访客”对应 Roles 表。User 和 Role 是多对多关系中间表叫 UserRoles。Claim 是声明可以理解成“用户身上带的一个个标签”比如“部门技术部”“工号00123”。Claim 比 Role 更细粒度适合做类似“只允许技术部的员工访问这个页面”这种授权逻辑。这三者配合起来就形成了一套完整的认证授权体系用户登录成功后系统会把用户包含的角色和声明打包在一个“身份”里后续每次请求都带上这个身份框架通过它判断你能不能访问某个接口或页面。理解了这条主线后面所有配置都不会觉得乱。2. 从零搭建把登录功能跑起来2.1 项目创建与Identity脚手架我在 .NET 7 里用的还是传统的 MVC 模板因为 Identity 默认带的 UI 页面注册、登录、忘记密码就是给 MVC 准备的。打开终端执行dotnet new mvc -n IdentityDemo cd IdentityDemo接下来要加几个包。这里容易踩坑网上很多教程会让你直接安装Microsoft.AspNetCore.Identity.EntityFrameworkCore但别忘了它依赖 EF Core你还需要指定使用哪种数据库。我图省事用了 SQLite一条命令全搞定dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore dotnet add package Microsoft.EntityFrameworkCore.Sqlite dotnet add package Microsoft.EntityFrameworkCore.Design dotnet add package Microsoft.AspNetCore.Identity.UI然后运行脚手架命令让它把 Identity 相关页面自动生成到项目里dotnet aspnet-codegenerator identity --useDefaultUI --dbContext ApplicationDbContext --force这条命令会做几件事生成ApplicationDbContext它继承自IdentityDbContext生成 Areas/Identity 目录里面放的是管理登录注册的 Razor 页面还会在Program.cs里补上部分配置。如果你的 VS Code 命令行找不到aspnet-codegenerator说明缺少全局工具先执行dotnet tool install -g dotnet-aspnet-codegenerator生成完之后别急着跑还有几个配置要手工检查不然启动就会报错。2.2 注册服务与中间件配置打开Program.cs核对下面几行是否齐全。我的写法是显式加上了角色服务因为很多场景都需要用到“管理员”这个概念using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Identity; using IdentityDemo.Data; var builder WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); builder.Services.AddDbContextApplicationDbContext(options options.UseSqlite(Data Sourceapp.db)); builder.Services.AddDefaultIdentityIdentityUser(options { options.SignIn.RequireConfirmedAccount false; }) .AddRolesIdentityRole() .AddEntityFrameworkStoresApplicationDbContext(); var app builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler(/Home/Error); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.MapRazorPages(); app.MapDefaultControllerRoute(); app.Run();几个要留意的细节UseAuthentication()和UseAuthorization()的顺序不能反先有认证才能做授权这是中间件管道的铁律。启动项目前记得执行数据库迁移dotnet ef migrations add InitialIdentity dotnet ef database update不执行这两步运行时会抛“no such table: AspNetUsers”之类的错误。这里多说一句AddDefaultIdentity默认会注册掉SignInManager、UserManager、RoleManager这些服务依赖注入直接在构造函数里拿就行不需要额外 new。2.3 在 VS Code 里把项目跑起来经常有人问“VS Code 怎么运行 ASP.NET Core 项目”。其实 .NET 的命令行工具和 IDE 是解耦的VS Code 只是编辑器运行靠的是dotnetCLI。三步走dotnet restore dotnet build dotnet run看到 “Now listening on: https://localhost:xxxx” 就说明起来了。浏览器打开对应地址你会发现右上角自动出现了“Register”和“Login”链接这就是 Identity 自带 UI 页面。如果你想让 VS Code 支持断点调试需要安装 C# Dev Kit 插件。装好之后打开.csproj文件VS Code 会提示生成launch.json选“C#”这个调试配置按 F5 就能命中断点。用习惯之后我觉得比 Visual Studio 还轻便尤其适合写小项目和做演示。3. 核心机制拆解三个Manager撑起整个框架3.1 UserManager用户数据的CRUDIdentity 的价值一半体现在UserManagerTUser上。这个类封装了几乎所有用户管理的原子操作创建用户、删除用户、修改密码、确认邮箱、锁定账号、添加删除 Claim 和角色。我平时用得最多的是这个public class UserService { private readonly UserManagerIdentityUser _userManager; public UserService(UserManagerIdentityUser userManager) { _userManager userManager; } public async TaskIdentityResult CreateUserAsync(string username, string email, string password) { var user new IdentityUser { UserName username, Email email, EmailConfirmed true }; var result await _userManager.CreateAsync(user, password); return result; } }注意CreateAsync的第二个参数是明文密码框架内部会自动生成哈希存储你永远不需要自己写 MD5 或 SHA256那是给自己挖坑。拿到IdentityResult之后记得检查result.Succeeded如果失败result.Errors里会有具体原因比如密码强度不够、用户名已存在。我在项目中一定会把这些错误逐条显示到前端不这么干用户都不知道自己错哪了。另外UserManager还提供FindByIdAsync、FindByNameAsync、FindByEmailAsync、GetRolesAsync、IsInRoleAsync这些查询方法基本覆盖了日常所有查询场景。它的设计意图很清晰你的控制器尽可能只依赖 UserManager不直接操作数据库上下文这样用户数据的一致性有框架保证也不会写出“把密码哈希直接 update 进表里”的骚操作。3.2 SignInManager登录、登出与Cookie认证SignInManagerTUser管的是“你怎么证明你登录过”。每个请求进来时框架会读取 Cookie 里的身份票据验证票据的有效性然后恢复用户信息。默认情况下这些逻辑都在UseAuthentication()中间件里完成了但“验证密码并签发票据”的动作必须由你主动调用。一个标准登录流程长这样public class AccountController : Controller { private readonly SignInManagerIdentityUser _signInManager; private readonly UserManagerIdentityUser _userManager; public AccountController( SignInManagerIdentityUser signInManager, UserManagerIdentityUser userManager) { _signInManager signInManager; _userManager userManager; } [HttpPost] [ValidateAntiForgeryToken] public async TaskIActionResult Login(LoginViewModel model) { if (!ModelState.IsValid) return View(model); var result await _signInManager.PasswordSignInAsync( model.UserName, model.Password, isPersistent: model.RememberMe, lockoutOnFailure: true); if (result.Succeeded) return RedirectToAction(Index, Home); if (result.IsLockedOut) ModelState.AddModelError(string.Empty, 账号已被锁定请稍后再试); else ModelState.AddModelError(string.Empty, 用户名或密码错误); return View(model); } [HttpPost] public async TaskIActionResult Logout() { await _signInManager.SignOutAsync(); return RedirectToAction(Index, Home); } }PasswordSignInAsync的第四个参数lockoutOnFailure值得讲讲我见过不少人这里直接传false等于白白放弃了防暴力破解的能力。它的作用是连续输错密码超过阈值后账号进入锁定状态。锁定时间在AddDefaultIdentity的选项里配置默认是 5 次失败锁 5 分钟。生产环境我一定开这个不然弱密码用户会被撞库撞到怀疑人生。登出调用的SignOutAsync所做的核心操作就是清除当前 HTTP 上下文里的登录 Cookie。弄懂了这一点你遇到“退出登录后按浏览器返回还能看到页面”的情况就不会慌了——那只是浏览器本地缓存刷新一下就会跳回登录页。3.3 RoleManager与Claims权限从哪来权限控制我分成两级来用。第一级是 Role适合“整个模块”的粗粒度控制。比如后台管理页必须“管理员”才能进直接用特性标签[Authorize(Roles Admin)] public IActionResult AdminDashboard() { return View(); }角色数据的维护靠RoleManagerIdentityRole。初始化角色是项目启动时必做的一件事我习惯写成一个种子方法public static async Task EnsureRolesAsync(IServiceProvider serviceProvider) { var roleManager serviceProvider.GetRequiredServiceRoleManagerIdentityRole(); string[] roleNames { Admin, User }; foreach (var roleName in roleNames) { if (!await roleManager.RoleExistsAsync(roleName)) { await roleManager.CreateAsync(new IdentityRole(roleName)); } } }第二级是 Claim适合“数据级别”的细节控制。比如某个报表页面只有“部门A”的员工能看。这时业务字段不应该塞进 Role 名字里面否则角色会爆炸。正确做法是在注册或登录的时候给用户附加一个自定义 Claimawait _userManager.AddClaimAsync(user, new Claim(Department, A));然后写一个授权策略builder.Services.AddAuthorization(options { options.AddPolicy(DepartmentAOnly, policy policy.RequireClaim(Department, A)); });用法就是[Authorize(Policy DepartmentAOnly)]。这种用“角色管大面、Claim 管细节”的方式在项目里非常顺手逻辑也清晰。4. 踩坑记录几个让我头痛的问题4.1 登录页面无限重定向刚把项目部署到测试环境时我遇到过一个极其诡异的问题访问任何需要登录的页面全部跳转到登录页填完账号密码提交又跳回登录页像一个死循环。排查了大半天最后发现是 HTTPS 和 Cookie 的安全策略打架了。默认情况下UseHttpsRedirection()会把 HTTP 请求重定向到 HTTPS。但测试环境直接用 IP 访问证书是自签的浏览器根本不信任。Cookie 又设置了Secure属性只在 HTTPS 连接下传输。登录成功后浏览器拿到新 Cookie结果当前连接是 HTTPCookie 发不回去服务端每次看到的都是“未登录”于是死循环。解决方案有两种看你部署环境怎么搭一种是测试环境就别启用 HTTPS 重定向直接在开发环境用 HTTP 调另一种是搭一个可信的反向代理终止 TLS 之后内部走 HTTP同时把ForwardedHeaders中间件打开。我后来用的第二种正式环境也稳定。4.2 密码策略太严导致用户流失Identity 默认密码策略要求至少 6 位、大小写和数字混合、还有特殊字符。这对内部系统来说简直劝退。有一次给客户演示现场输密码我设了个Admin123结果因为里面没有小写字母被拒了场面一度非常尴尬。这个配置在AddDefaultIdentity的选项里可以在不影响安全性的前提下适当放宽builder.Services.AddDefaultIdentityIdentityUser(options { options.Password.RequiredLength 6; options.Password.RequireDigit true; options.Password.RequireLowercase true; options.Password.RequireUppercase false; options.Password.RequireNonAlphanumeric false; options.Lockout.DefaultLockoutTimeSpan TimeSpan.FromMinutes(5); options.Lockout.MaxFailedAccessAttempts 5; }) .AddRolesIdentityRole() .AddEntityFrameworkStoresApplicationDbContext();我的原则是长度必须够至少要 8 位大小写和特殊字符可以适当放宽。尤其对内部管理系统用户都是同事密码策略太变态最后只能大家都往标签上贴密码。4.3 常见问题速查表问题现象可能原因解决办法运行时报 “no such table: AspNetUsers”没执行数据库迁移执行dotnet ef database update登录后访问页面仍跳回登录页Cookie 未正确写入HTTPS 配置不一致检查 HTTPS 重定向与 Cookie Secure 属性注册时提示 “The password must have at least one uppercase”密码策略过于严格调整Password.RequireUppercase等选项[Authorize(Roles Admin)]不生效用户虽然角色匹配但登录时角色未加载确保重写IUserClaimsPrincipalFactory时加载角色多环境部署时登录状态反复丢失应用数据保护密钥不一致在AddDataProtection().PersistKeysToFileSystem(...)中固定密钥存储位置使用自定义用户类后IdentityUser方法报错依赖注入类型没换成自定义用户统一使用UserManagerApplicationUser并注册AddIdentityApplicationUser, IdentityRole有两项需要额外说明。第一项“角色未加载”的问题常见于你重写了登录用户的信息生成逻辑。默认框架在用户登录后会把用户所属角色加进 Claims 里授权时才能判断。如果你自己实现了IUserClaimsPrincipalFactory一定要调用_userManager.GetRolesAsync(user)并把角色转成Claim加进去否则角色权限全部失效。第二项“数据保护密钥不一致”是我做过一次多机部署后才发现的。默认情况下密钥存储在本地文件开发机、服务器各存各的一台机器上登录换到另一台机器就提示要重新登录。解决方法是把密钥统一存在一个共享目录builder.Services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(D:\shared-keys)) .SetApplicationName(IdentityDemo);同一个应用名、同一份密钥登录状态在多机之间就能共享。4.4 外部登录和 JWT 场景的一点扩展很多项目现在不是传统 MVC而是前后端分离。这时候 Identity 默认的 Cookie 认证就不太够用你需要签发 JWT 而不是 Cookie。Identity 的核心逻辑仍然可以复用只是把“验证成功之后签发 Cookie”替换成“验证成功之后签发 Token”。我做过的一个小程序项目就是这么干的前端 Vue 拿账号密码调登录接口后端用SignInManager验证身份验证通过后你自己生成一个 JWT 返回给前端。JWT 的生成可以用System.IdentityModel.Tokens.Jwt包代码核心就是创建一个包含Claim的安全令牌然后设置有效期和密钥。Identity 在这套架构里负责的是“用户的存储和密码验证”JWT 负责的是“后续请求的身份传递”两者不冲突搭在一起反而各司其职。5. 聊聊 VS Code 与命令行跑项目这节我单独拿出来是因为很多从 Visual Studio 转过来的人对命令行又爱又怕。其实 ASP.NET Core 的项目结构、配置、构建过程本质都是围绕.csproj文件展开的跟 IDE 没有强绑定。VS Code 只是帮你写代码、看代码的工具编译和运行全交给dotnetCLI。所以如果你的电脑只需要能跑 .NET 7 SDK加一个 VS Code再装 C# Dev Kit 插件开发体验已经非常好了。我用这个组合写过一个完整的小型 SaaS 后台除了数据库管理器用单独的工具之外全程没有打开过一次 Visual Studio。多提一句如果你后续要配合 Docker 部署这个工作流会更舒服。因为dotnet publish打出来的产物、dotnet ef migrations生成的 SQL 脚本都可以直接在命令行集成进 CI/CD 管道里。我最后发布到服务器时用的就是一条命令构建镜像、一条命令启动容器整套流程不用点任何图形界面。6. 写在最后的一些心得兜兜转转用了这么久 Identity我最大的感受是它不是一个“可以躺平”的黑盒。你仍然需要理解用户、角色、Claim 之间的关系需要知道 Cookie 认证的流程需要在合适的场景换成 JWT。但 Identity 替你把最容易被写错、最容易有安全漏洞的那部分——密码存储、尝试次数锁定、身份票据校验——做成了一套经过大量生产环境验证的机制。按照行业里的成功案例身份认证这块是最不该自己造轮子的地方。我见过不少代码库因为“想给用户加个字段”最后把整张AspNetUsers表都改得面目全非迁移时惨不忍睹。如果你遇到扩展用户信息的需求正路是新建一张表用外键关联到AspNetUsers的 Id而不是去动框架的原生表结构。最后分享一个小技巧Identity 自带 UI 页面的表单样式可能不够好看但你不需要一上来就重写整个页面。先跑起来、把业务逻辑打通之后随便挑一个页面局部修改它的 Razor 文件即可。开发时永远让“能跑”优先于“好看”这套组合拳打下来你的系统第一周就能有个能登录、能退出的基本盘后面加功能也就从容多了。