简介本资源是一套基于.NET6构建的Web API实战示例面向具备一定C#基础、希望掌握ASP.NET Core Web API与数据库交互及身份验证的开发者。项目围绕SQL Server数据库展开增删改查操作并集成JWT实现无状态身份验证同时引入Swagger生成交互式接口文档便于理解与调试API。内容涵盖RESTful API设计原则、Entity Framework Core数据库迁移、全局异常处理与错误日志、SQL注入与XSS防护等安全实践还涉及依赖注入、中间件、异步流等.NET6新特性适合作为学习现代Web服务开发的完整参考。资源包共665个文件以dll动态库、cs源码、json配置、pdb调试符号及csproj项目文件为主压缩包约34.81MB目录结构清晰便于按模块查阅。目前已有1060人学习下载可帮助读者快速搭建可运行项目掌握从接口设计到安全部署的完整链路。1. 从零搭一套带鉴权的 .NET 6 WebAPISqlServer JWT 增删改查到底解决什么问题很多团队做后台管理系统时最头疼的不是业务逻辑有多复杂而是接口裸奔——谁都能调谁都能删。我见过一个内部工具上线三天因为没做鉴权测试环境的订单表被误删了一半。后来我们用 .NET 6 WebAPI 配合 SqlServer 和 JWT 重新搭了一套标准模板从登录发令牌到增删改查全链路打通才算把这类问题按住。这套方案的核心就三件事用 SqlServer 存业务数据用 JWT 做无状态身份验证用 WebAPI 暴露 RESTful 接口。适合谁适合正在从零搭建后端服务、或者想给现有接口补上鉴权层的开发者。下面我把整套流程拆开从建项目到跑通第一个受保护接口每一步都给出可复现的命令和代码。2. 环境准备与项目骨架把 SqlServer 和 JWT 依赖一次装对2.1 开发环境清单与版本选择理由在动手之前先把工具链定下来。.NET 6 是长期支持版本虽然现在有更新的版本但很多企业内网项目仍然锁在 6.0因为它的运行时稳定、依赖包兼容性好。SqlServer 我一般用 2019 或 2022 的开发者版本地开发足够不用纠结授权问题。IDE 用 Visual Studio 2022 或者 VS Code 加 C# 插件都行我习惯用 CLI 加 VS Code轻量且命令可追溯。需要安装的组件如下组件用途验证命令.NET 6 SDK编译和运行 WebAPIdotnet --version输出 6.xSqlServer 2019存储业务数据用 SSMS 或sqlcmd能连上EF Core 6 工具数据库迁移dotnet ef --versionPostman 或 curl接口测试能发 HTTP 请求安装 EF Core 工具的命令是dotnet tool install --global dotnet-ef --version 6.*这里锁 6 的大版本避免和 .NET 6 运行时产生兼容问题。SqlServer 的连接字符串后面会写在配置文件里本地默认实例一般是Serverlocalhost;DatabaseAppDb;Trusted_ConnectionTrue;如果用账号密码就换成User Idsa;Password你的密码;。2.2 创建项目并安装 NuGet 包打开终端执行下面这组命令。我习惯把解决方案和项目分开建方便后面加测试项目或类库。# 创建解决方案目录 mkdir JwtApiDemo cd JwtApiDemo # 创建 WebAPI 项目禁用 HTTPS 重定向以简化本地调试 dotnet new webapi -n JwtApiDemo.Api --no-https # 创建解决方案文件并添加项目 dotnet new sln -n JwtApiDemo dotnet sln add JwtApiDemo.Api/JwtApiDemo.Api.csproj # 安装 EF Core 和 SqlServer 驱动 dotnet add JwtApiDemo.Api package Microsoft.EntityFrameworkCore.SqlServer --version 6.0.25 dotnet add JwtApiDemo.Api package Microsoft.EntityFrameworkCore.Design --version 6.0.25 # 安装 JWT 认证包 dotnet add JwtApiDemo.Api package Microsoft.AspNetCore.Authentication.JwtBearer --version 6.0.25这里每个包都锁了 6.0.25是因为 .NET 6 的补丁版本之间偶尔会有 API 行为差异锁住小版本能保证你复现时和我这边一致。--no-https是为了本地用 HTTP 调试省去证书信任的麻烦生产环境肯定要开 HTTPS这个后面会提。装完后用dotnet build编译一下确认没有报错。如果提示找不到dotnet-ef检查一下全局工具路径有没有加到环境变量里。2.3 配置 SqlServer 连接串与 JWT 参数在appsettings.json里把数据库连接和 JWT 密钥写进去。注意 JWT 的密钥长度要够至少 32 个字符否则 HMAC-SHA256 签名会抛异常。{ ConnectionStrings: { DefaultConnection: Serverlocalhost;DatabaseJwtApiDemo;Trusted_ConnectionTrue;TrustServerCertificateTrue; }, Jwt: { Key: ThisIsASecretKeyForJwtTokenDemo123456, Issuer: JwtApiDemo, Audience: JwtApiDemoClient, ExpireMinutes: 60 }, Logging: { LogLevel: { Default: Information, Microsoft.AspNetCore: Warning } }, AllowedHosts: * }TrustServerCertificateTrue是本地开发时绕过证书验证的常见做法生产环境要换成正式证书。ExpireMinutes设 60 分钟太短会导致用户频繁重新登录太长则令牌泄露风险增大这个值可以根据业务敏感度调整。3. 数据模型与数据库迁移用 EF Core 把表建起来3.1 定义实体类和 DbContext先建一个Models文件夹放两个类User和Product。User 用来做登录验证Product 用来演示增删改查。// Models/User.cs namespace JwtApiDemo.Api.Models; public class User { public int Id { get; set; } public string Username { get; set; } string.Empty; public string PasswordHash { get; set; } string.Empty; public string Role { get; set; } User; } // Models/Product.cs namespace JwtApiDemo.Api.Models; public class Product { public int Id { get; set; } public string Name { get; set; } string.Empty; public decimal Price { get; set; } public int Stock { get; set; } }PasswordHash 存的是哈希后的密码绝对不能存明文。Role 字段用来做简单的角色控制后面 JWT 里会带上这个声明。接着建Data/AppDbContext.csusing JwtApiDemo.Api.Models; using Microsoft.EntityFrameworkCore; namespace JwtApiDemo.Api.Data; public class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } public DbSetUser Users SetUser(); public DbSetProduct Products SetProduct(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityUser() .HasIndex(u u.Username) .IsUnique(); modelBuilder.EntityProduct() .Property(p p.Price) .HasPrecision(18, 2); } }这里给 Username 加了唯一索引防止重复注册。Price 字段指定精度避免 SqlServer 默认的 decimal 精度不够导致金额计算误差。3.2 执行迁移并生成数据库在Program.cs里先注册 DbContext然后跑迁移命令。// Program.cs 片段 using JwtApiDemo.Api.Data; using Microsoft.EntityFrameworkCore; var builder WebApplication.CreateBuilder(args); builder.Services.AddDbContextAppDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(DefaultConnection))); builder.Services.AddControllers(); var app builder.Build(); app.MapControllers(); app.Run();然后在终端执行cd JwtApiDemo.Api dotnet ef migrations add InitialCreate dotnet ef database update第一条命令生成迁移文件第二条把变更应用到数据库。执行完后用 SSMS 连上JwtApiDemo库应该能看到Users和Products两张表。如果报连接错误检查 SqlServer 服务有没有启动以及连接串里的实例名对不对。3.3 种子数据插入一个测试用户为了后面能登录先手动插一个用户。密码用 BCrypt 哈希这里我直接用一段代码生成哈希值然后写进种子数据。// 在 AppDbContext 里重写 OnModelCreating 追加种子 modelBuilder.EntityUser().HasData(new User { Id 1, Username admin, PasswordHash $2a$11$Q9Z8Z8Z8Z8Z8Z8Z8Z8Z8Zu, // 实际值用 BCrypt 生成 Role Admin });更稳妥的做法是写一个初始化类在Program.cs启动时检查并插入。BCrypt 的哈希可以用BCrypt.Net-Next包生成命令是dotnet add package BCrypt.Net-Next --version 4.0.3。生成哈希的代码using BCrypt.Net; var hash BCrypt.HashPassword(Admin123); Console.WriteLine(hash);把输出的哈希串替换到种子数据里再跑一次dotnet ef database update。这样数据库里就有一个用户名为admin、密码为Admin123的账号。4. JWT 签发与验证把登录接口和鉴权中间件串起来4.1 编写 JWT 工具类建一个Services/JwtService.cs负责生成令牌。using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using JwtApiDemo.Api.Models; using Microsoft.IdentityModel.Tokens; namespace JwtApiDemo.Api.Services; public class JwtService { private readonly IConfiguration _config; public JwtService(IConfiguration config) { _config config; } public string GenerateToken(User user) { var key new SymmetricSecurityKey( Encoding.UTF8.GetBytes(_config[Jwt:Key]!)); var credentials new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var claims new[] { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.Username), new Claim(ClaimTypes.Role, user.Role) }; var token new JwtSecurityToken( issuer: _config[Jwt:Issuer], audience: _config[Jwt:Audience], claims: claims, expires: DateTime.UtcNow.AddMinutes( double.Parse(_config[Jwt:ExpireMinutes]!)), signingCredentials: credentials ); return new JwtSecurityTokenHandler().WriteToken(token); } }关键参数说明SymmetricSecurityKey用配置文件里的 Key 构造HmacSha256是对称签名算法性能好且实现简单。Claims 里放了用户 ID、用户名和角色后面控制器里可以直接读。过期时间从配置读方便统一调整。4.2 登录接口验证密码并返回令牌建Controllers/AuthController.csusing JwtApiDemo.Api.Data; using JwtApiDemo.Api.Dtos; using JwtApiDemo.Api.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace JwtApiDemo.Api.Controllers; [ApiController] [Route(api/[controller])] public class AuthController : ControllerBase { private readonly AppDbContext _db; private readonly JwtService _jwt; public AuthController(AppDbContext db, JwtService jwt) { _db db; _jwt jwt; } [HttpPost(login)] public async TaskIActionResult Login([FromBody] LoginRequest request) { var user await _db.Users .FirstOrDefaultAsync(u u.Username request.Username); if (user null || !BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash)) { return Unauthorized(new { message 用户名或密码错误 }); } var token _jwt.GenerateToken(user); return Ok(new { token }); } } public record LoginRequest(string Username, string Password);这里用BCrypt.Verify比对密码避免明文比较。返回的 JSON 里只放 token不暴露用户敏感信息。如果登录失败统一返回 401不区分是用户不存在还是密码错误防止账号枚举。4.3 注册 JWT 认证中间件回到Program.cs把认证和授权服务加进去using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.Text; var builder WebApplication.CreateBuilder(args); builder.Services.AddDbContextAppDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(DefaultConnection))); builder.Services.AddScopedJwtService(); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuer true, ValidateAudience true, ValidateLifetime true, ValidateIssuerSigningKey true, ValidIssuer builder.Configuration[Jwt:Issuer], ValidAudience builder.Configuration[Jwt:Audience], IssuerSigningKey new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration[Jwt:Key]!)) }; }); builder.Services.AddAuthorization(); builder.Services.AddControllers(); var app builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();注意UseAuthentication必须在UseAuthorization之前否则授权中间件拿不到用户身份。ValidateLifetime设为 true 会自动检查令牌过期不用手动写逻辑。5. 增删改查接口给每个操作加上角色和权限控制5.1 产品控制器五个标准动作建Controllers/ProductsController.cs实现 GET 列表、GET 单个、POST 新增、PUT 修改、DELETE 删除。using JwtApiDemo.Api.Data; using JwtApiDemo.Api.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace JwtApiDemo.Api.Controllers; [ApiController] [Route(api/[controller])] [Authorize] public class ProductsController : ControllerBase { private readonly AppDbContext _db; public ProductsController(AppDbContext db) { _db db; } [HttpGet] public async TaskIActionResult GetAll() { var products await _db.Products.ToListAsync(); return Ok(products); } [HttpGet({id})] public async TaskIActionResult GetById(int id) { var product await _db.Products.FindAsync(id); if (product null) return NotFound(); return Ok(product); } [HttpPost] [Authorize(Roles Admin)] public async TaskIActionResult Create([FromBody] Product product) { _db.Products.Add(product); await _db.SaveChangesAsync(); return CreatedAtAction(nameof(GetById), new { id product.Id }, product); } [HttpPut({id})] [Authorize(Roles Admin)] public async TaskIActionResult Update(int id, [FromBody] Product input) { var product await _db.Products.FindAsync(id); if (product null) return NotFound(); product.Name input.Name; product.Price input.Price; product.Stock input.Stock; await _db.SaveChangesAsync(); return NoContent(); } [HttpDelete({id})] [Authorize(Roles Admin)] public async TaskIActionResult Delete(int id) { var product await _db.Products.FindAsync(id); if (product null) return NotFound(); _db.Products.Remove(product); await _db.SaveChangesAsync(); return NoContent(); } }控制器级别加了[Authorize]所有接口都需要令牌。新增、修改、删除额外要求Admin角色查询接口只要登录就能访问。CreatedAtAction会返回 201 状态码和 Location 头符合 REST 规范。5.2 用 curl 跑通完整流程启动项目dotnet run。然后按顺序执行下面命令。# 1. 登录获取 token curl -X POST http://localhost:5000/api/auth/login \ -H Content-Type: application/json \ -d {username:admin,password:Admin123} # 假设返回 {token:eyJhbGci...}把它存到变量里 TOKENeyJhbGci... # 2. 查询列表带令牌 curl http://localhost:5000/api/products \ -H Authorization: Bearer $TOKEN # 3. 新增产品 curl -X POST http://localhost:5000/api/products \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d {name:测试商品,price:99.9,stock:10} # 4. 修改产品 curl -X PUT http://localhost:5000/api/products/1 \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d {name:修改后,price:88.8,stock:5} # 5. 删除产品 curl -X DELETE http://localhost:5000/api/products/1 \ -H Authorization: Bearer $TOKEN如果第 2 步返回 401检查令牌有没有过期、Bearer 后面有没有空格。如果第 3 步返回 403说明当前用户角色不是 Admin去数据库里把 Role 字段改成 Admin 再重新登录。5.3 参数校验与统一返回格式上面的代码直接用了实体类接收输入生产环境建议加 DTO 和验证特性。比如给 Product 加[Required]和[Range]using System.ComponentModel.DataAnnotations; public class ProductCreateDto { [Required(ErrorMessage 名称不能为空)] [StringLength(100)] public string Name { get; set; } string.Empty; [Range(0.01, 999999, ErrorMessage 价格必须在合理范围内)] public decimal Price { get; set; } [Range(0, int.MaxValue)] public int Stock { get; set; } }然后在控制器里用[FromBody] ProductCreateDto input模型验证会自动执行不通过时返回 400 和错误详情。统一返回格式可以用一个ApiResponseT包装但这不是必须的看团队规范。6. 避坑与排查JWT 和 SqlServer 联调时最容易翻车的五个点6.1 令牌过期时间设错导致刚登录就 401现象登录成功拿到 token立刻调其他接口却返回 401。原因ExpireMinutes配置读出来是字符串double.Parse在某些区域设置下会解析失败或者服务器时间用了本地时间而令牌用了 UTC。解决统一用DateTime.UtcNow并且在TokenValidationParameters里把ClockSkew设为TimeSpan.Zero默认的 5 分钟偏移会让人误以为没过期。6.2 SqlServer 连接串少了 TrustServerCertificate现象dotnet ef database update报证书链错误。原因本地 SqlServer 用了自签名证书.NET 6 默认验证。解决连接串加TrustServerCertificateTrue;生产环境换成受信任证书。6.3 迁移文件冲突导致数据库更新失败现象执行dotnet ef database update提示有多个迁移待应用或模型快照不一致。原因手动改了实体类但没重新生成迁移或者多人协作时迁移文件合并冲突。解决删掉Migrations文件夹重新dotnet ef migrations add但前提是数据库可以重建。生产环境要用dotnet ef migrations script生成 SQL 脚本人工审核。6.4 角色声明读不到导致 403现象用户明明是 Admin调新增接口还是 403。原因JWT 里的角色声明用了ClaimTypes.Role但验证时映射成了短名role两边不一致。解决在AddJwtBearer里加options.TokenValidationParameters.RoleClaimType ClaimTypes.Role;或者签发时直接用role作为 claim type。6.5 并发修改导致数据覆盖现象两个请求同时修改同一条产品记录后提交的覆盖了先提交的。原因没有做并发控制。解决给 Product 加一个RowVersion字段在OnModelCreating里配置IsRowVersion()EF Core 会在更新时自动加WHERE RowVersion 原值冲突时抛DbUpdateConcurrencyException捕获后返回 409。7. 进阶技巧把令牌刷新和接口版本控制加进来前面跑通的是最基础的版本实际项目里还有两件事值得做令牌刷新和接口版本控制。令牌刷新是为了让用户不用每小时重新登录做法是登录时同时返回 access token 和 refresh tokenrefresh token 存数据库并设更长的过期时间。当 access token 过期后客户端用 refresh token 换新的 access token。实现上可以加一个RefreshTokens表字段包括 Token、UserId、ExpireAt、IsRevoked。刷新接口验证 refresh token 有效且未撤销后签发新的 access token 并旋转 refresh token。接口版本控制用Asp.Versioning.Mvc包给控制器加[ApiVersion(1.0)]和路由api/v{version:apiVersion}/[controller]这样以后升级接口不影响老客户端。配置代码builder.Services.AddApiVersioning(options { options.DefaultApiVersion new ApiVersion(1, 0); options.AssumeDefaultVersionWhenUnspecified true; options.ReportApiVersions true; });验证方法很简单用同一个 token 分别调 v1 和 v2 的接口看路由是否正确匹配。我一般会在 Swagger 里把两个版本都列出来方便前端对照。最后说个血泪经验JWT 的 Key 千万不要提交到代码仓库。我见过有人把密钥硬编码在Program.cs里推到公开仓库结果被人拿去伪造令牌。正确做法是用环境变量或者用户机密dotnet user-secrets管理生产环境用密钥管理服务。这个习惯一旦养成能省掉很多后悔药。希望帮到你。本文还有配套的精品资源点击获取