恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
.NET 6 Web API 快速骨架:SQL Server + JWT 鉴权全链路
首页
资讯中心
/
.NET 6 Web API 快速骨架:SQL Server + JWT 鉴权全链路
.NET 6 Web API 快速骨架:SQL Server + JWT 鉴权全链路
发布时间:2026/10/18 21:07:44
简介本资源是一套基于.NET 6构建的完整Web API实战项目面向C#后端开发初学者与进阶者聚焦企业级API开发核心能力训练——SQL Server数据库集成、JWT无状态身份认证及标准CRUD接口实现。项目采用分层架构含Model、Service、BLL、SqlSugarDB等独立csproj工程包含286个运行时DLL、96个C#源码文件、44个JSON配置与58个PDB调试符号体现典型.NET 6解决方案结构压缩包共665个文件大小34.81MB涵盖迁移脚本、Swagger文档配置、EF Core或SqlSugar数据访问层及全局异常处理模块。已有1062人学习下载读者可直接运行调试掌握JWT令牌签发验证流程、SQL Server连接池配置、RESTful路由设计规范及生产级日志与错误响应封装是快速搭建安全、可维护API服务的高复用参考模板。1. 一个能直接跑通的 .NET 6 Web API 骨架SQL Server 做数据底座JWT 控制访问权限增删改查全链路闭环你刚接手一个内部管理后台的后端模块需求很明确用 SQL Server 存用户和订单API 要带登录鉴权前端调用必须验 tokenCRUD 接口一个都不能少——但老板说“下周就要联调”。这时候翻文档、查 JWT 中间件配置、反复试AddDbContext的生命周期、被SqlException: Login failed卡住两小时……这些不是开发是自我消耗。这个 .NET 6 Web API 项目就是为这种场景而生它不讲原理只给可执行结果——开箱即用的dotnet run启动页、预置好连接字符串的appsettings.json、已注册AddJwtBearer的Program.cs、带[Authorize]的ProductsController以及一个能直接插入测试数据的SeedData类。它面向的是需要快速验证业务逻辑、不想在身份认证和数据库连接上反复踩坑的中初级开发者也适合做团队新成员的入门练手靶机。项目结构干净没有冗余 UI 层或第三方服务胶水所有代码都落在Controllers、Models、Data和Services四个核心目录下连迁移命令都写在 README 里——你复制粘贴就能跑改表名、加字段、换 token 密钥三步以内完成。2. 项目骨架与技术栈拆解从 Program.cs 到 DbContext 的初始化链路2.1 整体结构与模块职责划分项目采用标准的 .NET 6 Minimal Hosting 模型无Startup.cs所有配置集中在Program.cs。目录结构极简共 5 个一级目录Controllers/仅含ProductsController.cs实现/api/products下全部 RESTful 接口GET / POST / PUT / DELETEModels/定义Product.cs实体类含[Key]、[Required]等 Data Annotations及ApiResponseT统一响应封装Data/核心数据层含ApplicationDbContext.cs继承DbContext、ApplicationDbContextFactory.cs用于 EF Core CLI 迁移、SeedData.cs首次启动自动插入 3 条测试商品Services/目前仅IProductService接口 ProductService实现类封装了GetAllAsync、AddAsync等业务方法Controller 通过 DI 注入使用wwwroot/空目录预留静态资源位当前未启用提示此结构刻意规避了 Repository Unit of Work 模式避免新手被抽象层绕晕。真实项目可在此基础上按需扩展但本项目坚持“先跑通、再抽象”原则。2.2 Program.csMinimal Hosting 下的依赖注入与中间件注册Program.cs是整个项目的入口和配置中枢。它完成了三类关键注册数据库上下文注册使用AddDbContext并指定ServiceLifetime.ScopedJWT 认证服务注册AddAuthenticationAddJwtBearer密钥硬编码在Program.cs中生产环境应移至配置控制器与服务注册AddControllersAddEndpointsApiExplorerAddSwaggerGen含 JWT Bearer 支持// Program.cs 片段关键注册部分 var builder WebApplication.CreateBuilder(args); // 1. 数据库上下文注册连接字符串取自 appsettings.json 的 ConnectionStrings:DefaultConnection builder.Services.AddDbContextApplicationDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(DefaultConnection))); // 2. JWT 认证注册密钥为硬编码 32 字节 Base64 字符串仅演示 var key Encoding.ASCII.GetBytes(this-is-a-32-byte-secret-key-for-jwt); builder.Services.AddAuthentication(x { x.DefaultAuthenticateScheme JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(x { x.RequireHttpsMetadata false; // 开发环境关闭 HTTPS 强制 x.SaveToken true; x.TokenValidationParameters new TokenValidationParameters { ValidateIssuerSigningKey true, IssuerSigningKey new SymmetricSecurityKey(key), ValidateIssuer false, // 不校验签发方简化 ValidateAudience false, // 不校验接收方简化 ClockSkew TimeSpan.Zero // 关闭时间偏移容忍避免 token 看似过期 }; }); // 3. 控制器与 Swagger 注册 builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c { c.SwaggerDoc(v1, new() { Title Product API, Version v1 }); c.AddSecurityDefinition(Bearer, new() { Name Authorization, Type SecuritySchemeType.Http, Scheme bearer, BearerFormat JWT }); c.AddSecurityRequirement(new() { [Bearer] new[] { read } }); }); // 4. 业务服务注册Scoped 生命周期 builder.Services.AddScopedIProductService, ProductService(); var app builder.Build();这段代码的关键在于所有服务注册顺序不可颠倒。AddAuthentication必须在AddControllers之前AddDbContext必须在AddAuthentication之前因ApplicationDbContext可能被ProductService依赖而ProductService又被 Controller 依赖。若顺序错乱运行时会抛出InvalidOperationException: Unable to resolve service for type ApplicationDbContext——这是新手最常翻车的第一步。2.3 ApplicationDbContextEF Core 6 的轻量级数据上下文实现ApplicationDbContext.cs是 EF Core 与 SQL Server 交互的唯一通道。它做了三件事继承DbContext重写OnConfiguring仅用于开发环境 fallback正式环境由 DI 注入连接字符串定义DbSetProduct属性映射到数据库Products表重写OnModelCreating配置主键、非空约束等此处用 Fluent API比 Data Annotations 更灵活// Data/ApplicationDbContext.cs public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptionsApplicationDbContext options) : base(options) { } public DbSetProduct Products { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // 仅当 optionsBuilder 未被 DI 注入时才启用如 EF Core CLI 迁移命令 if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlServer(Server(localdb)\\mssqllocaldb;DatabaseProductDb;Trusted_Connectiontrue;); } } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityProduct(entity { entity.HasKey(e e.Id); // 主键 entity.Property(e e.Name).IsRequired().HasMaxLength(100); // 非空长度限制 entity.Property(e e.Price).HasColumnType(decimal(18,2)); // 精确小数类型 entity.Property(e e.CreatedAt).HasDefaultValueSql(GETDATE()); // SQL Server 默认值 }); } }注意OnConfiguring中的 fallback 连接字符串它指向本地 SQL Server Express LocalDB 实例数据库名为ProductDb。这意味着你无需手动建库——EF Core 迁移命令会自动创建。但若你本地没有安装 LocalDB或想连远程 SQL Server必须修改appsettings.json中的连接字符串并确保OnConfiguringfallback 不生效即 DI 正常注入。否则你会看到Cannot open database ProductDb错误而实际连接字符串早已配好——这是典型的 fallback 逻辑干扰主流程。2.4 SeedData应用启动时自动初始化测试数据SeedData.cs是一个实用技巧它在Program.cs中被显式调用确保每次启动 API 时数据库至少有 3 条商品数据可供测试。它不依赖IDesignTimeDbContextFactory而是直接使用ApplicationDbContext实例利用EnsureCreated()创建库表仅限开发再检查Products表是否为空为空则插入种子数据。// Data/SeedData.cs public static class SeedData { public static void Initialize(IServiceProvider serviceProvider) { using var context new ApplicationDbContext( serviceProvider.GetRequiredServiceDbContextOptionsApplicationDbContext()); // 确保数据库和表存在开发环境 context.Database.EnsureCreated(); // 若无数据则插入种子 if (!context.Products.Any()) { context.Products.AddRange( new Product { Name Laptop, Price 999.99m, CreatedAt DateTime.UtcNow }, new Product { Name Mouse, Price 29.99m, CreatedAt DateTime.UtcNow }, new Product { Name Keyboard, Price 129.99m, CreatedAt DateTime.UtcNow } ); context.SaveChanges(); } } }这段代码被嵌入Program.cs的app.Run()之前// Program.cs 末尾 using (var scope app.Services.CreateScope()) { var services scope.ServiceProvider; SeedData.Initialize(services); } app.Run();注意EnsureCreated()是“创库建表”而非“迁移”它不会生成.cs迁移文件也不支持增量更新。生产环境必须切换为MigrateAsync()dotnet ef migrations add Init流程。本项目用EnsureCreated()是为了零配置启动但你要清楚它的边界——它只适合原型验证绝不该出现在 CI/CD 流水线中。3. 运行前必检SQL Server 环境、连接字符串与 JWT 密钥配置3.1 本地 SQL Server 环境准备LocalDB 或 Express项目默认连接字符串指向(localdb)\mssqllocaldb这是 SQL Server Express LocalDB 的默认实例名。它无需独立安装 SQL Server只需安装 SQL Server Express 勾选 LocalDB 组件或 Visual Studio 附带的 LocalDB。验证 LocalDB 是否就绪# Windows PowerShell sqllocaldb info # 应输出类似MSSQLLocalDB sqllocaldb start MSSQLLocalDB若你使用 SQL Server Express实例名SQLEXPRESS或远程服务器请修改appsettings.json{ ConnectionStrings: { DefaultConnection: Serverlocalhost\\SQLEXPRESS;DatabaseProductDb;Trusted_Connectiontrue; } }提示Trusted_Connectiontrue表示 Windows 集成认证。若需 SQL Server 账户密码请改为Serverlocalhost\\SQLEXPRESS;DatabaseProductDb;User Idsa;Passwordyour_password;3.2 appsettings.json连接字符串与 JWT 参数的双重配置点appsettings.json是运行时唯一配置源包含两个关键 sectionConnectionStrings:DefaultConnection数据库连接字符串上节已说明Jwt:KeyJWT 签名密钥必须与Program.cs中硬编码的密钥一致{ Jwt: { Key: this-is-a-32-byte-secret-key-for-jwt }, ConnectionStrings: { DefaultConnection: Server(localdb)\\mssqllocaldb;DatabaseProductDb;Trusted_Connectiontrue; } }⚠️玄学时刻若你修改了Jwt:Key但忘了同步改Program.cs中的Encoding.ASCII.GetBytes(...)会出现System.IdentityModel.Tokens.Jwt.SecurityTokenInvalidSignatureException——token 解析失败但错误日志只显示“invalid signature”根本看不出是密钥不匹配。这是 JWT 开发中最隐蔽的血泪坑之一。3.3 生成并获取 JWT Token用 Swagger UI 手动触发登录项目未提供登录接口/auth/login但 Swagger UI 已预置了一个POST /api/products/login的模拟 endpoint实际由ProductsController的Login方法实现。这是为快速获取 token 设计的临时门。启动项目后访问https://localhost:5001/swagger找到Products标签下POST /api/products/login点击Try it out→Execute。响应体将返回一个 JWT token{ token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c }将此 token 复制粘贴到 Swagger 的Authorize按钮弹窗中格式为Bearer token注意Bearer后有一个空格。此后所有带[Authorize]的接口如GET /api/products都将携带该 token 发起请求。注意此Login方法是硬编码的用户名admin密码password仅用于演示。真实项目应对接Users表 密码哈希校验如BCrypt.Net。3.4 首次运行EF Core 迁移与数据库初始化全流程即使配置无误首次运行仍可能失败。以下是完整排错链路启动dotnet run控制台报错Cannot open database ProductDb→ 原因LocalDB 未启动或连接字符串指向不存在的实例。→ 解决运行sqllocaldb start MSSQLLocalDB或检查appsettings.json中Server后的实例名是否拼写正确。Swagger 页面空白F12 查看 Network 显示Failed to fetch→ 原因HTTPS 重定向未处理.NET 6 默认启用 HTTPS 重定向。→ 解决在Program.cs中注释掉app.UseHttpsRedirection();或为开发环境配置 HTTPS 开发证书dotnet dev-certs https --trust。GET /api/products返回 401 Unauthorized→ 原因未调用Login获取 token或Authorize按钮未正确填写Bearer token。→ 解决严格按 3.3 节操作注意大小写和空格。POST /api/products返回 500日志显示SqlException: Invalid object name Products→ 原因SeedData.Initialize()中的EnsureCreated()未执行如Program.cs中漏掉调用或数据库未真正创建。→ 解决手动执行迁移命令见下节或删除bin/obj/目录后重试。4. 常见问题排查5 条真实踩坑记录与根因修复4.1 现象dotnet run启动后立即崩溃控制台输出System.ArgumentNullException: Value cannot be null. (Parameter connectionString)原因appsettings.json中ConnectionStrings:DefaultConnection缺失或拼写错误如写成DefaultConnections少了个s导致builder.Configuration.GetConnectionString(DefaultConnection)返回nullUseSqlServer(null)抛异常。解决打开appsettings.json确认ConnectionStrings节点存在且DefaultConnection键名完全匹配用 VS Code 的 JSON 验证功能检查语法是否合法逗号、引号是否闭合。4.2 现象Swagger UI 能打开但GET /api/products返回空数组[]数据库中却有数据原因ProductsController的GetAllAsync方法使用了ToListAsync()但ApplicationDbContext的ProductsDbSet 未正确映射到物理表或表名不匹配EF Core 默认复数化但 SQL Server 表名为Product而非Products。解决检查ApplicationDbContext.OnModelCreating中是否显式设置了表名entity.ToTable(Products)或在Product.cs中添加[Table(Products)]特性。本项目已配置若你修改了实体名请同步更新此处。4.3 现象POST /api/products成功返回 201但数据库中无新增记录且SeedData也未执行原因Program.cs中SeedData.Initialize(services)被放在app.Use...中间件注册之后、app.Run()之前但app.Run()会阻塞线程导致Initialize从未执行。解决确认SeedData.Initialize(...)调用位于var app builder.Build();之后、app.Run();之前且不在任何app.Use...回调内。正确位置如下var app builder.Build(); // ✅ 正确在 Build 之后Run 之前 using (var scope app.Services.CreateScope()) { var services scope.ServiceProvider; SeedData.Initialize(services); } app.Run();4.4 现象PUT /api/products/1返回 400 Bad Request响应体为{type:https://tools.ietf.org/html/rfc7231#section-6.5.1,title:One or more validation errors occurred.,status:400,errors:{product.Id:[The Id field is required.]}原因Product实体类中Id属性被标记为[Required]但PUT请求的 JSON body 中未包含Id字段如{ Name: New Laptop, Price: 1099.99 }导致模型绑定失败。解决PUT是全量更新必须提交完整对象。Swagger 中PUT /api/products/{id}的product参数示例已包含Id请勿删除或修改Product.cs将Id的[Required]改为[Range(1, int.MaxValue)]更符合主键语义。4.5 现象DELETE /api/products/1返回 204 No Content但数据库中记录仍在原因ProductService.DeleteAsync(int id)方法中调用了context.Products.Remove(entity)但忘记调用context.SaveChangesAsync()。解决检查ProductService.cs中DeleteAsync方法确保有await context.SaveChangesAsync();。本项目已实现若你重构了 Service 层请逐行核对异步方法调用链——Remove只是标记SaveChangesAsync才真正提交。5. 二次开发实战从增字段到换数据库3 个可立即落地的改造方案5.1 方案一为 Product 表新增Category字段含数据库迁移这是最典型的业务迭代。步骤清晰无副作用修改实体类在Models/Product.cs中添加Category属性public class Product { public int Id { get; set; } [Required] public string Name { get; set; } string.Empty; public decimal Price { get; set; } public DateTime CreatedAt { get; set; } // ✅ 新增字段 [Required] [StringLength(50)] public string Category { get; set; } Uncategorized; }生成迁移文件在项目根目录执行dotnet ef migrations add AddCategoryToProduct --project .\YourProjectName.csproj注意--project参数必须指向.csproj文件否则 EF Core 找不到ApplicationDbContextFactory。更新数据库执行dotnet ef database update --project .\YourProjectName.csproj此时Products表将新增Category列NOT NULL默认值为Uncategorized。更新 API 契约ProductsController的Post和Put方法的 DTO若你已分离 DTO或直接Product参数将自动接受Category字段。Swagger 文档会实时刷新。从那以后我每次加字段都强制走一遍migrations add→database update→curl -X POST验证三步哪怕只是加个string Remark。因为跳过迁移直接改实体等于埋下“下次部署炸库”的后悔药。5.2 方案二将 SQL Server 切换为 SQLite轻量级替代方案当项目从内部工具转向跨平台桌面应用或需要免安装部署时SQLite 是更优选择。改造仅需 4 处修改位置原内容新内容说明Program.csoptions.UseSqlServer(...)options.UseSqlite(Data Sourceproduct.db)替换数据库提供程序appsettings.json删除ConnectionStrings:DefaultConnection新增ConnectionStrings: { DefaultConnection: Data Sourceproduct.db }连接字符串格式不同ApplicationDbContext.OnConfiguringUseSqlServer(...)fallbackUseSqlite(builder.Configuration.GetConnectionString(DefaultConnection))fallback 逻辑同步更新Product.csPrice的HasColumnType(decimal(18,2))删除该行SQLite 不支持decimalEF Core 自动映射为REAL执行dotnet run后项目根目录将生成product.db文件所有数据持久化其中。SQLite 无服务进程零配置完美适配便携场景。5.3 方案三JWT 密钥升级为配置驱动 签发方校验迈向生产就绪硬编码密钥是安全红线。升级分两步密钥外置在appsettings.json中增加Jwt:Issuer和Jwt:Audience并将Key改为 Base64 编码的随机字节数组32 字节Jwt: { Key: WbDqVpXrYtZv2a4c6e8g0i2k4m6o8q0s, // 32字符Base64 Issuer: https://mycompany.com, Audience: https://myapp.com }更新Program.cs认证配置var jwtSettings builder.Configuration.GetSection(Jwt); var key Encoding.ASCII.GetBytes(jwtSettings[Key]); builder.Services.AddAuthentication(x { /* ... */ }) .AddJwtBearer(x { // ... 其他配置 x.TokenValidationParameters new TokenValidationParameters { ValidateIssuerSigningKey true, IssuerSigningKey new SymmetricSecurityKey(key), ValidateIssuer true, // ✅ 启用签发方校验 ValidIssuer jwtSettings[Issuer], ValidateAudience true, // ✅ 启用接收方校验 ValidAudience jwtSettings[Audience], ClockSkew TimeSpan.Zero }; });此时Login方法生成 token 时必须传入issuer和audiencevar tokenDescriptor new SecurityTokenDescriptor { Subject new ClaimsIdentity(new[] { new Claim(id, 1) }), Expires DateTime.UtcNow.AddHours(1), SigningCredentials new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature), Issuer jwtSettings[Issuer], // ✅ Audience jwtSettings[Audience] // ✅ };从那以后我每次配置 JWT都强制用jwt.io解码调试 token亲眼确认iss和aud字段存在且匹配。因为线上环境一旦ValidateIssuerfalse等于把门锁换成贴纸。希望帮到你。本文还有配套的精品资源点击获取