☰
ASP.NET Core 6 Web API 实战:从 CRUD 到 JWT 认证的完整落地指南
2026/10/19 2:19:26 网站建设 项目流程

简介:这是一份面向 .NET 开发者的 ASP.NET Core 6.0 Web API 入门示例资源,演示如何结合 Entity Framework Core 与 RESTful 风格构建数据接口。项目完整包含 Program.cs 服务配置、WeatherForecast 模型、Controllers 控制器、Migrations 数据迁移以及 appsettings.json 配置等核心文件,适合需要快速掌握 .NET 6 依赖注入、中间件管道、ORM 映射与 CRUD 接口写法的初中级开发者。资源压缩包共 16 个文件,以 cs 源码为主,辅以 json 配置文件、解决方案和项目工程文件,包体仅 13KB,结构精简,可直接用 Visual Studio 2022 打开学习或作为新项目脚手架。目前已有 392 人浏览学习。通过该示例可梳理从项目初始化到控制器实现数据操作的完整链路,理解 Web API 路由、HTTP 方法映射和 EF Core 数据访问模式,便于后续迁移到实际业务开发场景。

1. 为什么这个 Web API 示例值得照着敲一遍:从 Hello World 到可上线骨架差着一整层

很多刚接触 .NET 6 Web API 的开发者都有过这种体验:教程里跑通了一个 Hello World,真去写生产接口时才发现,控制器里塞满了业务逻辑、数据库连接字符串写死在代码里、接口没有认证、Swagger 在服务器上 404。这套 ASP.NET Core 6 Web API 示例做的就是把这层补齐——完整的 CRUD 接口、EF Core 仓储、DTO 映射、JWT 认证、环境相关的配置,以及一条能直接部署的启动路径。它不是写给初学者背的,是让想快速搭一个可维护后端的开发者照着改的。适合要上手 .NET 6 的从业者,也适合要给团队交代一个后端接口规范的人。这篇就把它拆开讲,从项目结构一路拆到部署时最容易翻车的地方。

2. Web API 项目骨架:先看目录和 DI 注册再动手

拿到任何一套源码,我习惯先不跑起来,而是先把目录结构和Program.cs读完。因为这两个地方决定了整套代码的扩展方式:新增一个接口时要改几个文件、换数据库时要动哪里、加中间件时插在哪个位置。这套示例的项目分层是典型的三层结构,Controllers 只做参数绑定和状态码返回,业务逻辑在 Services,数据访问在 Repositories,实体和请求模型分开存放。

2.1 六个项目文件和它们的分工

SampleApi/ ├── Controllers/ # 接口入口,只做参数绑定、调用服务、返回状态码 ├── Models/ # 实体类,对应数据库表结构 ├── DTOs/ # 请求模型和响应模型,不直接暴露实体 ├── Data/ # EF Core 上下文、种子数据逻辑 ├── Services/ # 业务规则、事务处理、核心逻辑 ├── Repositories/ # 数据访问,封装 EF Core 查询 ├── Program.cs # 应用入口、DI 注册、中间件管道 └── appsettings.json # 连接字符串、JWT 配置、日志级别

实体的定义放在 Models,但接口的入参和出参放在 DTOs,这一层在示例里分得很清楚。一开始我不理解为什么要多写这么多类,后来维护一个接口版本混乱的项目才明白:实体是数据库的映射,接口暴露的字段是交互契约,两者混在一起,数据库加一列,前端就可能收到不想暴露的字段。Repositories 这层看需求,如果只是简单的 CRUD,直接在 Services 里用 DbContext 也行,但示例里保留了它,说明设计者是想把查询逻辑也隔离出来,方便做单元测试或者以后换数据库。

Program.cs 是整套示例的核心入口,所有服务注册都集中在这里。看这一层时重点不是看它写了多少行,而是看每个服务的生命周期选得对不对,选错了到高并发时才暴露问题。

2.2 DI 容器里该注册什么:Scoped、Singleton、Transient 的选型

依赖注入(DI)在 ASP.NET Core 6 里是内置能力,不需要额外引入容器,builder.Services就是注册入口。这套示例里可以看到三种生命周期混着用,这是经过了实际业务验证的。

var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddScoped<IOrderRepository, OrderRepository>(); builder.Services.AddScoped<IOrderService, OrderService>(); builder.Services.AddSingleton<IMemoryCacheService, MemoryCacheService>(); builder.Services.AddTransient<IEmailSender, EmailSender>();

DbContext 必须注册为 Scoped。它在同一个请求内复用同一个实例,请求结束就释放,避免了跨请求共享连接带来的并发问题,也避免了每次操作都新建连接的开销。OrderRepository 和 OrderService 同样注册为 Scoped,因为它们都依赖 DbContext,生命周期必须比 DbContext 短或相等,否则就会出现“从 Singleton 服务里使用 Scoped 服务”的运行时异常。

MemoryCacheService 注册为 Singleton,因为内存缓存本身就是全局共享的,每个请求各建一个缓存实例反而失去缓存意义。EmailSender 是发邮件的工具类,没有内部状态,注册为 Transient 最合适——每次请求都拿新实例,成本几乎为零。选型边界就一句话:有状态的、涉及数据库上下文的选 Scoped,纯工具类选 Transient,全局只允许存在一份的选 Singleton。

常见反模式是图省事把 DbContext 注册成 Singleton,启动时不报错,第一个请求也没事,并发一上来就出现 DbContext 线程安全问题,这是最典型的依赖注入翻车现场。

2.3 配置读取与环境变量覆盖

appsettings.json 里集中存放连接字符串、JWT 密钥、日志级别和允许的主机来源。这份示例的配置结构算得上标准,拿过来直接改连接字符串就能跑。

{ "ConnectionStrings": { "DefaultConnection": "Server=localhost;Database=SampleDb;User Id=sa;Password=your_password;TrustServerCertificate=True" }, "Jwt": { "Key": "a-32-char-secret-key-for-signing", "Issuer": "SampleApi", "Audience": "SampleClient", "ExpiresMinutes": 120 }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" }

读取配置不推荐在控制器里直接IConfiguration["Jwt:Key"]这种方式。示例用的是 Options 模式,把配置绑定成一个强类型对象:

builder.Services.AddOptions<JwtOptions>() .Bind(builder.Configuration.GetSection("Jwt")) .ValidateDataAnnotations();

ValidateDataAnnotations 会在启动时校验 JwtOptions 类上的特性标注,比如 Key 不能为空、ExpiresMinutes 必须大于零,配置缺失时应用直接启动失败,而不是等到用户请求时才报 500。这个习惯非常好,能把配置错误提前暴露在部署阶段。

部署到服务器时,配置项会被环境变量覆盖。规则是 JSON 层级用双下划线分隔,比如把ConnectionStrings:DefaultConnection写成环境变量就是ConnectionStrings__DefaultConnection。这套示例写清楚了这层逻辑,但我见过不少人在此翻车,后面避坑章会详细讲。

注意:Jwt 的 Key 在 appsettings.json 里只是占位符,生产环境必须通过环境变量覆盖,不能把真实密钥提交到代码仓库。

3. 把 CRUD 做成可维护的接口:仓储、DTO 映射与状态码的选择

这套示例里最核心的是订单模块的完整 CRUD:创建订单、按 ID 查询、列表查询、修改状态、删除。业务不复杂,但代码写法上刻意做了几个约束——实体和数据库表映射用 Fluent API 集中在 DbContext 里配置、接口层出入参走 DTO、控制器内不直接写查询逻辑。这三个约束分别对应数据层、接口层和控制器的规范化。

3.1 EF Core 上下文与实体映射:建表、软删除和枚举

EF Core 的惯例是实体类名对应表名、属性名对应列名,但业务规则复杂时,惯例不够用。示例在 DbContext 里用 Fluent API 统一配置,而不是在实体类上堆 Attribute,好处是所有表映射规则集中在一个文件里,评审代码时一眼能看全。

public class AppDbContext : DbContext { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } public DbSet<Order> Orders => Set<Order>(); public DbSet<OrderItem> OrderItems => Set<OrderItem>(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Order>(entity => { entity.HasKey(o => o.Id); entity.Property(o => o.OrderNo).HasMaxLength(32).IsRequired(); entity.Property(o => o.Status).HasConversion<string>().HasMaxLength(20); entity.HasQueryFilter(o => !o.IsDeleted); }); modelBuilder.Entity<OrderItem>() .HasOne(oi => oi.Order) .WithMany(o => o.Items) .HasForeignKey(oi => oi.OrderId); } }

HasQueryFilter 是全局查询过滤器,o => !o.IsDeleted这个表达式会自动附加到所有针对 Order 的 LINQ 查询上,实现逻辑删除。这样不需要在每个查询里手动加Where(o => !o.IsDeleted),也不会漏。IsDeleted 字段在实体上用[NotMapped]之外的常规属性即可,删除操作只更新这个字段,不执行 DELETE 语句。

Status 属性用 HasConversion 转成字符串存储,数据库里是 "Pending"、"Shipped" 这样的可读文本,而不是 0、1、2 这种魔法数字。排查线上数据时,直接看表就能明白订单状态,不需要再翻代码。

索引和外键约束也建议在 OnModelCreating 里配,比如 OrderNo 需要建唯一索引:

entity.HasIndex(o => o.OrderNo).IsUnique();

这样数据库层面防止重复订单号,比在应用层判断更可靠。EF Core 的迁移命令dotnet ef migrations add Init会根据这些配置生成迁移脚本,首次初始化时执行即可。

3.2 DTO 与映射:接口层不该暴露实体

直接返回实体对象在小型项目里看着省事,但实体被序列化成 JSON 时,导航属性会把整个关联数据带出来,使用者不需要的字段全暴露了,甚至可能产生循环引用导致序列化异常。示例里把新增订单的请求单独抽成 CreateOrderDto,响应单独抽成 OrderResponseDto,中间用映射处理。

public record CreateOrderDto(string CustomerName, List<CreateOrderItemDto> Items); public record CreateOrderItemDto(string ProductName, int Quantity, decimal UnitPrice);
public class OrderProfile : Profile { public OrderProfile() { CreateMap<CreateOrderDto, Order>() .ForMember(dest => dest.OrderNo, opt => opt.Ignore()); CreateMap<CreateOrderItemDto, OrderItem>(); CreateMap<Order, OrderResponseDto>(); } }

ForMember 配置了 OrderNo 忽略赋值,因为订单号是服务层生成的业务编号,不能由前端传进来。如果不写 Ignore,AutoMapper 会把 CreateOrderDto 里的 OrderNo 属性(如果存在)直接映射到实体上,前端传什么订单号就存什么,这是个安全隐患。

AutoMapper 的 Profile 类在启动时注册:

builder.Services.AddAutoMapper(typeof(Program));

然后在控制器构造函数里注入IMapper,在 Service 层把实体映射成响应 DTO。这套写法的收益要等到接口字段调整时才明显——数据库加字段、实体加属性,只要不涉及 DTO 变更,接口契约就不变,前端不需要同步修改。

3.3 控制器动作与状态码:201、404 和 NoContent

控制器的每个动作只做三件事:接收参数、调用服务、构造响应。示例里的写法值得直接抄:

[ApiController] [Route("api/[controller]")] public class OrdersController : ControllerBase { private readonly IOrderService _orderService; public OrdersController(IOrderService orderService) { _orderService = orderService; } [HttpPost] [ProducesResponseType(StatusCodes.Status201Created)] public async Task<ActionResult<OrderResponseDto>> Create(CreateOrderDto dto, CancellationToken ct) { var order = await _orderService.CreateAsync(dto, ct); return CreatedAtAction(nameof(GetById), new { id = order.Id }, order); } [HttpGet("{id:guid}")] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task<ActionResult<OrderResponseDto>> GetById(Guid id, CancellationToken ct) { var order = await _orderService.GetByIdAsync(id, ct); if (order is null) return NotFound(); return Ok(order); } [HttpDelete("{id:guid}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<IActionResult> Delete(Guid id, CancellationToken ct) { await _orderService.DeleteAsync(id, ct); return NoContent(); } }

创建成功返回 201 而不是 200,这是 REST 接口一个常见细节。CreatedAtAction 会在响应头里带上 Location,指向新资源的访问地址,前端拿到 201 后可以通过 Location 直接查询详情。GetById 查不到时返回 404 而不是 200 + null,这能保证客户端逻辑不会判空出错。Delete 成功后返回 204 NoContent,表示操作成功但没有响应体。

接口方法的返回值建议用ActionResult<T>,这样既能返回具体类型,也能返回 NotFound 这类非泛型结果。CancellationToken 参数从请求进来开始贯穿到数据查询,客户端断开连接时 EF Core 会取消正在执行的查询,避免无效数据库操作占用连接。

还有个实践是控制器方法参数上直接标路由约束{id:guid},格式不对的请求会在路由匹配阶段被拒绝,返回 404,而不是进到方法里再判断。

4. JWT 认证与授权:Token 生成、中间件顺序与策略边界

没有认证的 Web API 只能用在纯内部调试环境。这套示例在 JWT 认证上做了完整闭环:登录接口签发 Token,受保护接口校验 Token,管理员接口做授权策略限制。需要注意的不只是怎么写 Token 服务,还有中间件注册顺序和策略设计边界。

4.1 生成 Token:Claims、签名和过期时间

JWT 的结构是 Header.Payload.Signature,签名部分保证 Token 内容没有被篡改。示例用对称密钥 HS256 算法,同一把密钥既用来签发也用来验证,实现最简单,单服务部署场景完全够用。

public class TokenService { private readonly IConfiguration _config; public TokenService(IConfiguration config) { _config = config; } public string CreateToken(string userId, string role) { var key = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(_config["Jwt:Key"])); var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var claims = new List<Claim> { new Claim(JwtRegisteredClaimNames.Sub, userId), new Claim(ClaimTypes.Role, role), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }; var token = new JwtSecurityToken( issuer: _config["Jwt:Issuer"], audience: _config["Jwt:Audience"], claims: claims, expires: DateTime.UtcNow.AddMinutes(Convert.ToInt32(_config["Jwt:ExpiresMinutes"])), signingCredentials: credentials); return new JwtSecurityTokenHandler().WriteToken(token); } }

Claim 里放什么要克制。Sub 存用户 ID,Role 存角色,Jti 是 Token 的唯一标识,用于刷新和注销场景。不要放密码、手机号这类敏感信息,因为 JWT 的 Payload 默认只做 Base64 编码,不是加密的,能解码就能看到内容。密钥长度有硬性要求,HS256 要求至少 32 字节,长度不够时运行时直接抛异常。

过期时间建议设置短一些,比如 120 分钟,然后配合刷新 Token 机制。示例里的过期时间取的是配置项,方便在不同环境调整。这里有个小知识点:DateTime.UtcNow.AddMinutes(...)用的是 UTC 时间而不是本地时间,避免服务器时区不一致导致 Token 提前过期或延迟过期。

登录接口里需要配合用户验证逻辑,比如校验用户名密码,校验通过才调用 CreateToken。密码校验推荐PasswordHasher<TUser>,不要自己写哈希。

4.2 中间件管道顺序:认证在授权之前,CORS 在认证之前

Program.cs 里中间件的注册顺序就是请求处理顺序,这一层顺序错了,后面所有功能都白搭。示例把完整的管道结构写清楚了:

var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseCors("CustomPolicy"); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();

UseAuthentication 必须在 UseAuthorization 之前,因为授权中间件要读取认证中间件填充的用户信息才能判断权限。顺序反了的表现是:登录后拿着 Token 请求接口,返回 401 而不是 403,因为授权中间件执行时用户身份还没被识别。

UseCors 放在了 UseAuthentication 之前,这个顺序反映了一个关键细节:浏览器的 CORS 预检请求(OPTIONS)不会携带 Authorization 头。如果 CORS 中间件在认证中间件之后执行,预检请求会在认证环节被拦截,前端报“CORS 错误”但实际是认证问题。先执行 CORS,预检请求就能直接通过,真正携带 Token 的请求再由认证中间件处理。

UseSwagger 在生产环境默认不启用,这层逻辑在 Program.cs 里体现为环境判断。如果内网联调需要生产环境的 Swagger,可以用配置项控制而不是直接改成app.UseSwagger(),后面避坑章会展开。

4.3 授权策略:角色、权限与自定义 Policy

认证只管“你是谁”,授权管“你能不能做这件事”。示例里的授权策略不局限于简单的角色判断,而是把角色和操作权限组合在一起:

builder.Services.AddAuthorization(options => { options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin") .RequireClaim("Permission", "ManageOrders")); });

控制器上使用:

[Authorize(Policy = "AdminOnly")] [HttpDelete("{id:guid}")] public async Task<IActionResult> Delete(Guid id, CancellationToken ct) { await _orderService.DeleteAsync(id, ct); return NoContent(); }

RequireRole 和 RequireClaim 组合的含义是:用户必须同时具备 Admin 角色和 ManageOrders 权限声明才能执行删除操作。单纯用角色控制粒度太粗,比如“运营可以改价格但不能删订单”,只靠角色就需要拆出多个角色。加一层权限声明后,角色定义和操作权限解耦,新增一个操作只需要调整策略,不用修改用户角色。

如果策略要求超出内置 API 的能力——比如“只有订单创建者本人可以修改订单”——就需要自定义 AuthorizationHandler。示例没有扩展到这么深,但边界已经划定:当策略是多个条件的 AND 组合时,用内置 API 拼装;当策略逻辑涉及数据查询或复杂状态判断时,才需要实现IAuthorizationRequirement和AuthorizationHandler<T>。

注意:JWT 里放的 Claims 不会自动同步到用户权限,修改策略后必须重新签发 Token,否则新策略不会生效。

5. 部署与测试中的五个坑:环境变量、容器网络与同步等待

示例代码在本地跑通只是一半,真正的问题都出在部署和联调阶段。以下五条是我在接手类似项目时踩过的真实坑,每一条都按现象、原因、解决三步拆开,你可以对照检查自己的部署配置。

5.1 坑一:连接字符串被环境变量覆盖了还是连不上

现象:本地连接数据库正常,部署到服务器后接口一直报 SQL Server 连接超时或登录失败。

原因:服务器上设置了环境变量ConnectionStrings__DefaultConnection,但值里的数据库地址写的是内网 IP,服务器到那个 IP 的网络不通。或者环境变量名和配置层级拼写不一致,导致覆盖不生效。最常见的是把ConnectionStrings__DefaultConnection写成了ConnectionStrings_DefaultConnection,单下划线不会被识别为层级分隔符。

解决:先查实际解析出来的连接字符串,而不是猜。

# 在服务器上临时输出配置,确认实际值 dotnet run --project SampleApi --urls http://0.0.0.0:5000 curl http://localhost:5000/api/debug/config

或者直接在 Program.cs 启动时写一行临时日志:

var connStr = builder.Configuration.GetConnectionString("DefaultConnection"); app.Logger.LogWarning("ConnStr: {ConnStr}", connStr);

看到实际解析出的值再排查网络和账号问题。环境变量覆盖规则是:必须用双下划线分隔层级,大小写不敏感,Linux 和 Windows 行为一致。

5.2 坑二:容器里访问不到 SQL Server

现象:接口在 Docker 容器里启动正常,但一请求数据库就超时。在宿主机上直接运行同一个连接字符串却能连上。

原因:容器内的 localhost 是容器自己,不是宿主机。连接字符串写的Server=localhost在容器里指向了容器自身,当然连不上宿主机的 SQL Server。

解决:根据运行环境区分三种写法,并用 docker-compose 固定容器网络。

services: api: image: sample-api:dev environment: - ConnectionStrings__DefaultConnection=Server=database;Database=SampleDb;User Id=sa;Password=P@ssw0rd;TrustServerCertificate=True database: image: mssql-server:latest environment: - ACCEPT_EULA=Y - MSSQL_SA_PASSWORD=P@ssw0rd

docker-compose 会在同一个默认网络中创建服务,Server=database这里的 database 是 compose 里定义的 service 名,DNS 会自动解析到数据库容器。本机直接运行时,Windows 和 Mac 的 Docker 桌面支持Server=host.docker.internal访问宿主机,Linux 上需要额外加--add-host=host.docker.internal:host-gateway。把连接字符串放在 compose 的环境变量里而不是镜像里,换环境不用重新构建镜像,这也是这套示例用环境变量覆盖配置的一个原因。

5.3 坑三:Swagger 在生产环境 404

现象:开发环境访问 /swagger 正常,部署到测试服务器后一直 404,页面打不开。

原因:Program.cs 里把 UseSwagger 和 UseSwaggerUI 包在了if (app.Environment.IsDevelopment())块里。生产环境默认不是 Development,中间件根本没有注册,所以 404 非常正常。

解决:如果是内网联调环境,用配置项控制 Swagger 的开关,不要直接用环境判断。

if (app.Environment.IsDevelopment() || app.Configuration.GetValue<bool>("Swagger:Enabled")) { app.UseSwagger(); app.UseSwaggerUI(); }

appsettings.json 增加:

"Swagger": { "Enabled": true }

生产环境把Swagger:Enabled设为 false 即可。另一个常见错误是把UseSwaggerUI()写到了UseSwagger()前面,中间件按顺序执行时,先注册的 UseSwaggerUI 找不到 Swagger 文档生成器,也会导致访问异常。顺序永远是先 UseSwagger 再 UseSwaggerUI。

5.4 坑四:CORS 配置看起来正确,前端还是报跨域

现象:前端请求能发出,但浏览器控制台提示Access-Control-Allow-Origin缺失,或者请求在预检阶段直接失败。

原因:两种情况。一种是策略里同时调用了AllowAnyOrigin()和AllowCredentials(),这两者在浏览器规范里是互斥的,运行时直接抛异常。另一种是 CORS 中间件位置错误,OPTIONS 预检请求被后面的认证中间件拦截,浏览器收不到正确的 CORS 响应头。

解决:具体源用 WithOrigins,不要用 AllowAnyOrigin。

builder.Services.AddCors(options => { options.AddPolicy("CustomPolicy", policy => policy.WithOrigins("https://frontend.example.com") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials()); });

配合前文提到的管道顺序,UseCors 一定要在 UseAuthentication 之前。如果前端在开发环境跑的端口是 5173,源要精确到端口,WithOrigins("http://localhost:5173"),不要省略端口。写AllowAnyOrigin的教训是:浏览器规范不准同时带上 Cookie 凭证,需求一要求带凭证就得改回 WithOrigins。

5.5 坑五:异步接口用同步等待,高并发后请求排起长队

现象:压测时接口响应时间从 50 毫秒逐渐涨到几秒,CPU 占用和线程数持续走高,QPS 上不去。

原因:控制器或服务里用了.Result或.Wait()同步等待异步方法。ASP.NET Core 的处理线程池是有限的,同步阻塞会把线程占住不放,并发一高线程池就饿死了。

解决:异步一路到底,不在任何链路里同步阻塞。

// 错误写法:同步等待阻塞线程 var orders = _orderService.GetAllAsync().Result; // 正确写法:异步等待 var orders = await _orderService.GetAllAsync();

排查这类问题最简单的方法是把代码里所有.Result、.Wait()、.GetAwaiter().GetResult()搜出来逐个改掉。另外提一个最容易忽略的场景:构造函数里不能 await,有些人会把异步初始化改成.Result塞进构造函数,这在高并发下就是灾难。构造函数只做赋值,异步初始化放到一个显式的 InitializeAsync 方法里。

注意:async void 只用于事件处理器,用在控制器动作里异常会直接抛出到线程池,导致进程崩溃。

6. 用一条命令验证整套示例:OpenAPI 客户端与健康检查组合

示例跑起来之后,别急着写业务代码,先做两件事:配健康检查、把登录请求和受保护接口请求串成一条命令。这两步能确认数据库连接、JWT 签发、认证授权整条链路是通的,比在 Swagger UI 里手动点几十次高效得多。

先在 Program.cs 注册健康检查,示例用的是 EF Core 的数据库连通性检查:

builder.Services.AddHealthChecks() .AddDbContextCheck<AppDbContext>("Database");

然后在管道末尾映射健康检查端点:

app.MapHealthChecks("/health", new HealthCheckOptions { ResponseWriter = async (context, report) => { context.Response.ContentType = "application/json"; var result = JsonSerializer.Serialize(new { status = report.Status.ToString(), checks = report.Entries.Select(e => new { name = e.Key, status = e.Value.Status.ToString() }) }); await context.Response.WriteAsync(result); } });

启动后第一件事验证基础链路:

curl -s http://localhost:5000/health | jq

看到checks里数据库状态是 Healthy,说明连接字符串和网络层没问题。接下来把完整的认证流程串起来:

# 1. 登录获取 Token TOKEN=$(curl -s -X POST http://localhost:5000/api/auth/login \ -H 'Content-Type: application/json' \ -d '{"userName":"admin","password":"123456"}' | jq -r '.token') # 2. 携带 Token 访问受保护接口 curl -s http://localhost:5000/api/orders \ -H "Authorization: Bearer $TOKEN" | jq

第一条命令如果返回的不是 200,先看登录接口的入参格式和默认账号是否与示例代码一致。第二条命令如果返回 401,检查 Token 里是否包含正确的 Claims,以及 Authorization 头格式是不是Bearer加一个空格再加 Token 字符串。这套命令跑通之后,Swagger UI 里也能顺手验证——点 Authorize 按钮把 Token 粘进去,后续请求自动带上 Authorization 头。

示例代码里的 OpenAPI 描述还支持直接生成客户端,但我不建议在项目初期引入代码生成器,因为接口还没稳定,生成的客户端代码改动成本很高。先用 curl 验证完核心链路,再交给前端对接。

从那以后,我每次搭建接口项目都强制自己走一遍这个流程:先配健康检查,再写一条 curl 串起登录和业务请求,确认链路通了才写业务逻辑。这个习惯帮我避开了好几次“代码看着没问题,联调时全断”的尴尬局面。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询