ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

.NET6+SqlServer+JWT:WebAPI增删改查与Token鉴权实战指南

.NET6+SqlServer+JWT:WebAPI增删改查与Token鉴权实战指南 简介这是面向.NET开发者的Web API实战项目基于.NET6与ASP.NET Core构建演示SQL Server下的CRUD操作及JWT身份验证流程适合初学或进阶掌握RESTful API、Swagger文档、ORM与安全认证的开发者。包内含665个文件压缩包34.81MB主干为cs源码、csproj工程文件与sln解决方案同时包含dll/pdb依赖程序集、json配置文件、editorconfig规则及数据库备份.bak涵盖DapperCore、SqlSugar等多种数据访问方式。已有1060人学习查看。通过该项目可完整了解分层架构BLL、Service、Model下的接口设计、JWT令牌签发与鉴权、数据库迁移与异常处理思路还可参考Swagger测试API、网络安全防护及CI/CD相关配置适合作为快速上手的完整示例。1. 为什么是.NET6SqlserverJWT这套CRUD组合到底解决了什么年初给公司做内部工单系统第一版就三个接口登录、查工单、提交工单。技术选型没犹豫直接选了.NET6 WebAPI Sqlserver JWT一干就是大半年。今天回头聊这个组合不是为了推荐什么新框架而是这套增删改查登录令牌几乎是每个业务系统的地基里面的配置坑远比想象中多。适合谁从传统ASP.NET转过来的人、要在老项目里新开接口的人、以及想把能跑但不敢维护的接口重写一遍的团队。这套方案不花哨但胜在每条路都有人踩过出了错你能查到答案。2. 搭建项目骨架从空目录到第一个接口跑通的四步2.1 开发环境与版本选型SDK、数据库与SSMS该装哪一版先统一环境版本不一致是第一个翻车点。我用的是.NET 6 SDKLTS版本数据库用Sqlserver 2019 Developer图形工具用SSMS 18/19都可以。有人问为什么不直接上.NET 8——如果你的服务器是Windows Server 2019、运维只给装了.NET 6 Runtime那你写.NET 8代码根本发布不上去。选LTS版本不是保守是给部署留余地。Sqlserver安装这块新手最容易漏的是选择功能那一步一定要勾上数据库引擎服务和客户端工具连接。只装SSMS不等于装了数据库引擎这是最常见的误会。装完开发版后我用SQL Server 配置管理器确认一下TCP/IP协议是否启用这一步网上教程很少提但几乎决定了后面所有连接是否顺畅。2.2 创建WebAPI项目dotnet CLI一行命令搞定我习惯用命令行创建项目干净而且可重复dotnet new webapi -n Demo.Api --framework net6.0 cd Demo.Api code .-n指定项目名称--framework锁定目标框架为net6.0。如果你用的是Visual Studio 2022创建时选ASP.NET Core Web API模板也能得到同样的骨架但注意模板里默认带了一个WeatherForecastController后面要删掉不然发布后会多出几个无用接口。项目骨架里真正需要关注的只有三个文件Program.cs程序入口和依赖注入配置、appsettings.json连接字符串、JWT密钥等配置、Controllers目录放接口。其余都是编译产物和无关模板不用管。2.3 引入NuGet包EF Core、Sqlserver驱动与JWT Bearer单靠默认模板不能连数据库、也不能发JWT令牌需要手动加三个包。我直接敲命令dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 6.0.28 dotnet add package Microsoft.EntityFrameworkCore.Tools --version 6.0.28 dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 6.0.28EntityFrameworkCore.SqlServer是EF Core操作Sqlserver的驱动Tools提供迁移命令后面执行dotnet ef指令必须靠它JwtBearer是.NET官方对JWT认证的服务端实现。版本号必须锁定在6.0.x不能顺手装8.x否则程序集加载会直接报错。2.4 配置文件里的连接字符串与JWT密钥设计appsettings.json是最容易随手乱写、然后又花两个小时排查的配置文件。我一般这么写{ ConnectionStrings: { Default: Serverlocalhost\\SQLEXPRESS;DatabaseDemoDb;User Idsa;Password你的密码;TrustServerCertificateTrue; }, Jwt: { Key: Pssw0rdDemoKey-2024-HasAtLeast32Characters!, Issuer: Demo.Api, Audience: Demo.Client, ExpireMinutes: 120 }, Logging: { LogLevel: { Default: Information } } }连接字符串里几个参数容易踩坑Serverlocalhost\SQLEXPRESS表示本机默认实例名如果安装时改过实例名这里必须跟着改User Idsa用的是Sqlserver账号密码登录比Windows身份验证省去IIS权限问题TrustServerCertificateTrue必须加否则新版驱动访问自签名证书会中断连接。JWT的Key是签名密钥至少要32个字符。如果你的环境不允许明文存密码可以用环境变量覆盖但开发阶段直接写在配置里最省事。Issuer和Audience分别代表令牌签发方和接收方后面配置鉴权时要用。3. 数据层与增删改查一张用户表从建表到入库的完整链路3.1 设计实体类与DbContext先写模型再生成表先写实体类再让EF Core帮你生成数据库表这是EF Core的工作方式和传统先建表再写ADO.NET正好反过来。以用户表为例using System.ComponentModel.DataAnnotations; namespace Demo.Api.Models { public class User { [Key] public int Id { get; set; } [Required, MaxLength(50)] public string Username { get; set; } string.Empty; [Required, MaxLength(200)] public string PasswordHash { get; set; } string.Empty; [MaxLength(50)] public string Nickname { get; set; } string.Empty; public DateTime CreatedAt { get; set; } DateTime.Now; } }[Key]标注主键[Required]映射成非空列[MaxLength(50)]控制字符串列长度。PasswordHash字段设计为200位是因为哈希字符串通常很长不要用50位然后发现入库报错。接下来写DbContextusing Microsoft.EntityFrameworkCore; using Demo.Api.Models; namespace Demo.Api.Data { public class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } public DbSetUser Users { get; set; } } }DbSetUser是EF Core对用户表的操作入口后续增删改查都从这里走。如果你还有订单、工单等表每个实体类加一个DbSet即可。3.2 执行迁移EF Core从实体类生成Sqlserver表结构的命令实体类和DbContext写完还不能直接连数据库需要先执行迁移。打开终端在项目根目录运行dotnet ef migrations add InitUserTable dotnet ef database update第一条命令在项目里生成一个Migrations目录记录表结构的变更历史第二条命令真正把表创建到Sqlserver里。如果你运行第一条命令时报错No database provider has been configured说明Program.cs里还没注册DbContext回到2.4补上builder.Services.AddDbContextAppDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(Default)));这段代码把连接字符串传给EF CoreUseSqlServer是驱动入口。执行完database update后用SSMS刷新数据库列表会看到DemoDb库里出现Users表同时EF Core还会自动创建__EFMigrationsHistory表它是版本的记录表千万不要手动删。3.3 泛型仓储基类把增删改查封装成一套模板每个实体都要写五个增删改查方法太重复了我用泛型基类解决。先定义接口public interface IRepositoryT where T : class { TaskListT GetAllAsync(); TaskT? GetByIdAsync(int id); TaskT AddAsync(T entity); TaskT UpdateAsync(T entity); Taskbool DeleteAsync(int id); }接口约束where T : class保证泛型参数是引用类型T?表示返回值可能为空。实现类public class RepositoryT : IRepositoryT where T : class { private readonly AppDbContext _db; public Repository(AppDbContext db) { _db db; } public async TaskListT GetAllAsync() { return await _db.SetT().ToListAsync(); } public async TaskT? GetByIdAsync(int id) { return await _db.SetT().FindAsync(id); } public async TaskT AddAsync(T entity) { _db.SetT().Add(entity); await _db.SaveChangesAsync(); return entity; } public async TaskT UpdateAsync(T entity) { _db.SetT().Update(entity); await _db.SaveChangesAsync(); return entity; } public async Taskbool DeleteAsync(int id) { var entity await _db.SetT().FindAsync(id); if (entity null) return false; _db.SetT().Remove(entity); await _db.SaveChangesAsync(); return true; } }这里的关键是_db.SetT()EF Core会根据泛型类型自动找到对应的DbSet。SaveChangesAsync是EF Core将内存修改同步到数据库的唯一入口忘记调用会导致所有操作看似成功、实际没入库。在Program.cs注册泛型仓储builder.Services.AddScoped(typeof(IRepository), typeof(Repository));AddScoped表示每个请求作用域内复用同一个实例避免并发时DbContext冲突。3.4 控制器落地增删改查的五个HTTP端点怎么组织有了仓储基类控制器就清爽了。以用户管理接口为例[ApiController] [Route(api/users)] public class UsersController : ControllerBase { private readonly IRepositoryUser _repo; public UsersController(IRepositoryUser repo) { _repo repo; } [HttpGet] public async TaskIActionResult GetAll() { return Ok(await _repo.GetAllAsync()); } [HttpGet({id})] public async TaskIActionResult GetById(int id) { var user await _repo.GetByIdAsync(id); return user null ? NotFound() : Ok(user); } [HttpPost] public async TaskIActionResult Create(CreateUserDto dto) { var entity new User { Username dto.Username, PasswordHash dto.Password, Nickname dto.Nickname }; var created await _repo.AddAsync(entity); return CreatedAtAction(nameof(GetById), new { id created.Id }, created); } [HttpPut({id})] public async TaskIActionResult Update(int id, UpdateUserDto dto) { var user await _repo.GetByIdAsync(id); if (user null) return NotFound(); user.Nickname dto.Nickname; await _repo.UpdateAsync(user); return NoContent(); } [HttpDelete({id})] public async TaskIActionResult Delete(int id) { var deleted await _repo.DeleteAsync(id); return deleted ? NoContent() : NotFound(); } }[Route(api/users)]定义路由前缀[HttpGet({id})]这种写法把URL参数绑定到方法参数。Ok()、NotFound()、NoContent()是ControllerBase内置的响应包装方法分别对应200、404、204状态码。目前这个接口还没有任何权限控制任何人调一下GET /api/users就能看到全部用户数据下一章就把它锁起来。4. JWT登录取证登录一次后续请求全部免认证4.1 JWT的结构与签发逻辑Header、Payload、Signature不再玄学JWTJSON Web Token是一串由三个点分隔的字符串形如xxxxx.yyyyy.zzzzz。第一部分Header声明加密算法第二部分Payload携带用户声明Claims第三部分Signature是前两段内容的签名。服务端用同一个密钥对令牌签名客户端每次请求带上令牌服务端验证签名即可确认令牌未被篡改这就是无状态认证的核心服务端不用存Session扩展时天然适合多实例部署。签名算法我用HS256这类对称加密算法加密和解密用同一个密钥实现最简单。你只要保证密钥不泄露安全性就掌握在自己手里。关于JWT漏洞的讨论网上很多但绝大多数漏洞来自密钥太短、算法降级、过期时间太长这三个错误而不是JWT本身有问题。4.2 登录接口实现查用户表、验密码、发令牌先写一个独立的TokenServiceusing System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Microsoft.IdentityModel.Tokens; using Demo.Api.Models; namespace Demo.Api.Services { public class TokenService { private readonly IConfiguration _config; public TokenService(IConfiguration config) { _config config; } public string CreateToken(User user) { var claims new ListClaim { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.Username), new Claim(ClaimTypes.Role, User) }; var key new SymmetricSecurityKey( Encoding.UTF8.GetBytes(_config[Jwt:Key])); 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( Convert.ToDouble(_config[Jwt:ExpireMinutes])), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token); } } }Claim是JWT对用户身份信息的抽象把用户Id、用户名、角色放进令牌里接口层就能直接读出来。SymmetricSecurityKey的入参是字节数组所以用Encoding.UTF8.GetBytes把配置里的字符串转成字节。expires控制令牌过期时间这里从配置读取便于后续调整。登录接口写在AuthController里[ApiController] [Route(api/auth)] public class AuthController : ControllerBase { private readonly IRepositoryUser _userRepo; private readonly TokenService _tokenService; public AuthController(IRepositoryUser userRepo, TokenService tokenService) { _userRepo userRepo; _tokenService tokenService; } [HttpPost(login)] public async TaskIActionResult Login(LoginDto dto) { var users await _userRepo.GetAllAsync(); var user users.FirstOrDefault(x x.Username dto.Username); if (user null || !VerifyPassword(dto.Password, user.PasswordHash)) return Unauthorized(new { message 用户名或密码错误 }); var token _tokenService.CreateToken(user); return Ok(new { token, user.Nickname }); } private bool VerifyPassword(string input, string hash) { // 演示用简单Hash对比生产环境建议用BCrypt或ASP.NET Core PasswordHasher var sha System.Security.Cryptography.SHA256.Create(); var bytes System.Text.Encoding.UTF8.GetBytes(input); var computed Convert.ToHexString(sha.ComputeHash(bytes)); return computed.Equals(hash, StringComparison.OrdinalIgnoreCase); } }这里GetAllAsync查出所有用户再匹配是偷懒写法数据量大了应该用表达式树查询后面进阶章再提。Unauthorized返回401状态码前端可以根据这个状态跳转登录页。4.3 鉴权三件套配置AddAuthentication、UseAuthentication、[Authorize]生成令牌只是第一步关键是让每个受保护的接口自动验证令牌。在Program.cs里补上如下配置var key Encoding.UTF8.GetBytes(builder.Configuration[Jwt:Key]); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuer true, ValidIssuer builder.Configuration[Jwt:Issuer], ValidateAudience true, ValidAudience builder.Configuration[Jwt:Audience], ValidateIssuerSigningKey true, IssuerSigningKey new SymmetricSecurityKey(key), ValidateLifetime true, ClockSkew TimeSpan.FromMinutes(1) }; }); builder.Services.AddAuthorization();AddAuthentication注册认证服务JwtBearerDefaults.AuthenticationScheme指明使用Bearer Token方式AddJwtBearer内部配置验证参数六个Validate开关分别校验Issuer、Audience、签名密钥、签发时间、过期时间和时钟偏移。ClockSkew是服务端和客户端时钟差的容忍值默认5分钟我习惯压到1分钟减少令牌实际有效期被拉长的问题。然后修改Program.cs的管道顺序var app builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();注意UseAuthentication必须在UseAuthorization之前顺序反了会得到404而不是401。最后在UsersController类上加上[Authorize]特性没有令牌的请求统统被挡在门外。4.4 从令牌里读当前用户自定义Claims的三种取法接口需要知道当前登录的人是谁用HttpContext.User获取[Authorize] [HttpGet(me)] public IActionResult GetMe() { var userId User.FindFirst(ClaimTypes.NameIdentifier)?.Value; var username User.Identity?.Name; var role User.FindFirst(ClaimTypes.Role)?.Value; return Ok(new { userId, username, role }); }FindFirst(ClaimTypes.NameIdentifier)取用户IdUser.Identity.Name取用户名虽然Data里存的是用户Id在NameIdentifier这种映射关系但EF Core和JWT标准已经帮你把字符串和类型对齐了。实际项目中我还会把用户Id封装成一个扩展方法public static class ClaimsPrincipalExtensions { public static int GetUserId(this ClaimsPrincipal user) { var value user.FindFirst(ClaimTypes.NameIdentifier)?.Value; return int.TryParse(value, out var id) ? id : 0; } }这样控制器里直接写User.GetUserId()读到的就是当前登录用户的整型ID。5. 避坑指南Sqlserver连接失败、JWT解析翻车与EF Core迁移报错的排查5.1 连接失败Sqlserver的TCP/IP协议没启用现象项目启动后运行到第一个数据库操作报错A network-related or instance-specific error occurred while establishing a connection to SQL Server。不是密码错不是数据库名错是压根没连上。原因Sqlserver安装时默认可能只开了Named Pipes协议而客户端驱动的默认传输方式是TCP/IP。这种情况在开发机装Sqlserver时尤其常见安装向导不提示配置文件也不报错像个黑匣子。解决打开SQL Server 配置管理器找到SQL Server网络配置→ 实例名的协议右键TCP/IP选择启用然后到SQL Server服务里重启对应的Sqlserver服务。重启后回到项目不用改任何代码连接就通了。注意操作过程需要管理员权限。5.2 签名报错JWT密钥长度不足32字节现象登录接口生成令牌正常但调用受保护接口时返回401服务端日志显示IDX10603: Signature validation failed或IDX10653: key is too small。原因我用的是HS256算法该算法要求签名密钥至少256位即32字节。配置里随便写了一个mysecret只有9字节SymmetricSecurityKey会直接拒绝或生成弱签名令牌验签永远失败。解决把Jwt:Key替换成至少32位的随机字符串。你可以用PowerShell生成$bytes New-Object byte[] 32 [Security.Cryptography.RandomNumberGenerator]::Fill($bytes) [Convert]::ToBase64String($bytes)生成的Base64字符串贴到配置文件里。如果字符串含特殊字符记得用JSON转义。5.3 迁移报错排序规则冲突与生成表名不一致现象执行dotnet ef database update时报错提示Cannot resolve the collation conflict between Chinese_PRC_CI_AS and SQL_Latin1_General_CP1_CI_AS或者迁移后SSMS里看到表名变成了Users带复数和预期不一致。原因前者是Sqlserver实例默认排序规则与数据库排序规则不一致常见于服务器上既有中文实例、又有英文实例的情况。后者是EF Core默认会把实体名复数化User变成Users。本身没问题但如果你在连接字符串里指定了某个已有库表名不一致会导致运行时找不到表。解决排序规则冲突可以在DbContext的OnModelCreating里固定排序规则protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.EntityUser() .Property(x x.Username) .UseCollation(Chinese_PRC_CI_AS); modelBuilder.EntityUser().ToTable(User); }UseCollation指定列级别排序规则ToTable显式指定表名单数或你自己想要的表名避免EF Core的复数化和你手动建的表名不一致。这个配置要在迁移前写好已经迁移过的项目得先删掉旧迁移重建。5.4 参数校验不生效漏了[ApiController]现象CreateUserDto上标了[Required]但前端不传Username时接口仍然进入方法体返回200而不是400新增了空的用户记录。原因ASP.NET Core的模型自动校验依赖[ApiController]特性只有加上它控制器才会在参数绑定失败时自动返回400。如果继承了ControllerBase但没标注[Required]就成了摆设。解决控制器类上补上加粗代码。还不行就手动校验if (!ModelState.IsValid) return BadRequest(ModelState);放在方法体第一行。前者自动、后者手动两者组合起来覆盖所有场景。5.5 部署翻车IIS进程身份连不上Sqlserver现象本地调试一切正常发布到Windows服务器IIS后接口全部报500日志显示Sqlserver登录失败。原因IIS应用程序池默认身份是ApplicationPoolIdentity它属于IIS_IUSRS组在Sqlserver里没有登录权限。如果你用的是Windows身份验证数据库会拒绝这个匿名进程访问。解决三个方案任选其一。第一种在IIS应用程序池高级设置里把进程模型-标识改成NetworkService然后在Sqlserver里给NT AUTHORITY\NETWORK SERVICE加登录名并授权第二种连接字符串改用Sqlserver账号密码本地那种User Idsa方式第三种在Sqlserver里创建专用登录账号连接字符串里指定它。我推荐第三种权限最小、出问题好查审计。6. 进阶Token续签、统一响应与发布后的验证清单6.1 Token续签滑动过期与双Token两种方案JWT令牌过期后不能让用户重新输密码常见做法是滑动过期或者双Token。滑动过期是每次请求时如果令牌剩余时间不足一半就用RefreshToken换一个新令牌双Token则是签一个短命AccessToken和一个长命RefreshTokenAccessToken过期后用RefreshToken去换。我常用双Token方案RefreshToken存数据库过期时可以吊销。实现要点是给TokenService加一个CreateRefreshToken方法生成一个GUID字符串存入数据库的RefreshTokens表登录接口同时返回AccessToken和RefreshToken新增一个refresh端点验证RefreshToken有效后签发新令牌。这个方案要额外建一张表但逻辑直白、撤销方便。如果不想建表滑动过期用JwtSecurityTokenHandler自带的expires重签也能凑合只是没办法让令牌立即失效。6.2 统一响应与全局异常处理目前接口成功时返回裸数据失败时返回各种状态码前端对接时很痛苦。我一般把响应包一层public class ApiResultT { public int Code { get; set; } public string Message { get; set; } public T Data { get; set; } }再配合一个全局异常过滤器捕获未处理异常并统一返回ApiResultobject让500状态码带上可读的错误信息。这个过滤器继承IExceptionFilter约二十行代码就能覆盖所有接口收益不小。6.3 发布后验证用Swagger把增删改查完整跑一遍发布到IIS后不要急着联调前端先用Swagger走一遍完整链路。启动网站访问/swagger按这个顺序验证先调POST /api/auth/login拿token点右上角Authorize按钮把token粘贴进去然后GET /api/users应该返回200不带token应该返回401接着POST /api/users新增一条记录再用PUT /api/users/{id}改昵称最后DELETE /api/users/{id}删掉全链路无报错才算发布成功。这一步能筛掉八成部署问题。我做项目这些年最深刻的教训就是发布不是结束验证才算数——每次部署完都亲手过一遍这五个接口认证、读写、权限就都有了底。希望帮到你。本文还有配套的精品资源点击获取
返回列表