news 2026/9/26 5:28:12

Sqlserver + JWT 认证的 .NET6 WebAPI 从零搭建实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Sqlserver + JWT 认证的 .NET6 WebAPI 从零搭建实战

简介:这是一套基于.NET 6的Web API实战示例,面向熟悉C#基础、希望掌握ASP.NET Core与SQL Server数据交互及JWT认证的初中级开发者。项目围绕增删改查接口展开,完整演示从数据库设计到Swagger接口文档、JWT登录鉴权、依赖注入与ORM持久化方案的落地过程。资源包共665个文件,包含项目源码(cs、csproj、sln)、编译产物(dll、pdb、exe)、配置与文档(json、txt、xml)等,压缩包约34.81MB,目录结构清晰,便于按模块对照学习。已有1060人学习下载。通过学习可以直观理解RESTful API设计、数据库迁移、异常处理与安全防护等关键点,适合毕业设计、企业级项目起步或.NET6新技术学习。

1. 从零搭一套带认证的 .NET6 WebAPI:说清 Sqlserver + JWT 这条链路的真实工程量

接手一个内部管理系统,需求很直白:前端要一套能登录、能增删改查的后端接口,数据库用 Sqlserver,认证用 JWT。听起来是 .NET 开发的基本功,但真把 .NET6 WebAPI + Sqlserver + JWT 从空项目跑到线上,中间涉及的连接配置、Token 有效期、EF Core 跟踪、IIS 发布,每一步都可能卡人。这套资源就是把这整条链路打成一个可复现的项目:数据库建表脚本、仓储层、JWT 签发与校验、控制器 CRUD 都在里面,照着配置就能跑。适合刚接触 .NET6 的转岗开发,也适合接了老系统维护但没从头建过认证接口的从业者。这篇文章会把每一步的参数和踩坑写透。

2. 先打数据地基:Sqlserver 连接字符串、EF Core 选型与仓储落点

2.1 为什么用 EF Core 而不是 SqlSugar

选数据访问层方案时,网上吵得最多的就是 EF Core 和 SqlSugar。这套资源里用的是 EF Core,理由不复杂:.NET6 WebAPI 项目模板自带 EF Core 支持,官方文档全,跟迁移工具配合得最好。SqlSugar 的语法更轻,但如果你要维护的是一个长期项目,EF Core 的 LINQ 表达式树在做复杂查询时更顺,而且微软的更新节奏稳定,踩坑之后能在 StackOverflow 上找到大量同款问题。

一个反直觉的点:EF Core 的性能并不差。很多人一听 ORM 就觉得慢,实际上对于增删改查这种场景,EF Core 的查询计划缓存做得很好,真正慢的往往是 N+1 查询,跟 ORM 本身没关系。把这个层面想明白,后面写仓储层的时候就不会为了“性能”去手写一堆 SqlConnection。

2.2 连接字符串与 appsettings.json 配置

新建 .NET6 WebAPI 项目后,先打开 appsettings.json,把连接字符串和 JWT 配置一次写全:

{ "ConnectionStrings": { "DefaultConnection": "Server=localhost;Database=Dotnet6Demo;User Id=sa;Password=你的密码;TrustServerCertificate=True;Encrypt=False" }, "Jwt": { "Issuer": "Dotnet6Demo.Server", "Audience": "Dotnet6Demo.Client", "SecretKey": "your-secret-key-here-must-be-long-enough-32bytes", "ExpireMinutes": 120, "RefreshExpireDays": 7 }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" }

连接字符串里的Encrypt=False和TrustServerCertificate=True是.SqlServer 2019/2022 连接时最容易踩的坑。新版 SqlClient 驱动默认强制加密,如果你本地的 Sqlserver 没配 SSL 证书,不加这两个参数就报证书链错误。

JWT 配置里SecretKey用来签名 Token,必须超过 32 字节,否则 HMAC-SHA256 算法直接拒绝。Issuer和Audience是 Token 的发行方和受众,前后端分离时前端拿到的 Token 里会带着这两个字段,方便调试时用 jwt.io 查看。

2.3 数据库建表与实体映射

新建 Sqlserver 数据库和一张核心表,在这套资源里领域的例子是“用户管理”。建表脚本直接跑在 Sqlserver Management Studio 里:

CREATE DATABASE Dotnet6Demo; GO USE Dotnet6Demo; GO CREATE TABLE [dbo].[Users] ( [Id] INT IDENTITY(1,1) NOT NULL PRIMARY KEY, [UserName] NVARCHAR(50) NOT NULL, [PasswordHash] NVARCHAR(200) NOT NULL, [Email] NVARCHAR(100) NULL, [CreatedAt] DATETIME2(3) NOT NULL DEFAULT GETDATE(), [UpdatedAt] DATETIME2(3) NULL );

PasswordHash字段长度给到 200,是因为后面做 BCrypt 哈希时,标准的哈希串有 60 个字符,但为了兼容未来的算法升级,预留足够的宽度。IDENTITY(1,1)是 Sqlserver 的自增主键,ID 从 1 开始,每次加 1。

实体类直接对应这张表:

public class User { public int Id { get; set; } public string UserName { get; set; } public string PasswordHash { get; set; } public string? Email { get; set; } public DateTime CreatedAt { get; set; } public DateTime? UpdatedAt { get; set; } }

DateTime?表示可空类型,对应数据库里的 NULL。EF Core 的约定大于配置,这个实体放在 Models 文件夹下,不用写注解也能通过DbContext的 DbSet 属性映射到 Users 表。

2.4 DbContext 与仓储接口的定义

创建 ApplicationDbContext:

public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<User> Users { get; set; } }

Program.cs 里注册:

builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( builder.Configuration.GetConnectionString("DefaultConnection")));

DbContextOptions<ApplicationDbContext>是构造器注入的关键。Program.cs 里的AddDbContext默认作用域为 Scoped,意思是每个 HTTP 请求周期内,同一个 DbContext 实例被复用,这能避免并发情况下上下文状态混乱。

仓储接口定义在这里要格外注意粒度。接口设计成四个方法足够用,配合这套资源的增删改查目标:

public interface IUserRepository { Task<User?> GetByIdAsync(int id); Task<List<User>> GetAllAsync(); Task AddAsync(User user); Task UpdateAsync(User user); Task DeleteAsync(User user); }

不把数据库上下文直接暴露给控制器,是这套资源用的分层方式。控制器只管接收参数、校验、返回状态码,数据怎么取怎么映射是仓储层的事。

入库时的外国人名带缩写或撇号(如 O'Brien、J. Smith),在连接字符串或 SQL 拼接上处理不当就会触发格式错误。,设计仓储基座时我给每个实体都配了一列显式 GUID 名,这样能彻底避免子系统对接时因名称语义撞车导致的删除错位。但接下来的关键问题是:让 JWT 不再是无状态纸片,而是每次请求可验证的真实身份。

3. JWT 认证链路:从 Token 结构到中间件校验的完整落地

3.1 JWT 的三段式结构与密钥配置

JWT Token 由 Header、Payload、Signature 三段组成,中间用点号分隔。Header 里声明算法,Payload 里放用户信息,Signature 由服务端密钥加工出来。这里的关键认知是:Payload 只是 Base64 编码,不是加密的,任何人拿到 Token 解出来就能看到用户名和过期时间。所以敏感数据别放里面。

在 Program.cs 里配置认证服务:

builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = 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:SecretKey"])) }; });

ValidateLifetime = true是默认行为,但我要强调一点:这个配置只管校验 Token 的 exp 字段是否过期,不管 Token 是否被撤销。如果用户在退出登录时前端只是丢弃 Token,这个 Token 在有效期内还是能用的。所以后面做注销接口时,不能只靠 JWT 本身,要加白名单或黑名单机制。

3.2 登录接口与 Token 签发

登录接口要做两件事:校验用户名和密码,签发 Token。密码匹配用的是 BCrypt:

[HttpPost("login")] public async Task<IActionResult> Login([FromBody] LoginRequest request) { var user = await _userRepository.GetByUserNameAsync(request.UserName); if (user == null || !BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash)) { return Unauthorized(new { message = "用户名或密码错误" }); } var token = GenerateToken(user); return Ok(new { token, expireAt = DateTime.UtcNow.AddMinutes(120) }); }

BCrypt.Verify的调用方式很直接:数据库里存的是哈希串,传入明文密码做比对。BCrypt 是故意慢的哈希算法,单次验证大约 100ms 左右,对暴力破解的抵抗性比 MD5 强得多。

GenerateToken 方法:

private string GenerateToken(User user) { var claims = new[] { new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), new Claim(JwtRegisteredClaimNames.Name, user.UserName), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:SecretKey"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: _config["Jwt:Issuer"], audience: _config["Jwt:Audience"], claims: claims, expires: DateTime.Now.AddMinutes(120), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token); }

Jti是一个随机 ID,用来唯一标识这个 Token。如果以后要做 Token 撤销功能,jti 就是黑名单的索引。expires用的时间是 120 分钟,和配置里的ExpireMinutes对齐。

3.3 中间件校验顺序与自定义策略

Program.cs 里中间件的注册顺序是很多人的知识盲区,包括我自己也在这上面吃过亏。正确顺序是:

app.UseAuthentication(); app.UseAuthorization();

这两行必须在 UseRouting 之后、UseEndpoints 之前。如果反了,认证中间件不会拦截任何请求,而且不会报错,只是所有接口都匿名可以访问。这种静默失败最坑。

还要注意:在 .NET6 的 Minimal API 里,app.MapControllers 的位置也有讲究。常规顺序是先用 app.MapControllers() 再跑 UseAuthentication?实际正确姿势是 UseAuthentication 和 UseAuthorization 必须在 MapControllers 之前。这个错误不会触发任何编译警告,项目照样启动,直到你用 Postman 测登录保护接口时才发现所有人都能进。

增删改查的控制器并不能幸免于认证中间件的配置题。接下来进入接口层,看看 DTO 的正确写法。

4. 增删改查实战:DTO、控制器与统一返回格式的一次性写对

4.1 为什么需要 DTO 而不是直接暴露实体

很多新手图省事,直接把 User 实体放在控制器里接收参数。这样做会在两个问题上翻车:一是前端传了不属于实体的字段,比如把 Id 传进来导致 EF Core 报并发冲突;二是 PasswordHash 字段被序列化返回给前端,登录凭据直接泄露。DTO 在这套资源里的定位就是隔离层。

以创建用户为例,接收参数的 DTO:

public class CreateUserRequest { [Required(ErrorMessage = "用户名不能为空")] [StringLength(50, MinimumLength = 3)] public string UserName { get; set; } [Required(ErrorMessage = "密码不能为空")] [StringLength(100, MinimumLength = 6)] public string Password { get; set; } [EmailAddress] public string? Email { get; set; } }

返回给前端的 DTO 则不带密码字段,避免敏感数据出网。多了一个[ApiController]属性,模型验证失败会自动返回 400 响应,不用手动写唇舌校验逻辑。

4.2 控制器里 CRUD 的四个标准动作

在 UsersController 里,增删改查四个方法要按 RESTful 风格写:

[HttpGet] public async Task<ActionResult<List<UserDto>>> GetAll() { var users = await _userRepository.GetAllAsync(); var result = users.Select(u => new UserDto { Id = u.Id, UserName = u.UserName, Email = u.Email, CreatedAt = u.CreatedAt }).ToList(); return Ok(result); } [HttpGet("{id:int}")] public async Task<ActionResult<UserDto>> GetById(int id) { var user = await _userRepository.GetByIdAsync(id); if (user == null) { return NotFound(new { message = "用户不存在" }); } return Ok(new UserDto { Id = user.Id, UserName = user.UserName }); } [HttpPost] public async Task<ActionResult<UserDto>> Create([FromBody] CreateUserRequest request) { var existing = await _userRepository.GetByUserNameAsync(request.UserName); if (existing != null) { return Conflict(new { message = "用户名已存在" }); } var user = new User { UserName = request.UserName, PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.Password), Email = request.Email, CreatedAt = DateTime.UtcNow }; await _userRepository.AddAsync(user); return CreatedAtAction(nameof(GetById), new { id = user.Id }, user); } [HttpPut("{id:int}")] public async Task<IActionResult> Update(int id, [FromBody] UpdateUserRequest request) { if (id != request.Id) { return BadRequest(new { message = "路径 ID 与请求体 ID 不一致" }); } var user = await _userRepository.GetByIdAsync(id); if (user == null) { return NotFound(new { message = "用户不存在" }); } user.Email = request.Email; user.UpdatedAt = DateTime.UtcNow; await _userRepository.UpdateAsync(user); return NoContent(); } [HttpDelete("{id:int}")] public async Task<IActionResult> Delete(int id) { var user = await _userRepository.GetByIdAsync(id); if (user == null) { return NotFound(new { message = "用户不存在" }); } await _userRepository.DeleteAsync(user); return NoContent(); }

注意事项有几个挡住新手的问题。第一,[HttpPut]不是只管传参,路径里的 id 和请求体里的 Id 必须一致,这一步拦截在后端的防错意义上非常有价值。第二,CreatedAtAction返回的 201 状态码,Location 头指向 GetById 路由,这是在告诉前端资源已经创建成功,在哪里能取到这个资源。第三,删除成功后返回 204 NoContent,别返回 200 加空 JSON,RESTful 语义对不上。

4.3 并发冲突的响应处理

EF Core 在并发更新时默认有乐观并发控制,需要在实体里加一个字段配合。更常见的做法是在 UpdateAsync 里捕获 DbUpdateConcurrencyException:

public async Task UpdateAsync(User user) { try { _context.Users.Update(user); await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { throw new InvalidOperationException("数据已被其他请求修改,请刷新后重试"); } }

_context.Users.Update(user)会把整个实体标记为 Modified,所有列都会更新。如果你只想更新个别字段,应该重新加载实体后逐个赋值,否则前端传入的 NULL 可能会覆盖数据库里有值的字段。这一点在踩坑章还会展开。

但除了上述设计期的取舍,运行期的故障往往更让人措手不及。Sqlserver 与 JWT 的组合在真实环境里有一批高频坑,值得专门梳理。

5. 避坑指南:Sqlserver 与 JWT 场景下五个高频翻车点

5.1 字符串转数字的隐式转换问题

现象:在 Sqlserver 里执行带字符串参数的查询时,报错提示类型转换失败。

原因:EF Core 生成的 SQL 会把字符串参数直接传给数据库,如果字段类型是 INT,而传入的值是空字符串或非数字内容,Sqlserver 无法完成隐式转换。

解决:在进仓储层之前做类型校验,不要依赖数据库兜底。特别是查询接口,前端传pageIndex=abc时要保证模型验证先把这道门拦住。

5.2 JWT 密钥太短导致启动即报错

现象:项目启动时抛出IDX10720: Unable to create KeyedHashAlgorithm异常。

原因:JWT 配置里的SecretKey长度小于 32 字节,HMAC-SHA256 的密钥长度必须达标。把 SecretKey 设为 16 字节也不行,那是 MD5 的强度。

解决:用一段至少 32 字符的随机字符串做密钥。建议用 PowerShell 生成;在密钥管理上,把它放到环境变量或用户机密里,别直接写进 appsettings.json 提交到代码仓库。

5.3 EF Core 更新操作覆盖了未被修改的字段

现象:用户在编辑界面只改了 Email,保存后 UserName 变成了空值。

原因:控制器里_context.Users.Update(user)将状态一次性标为 Modified,SQL 生成的 UPDATE 语句包含所有列,而 DTO 没有接收 UserName 字段,导致空值覆盖。

解决:先按 ID 重新从数据库加载实体,再只更新 DTO 里明确出现的字段。这个方案会多查一次数据库,但能避免脏写。

5.4 Sqlserver 事务日志暴涨

现象:数据库运行一段时间后,日志文件膨胀到几十 GB,磁盘被耗尽。

原因:数据库处于简单恢复模式还好,完整恢复模式下,超过一定规模,日志不会自动截断。系统频繁的增删改操作都会写日志,日志文件只增不减。

解决:定期做差异备份,备份之后日志点会被截断。把数据库恢复到简单模式是一种即时止血方案,适合测试环境;生产交接时,我也会在恢复模型和日志备份策略上留下清晰的备注,避免后来的人对着膨胀日志无从下手。

5.5 JWT 中间件不生效,接口全部匿名可访问

现象:加了[Authorize]特性后,未登录仍能正常调用接口,也不报错。

原因:Program.cs 里中间件注册顺序错误。很多教程只写 AddAuthentication 然后忘了 UseAuthentication,或把 UseAuthentication 写在 MapControllers 之后,JWT 的 Bearer Handler 从未被执行。

解决:检查中间件注册顺序,UseAuthentication 必须出现在 UseRouting 之后、MapControllers 之前。我一般会在 Program.cs 里加注释标明顺序。调试时看响应头里有没有WWW-Authenticate: Bearer,没有就是没套上认证中间件。

排查边界问题,连不上是批次里最常见也最没头绪的一条。但是调试链路走通后,发布与部署才是工程闭环的最后一公里。

6. 发布与自检:用 Postman 走通认证流程,再上 IIS 部署的收尾细节

6.1 Postman 里建立环境变量,完整走一遍认证+CRUD

调试接口时,在 Postman 里新建环境变量,把请求地址和 Token 串起来。登录成功后,在 Tests 脚本里自动写入 Token:

const json = pm.response.json(); pm.collectionVariables.set("token", json.token);

这样后续的增删改查请求,只需在 Authorization 标签里选择 Bearer Token 类型,值填{{token}}即可。如果 Token 过期,401 响应会提醒重新登录。这条链路从头跑到尾,大约 3 分钟能确认接口层没有问题。

我习惯给 Postman 收藏集加一个环境文件夹,分成本地、测试、线上三套环境,环境切换时只需在右上角下拉切换,不用改任何一个 URL。生产环境的环境变量里绝不放密钥,只放连接字符串和内存不落盘的临时凭据。

6.2 发布到 IIS:runtime 版本和宿主机权限

发布配置用框架依赖模式省体积,但目标服务器必须安装对应的 ASP.NET Core Runtime 和 Hosting Bundle。很多部署翻车都发生在这一步:服务器只装了 .NET Runtime 没有装 Hosting Bundle,IIS 反向代理标志没装上,请求进到 IIS 就变成 502。

发布流程是这样的:VS 里右键项目发布,目标选“文件夹”,用 Release 配置生成。把发布出的文件拷到服务器上,在 IIS 里新建应用程序池,.NET CLR 版本选“无托管代码”。站点物理路径指到发布文件夹,绑定端口后重启站点。

6.3 验证与收尾习惯

部署完成后,用 Postman 跑一条全链路脚本:登录拿 Token、创建用户、查询用户、更新用户、删除用户。确认 Token 过期后接口正确返回 401,再检查 Sqlserver 里的 Users 表数据与数据库操作后的预期一致。

从那以后我每次交付 .NET6 WebAPI 项目,都强制走一遍这个流程:本机跑通、Postman 自动化验证、IIS 空应用程序池部署、日志告警检查。这套资源从建表到 JWT 再到 CRUD 就是一个完整闭环,关键是每个环节的配置都有据可查。把坑提前踩完,省下来的时间足够你多看几个需求文档。希望这篇笔记帮到你落地这套链路。

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

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

C++20模块接口设计:从最小导出到实践落地

1. 模块接口设计到底在解决什么问题干过几年 C 的人都有一种共同的感受&#xff1a;真正让人崩溃的往往不是语法&#xff0c;而是一个项目里的模块接口设计。类写得很漂亮&#xff0c;算法实现得很精巧&#xff0c;但只要接口设计得乱&#xff0c;后面接手的同事一定会骂人&…

作者头像 李华
网站建设 2026/9/26 5:26:02

高校电动车租赁系统:SpringBoot+Vue+MySQL全栈毕设实战

每年到了毕业设计季&#xff0c;总能看到大量同学在“电动车租赁系统”、“共享单车系统”、“校园二手交易平台”这类题目之间反复横跳。这题目看着平淡无奇&#xff0c;但真上手去做&#xff0c;从技术选型、数据库设计到联调部署&#xff0c;每一步都藏着不少门道。这篇博文…

作者头像 李华
网站建设 2026/9/26 5:25:41

Claude Code 多 API 节点切换:环境变量与配置加载机制全解析

如果你经常用 Claude Code 写项目&#xff0c;大概率经历过这种崩溃瞬间&#xff1a;上午还在用官方模型调架构&#xff0c;下午想切到 DeepSeek 跑一轮批量重构&#xff0c;晚上又得换另一个服务商的 API 做测试。这时候如果还靠手动改ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY&…

作者头像 李华
网站建设 2026/9/26 5:25:17

5G时间同步仿真源码解析:PTP/gPTP协议、OMNeT++建模与避坑指南

简介&#xff1a;一套完整的5G通信系统时间同步仿真源码&#xff0c;面向移动通信研究人员、算法工程师及高年级通信专业学生&#xff0c;用于解决5G网络中小区间同步、终端与基站同步及核心网时钟同步等核心问题&#xff0c;可作为物理层学习、算法验证与性能优化的参考工具。…

作者头像 李华
网站建设 2026/9/26 5:24:29

从《鬼谷子》“养志法灵龟”看现代表情管理与情绪控制

1. 从“灵龟”说起&#xff1a;为什么养志要和表情管理挂钩我第一次读到《本经阴符七术》里“养志法灵龟”这五个字时&#xff0c;第一反应是愣住。龟&#xff0c;在传统文化里从来不是“快”的象征&#xff0c;更和“表情”八竿子打不着。但后来真正琢磨进去&#xff0c;才发现…

作者头像 李华
网站建设 2026/9/26 5:23:45

Go TCP编程中的handle:句柄与处理函数的双重身份

刚开始学 Go 的 TCP 编程时&#xff0c;我几乎每一篇教程里都会碰到一个词&#xff1a;handle。标准库里有个http.Handle&#xff0c;论坛代码里总写handleConn&#xff0c;有一次编译还报出invalid gc handle。一个词横跨了操作系统、运行时、标准库和业务代码&#xff0c;绕都…

作者头像 李华