资讯详情

.NET 5 WebApi三件套:跨域、响应压缩与JWT鉴权授权实战

📅 2026/10/19 8:35:28 | 华诺云谱 👁 阅读
.NET 5 WebApi三件套:跨域、响应压缩与JWT鉴权授权实战
1. 从零搭好一个能打的WebApi先想清楚要处理哪三件事做后端接口开发尤其是用.NET 5现在叫.NET Core 5.0写WebApi光能把Controller跑起来根本不算完。真正到了前后端分离、多端共用、线上部署这一步你迟早要面对三个绕不开的问题跨域、压缩、鉴权授权。这三个词在面试和项目评审里出现的频率极高但很多人对它们的态度是“能用就行”结果一到生产环境就各种翻车。我这个系列文章既然写到第32篇今天就把这三个核心模块一次性拆透。先说清楚它们分别解决什么问题跨域前端站点跑在http://localhost:8080你的API跑在http://localhost:5000浏览器默认会拦死这种跨端口请求不配置跨域策略前端调接口必然报CORS错误。压缩接口返回的JSON动辄几百KB甚至几MB不压缩的话用户的4G/5G流量和服务器带宽都在白白浪费网络传输耗时直接拖垮接口响应速度。JWT鉴权授权接口裸奔意味着任何人都能直接调用你的数据必须要有Token机制来确认“你是谁”“你能干什么”这是API安全的第一道防线。适合正在学习.NET WebApi开发、准备上手真实项目、或者项目里已经遇到这三种问题但一直在“绕路”的朋友。看完这一篇你能获得一套可以直接抄进代码的完整方案而且我还会讲清楚每一步为什么要这么做——知其然也知其所以然才是能做深做扎实的关键。需要先说明的是下面所有代码和配置都是基于.NET 5这一代的标准写法框架版本不同有些命名空间和API会有细微差别但核心思想至今通用。2. 环境与初始项目准备先有一个能复现问题的地基2.1 需要准备的开发环境和版本我假设你用的是Visual Studio 2019/2022或者直接装了.NET 5 SDK。可以打开终端命令行输入下面的命令确认环境dotnet --version正常输出应该是5.0.x系列。如果你机器上装的是.NET 6/7/8依然可以照着这篇文章的操作思路走只是部分配置项在创建项目时的默认模板会略有差异。先创建一个WebApi项目命令如下dotnet new webapi -n Net5WebApiDemo cd Net5WebApiDemo如果你用的是Visual Studio创建项目时选“ASP.NET Core Web API”模板方式一样。2.2 控制器和入口程序构建复现场景我下面演示的所有配置都会基于这个项目结构来展开。先用最原始的状态把项目跑起来验证它确实存在跨域、没有压缩、没有鉴权这三个“裸奔”状态。有一点要提醒.NET 5默认模板里自带一个WeatherForecastController我们直接以它为例就行。实际上真实业务中你会写自己的业务控制器但配置层的思路完全一致。项目启动后默认监听地址一般是https://localhost:5001HTTP的是http://localhost:5000。为了下面的演示更方便在Properties/launchSettings.json里我把applicationUrl改成同时监听两个地址{ profiles: { Net5WebApiDemo: { commandName: Project, applicationUrl: http://localhost:5000;https://localhost:5001, environmentVariables: { ASPNETCORE_ENVIRONMENT: Development } } } }这里改动地址的目的是为了后面模拟跨域场景时前端页面用一个端口、API用另一个端口浏览器才会真正触发CORS机制。现在你直接运行项目调一下/weatherforecast这个接口是能正常返回数据的。但如果你打开浏览器的开发者工具在Console里执行一段跨源请求代码就会发现被拦截。这正是所有问题的起点。3. 跨域配置前后端联调的第一道坎3.1 跨域问题的本质与为什么不能乱放开跨域全称是跨源资源共享CORSCross-Origin Resource Sharing。浏览器基于同源策略默认只允许页面请求“同协议、同域名、同端口”的资源。前端页面跑在某一个源的页面上去请求另一个源的API接口浏览器就会阻止我们看到响应。这不是浏览器在“捣乱”而是安全机制在防CSRF之类的攻击。所以最安全的做法是明确告诉服务器“哪些源可以访问我的API”而不是粗暴地设置AllowAnyOrigin()——如果API完全不做限制就等于把后台敞开了门任何人从任何网站发起的请求都可以打到你的接口上风险极高。平时我见过不少新手图省事直接这样配置app.UseCors(builder builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());这种“三全”配置在开发环境很爽但它意味着你的API完全开放。如果接口涉及用户数据、订单信息这样做几乎是给攻击者递刀子。所以我对生产环境跨域配置的建议是必须白名单化谁允许访问、允许哪些方法、允许哪些请求头都写清楚。3.2 用策略方式配置跨域可控又清晰在.NET 5里推荐的做法是在Startup.cs里预先定义一个或多个CORS策略然后按需启用。下面是我整理好的代码其中包含两个典型场景的配置场景一开发环境前后端分离前端可能跑在不同端口而且调试时需要把Token放在请求头里。这种情况下可以在开发环境放得宽一些但依然不建议允许所有源长期挂着。场景二生产环境只允许固定的前端域名访问方法限定为GET、POST、PUT、DELETE并允许携带Authorization请求头。完整代码如下放在Startup.cs的ConfigureServices方法中public void ConfigureServices(IServiceCollection services) { services.AddControllers(); // 定义CORS策略 services.AddCors(options { options.AddPolicy(CommonPolicy, policy { policy.WithOrigins(http://localhost:8080, https://myweb.example.com) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); options.AddPolicy(DevPolicy, policy { policy.SetIsOriginAllowed(origin true) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); }); }然后在Configure方法中把中间件加进去注意UseCors和UseRouting的执行顺序public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); // 这里启用CORS app.UseCors(CommonPolicy); app.UseAuthorization(); app.UseEndpoints(endpoints { endpoints.MapControllers(); }); }这里有一个极其关键的细节必须给你们提个醒CORS配置错位置会导致完全无效。UseCors必须放在UseRouting之后、UseEndpoints之前而且必须在UseAuthorization之前。因为请求是先被路由选中经过CORS中间件处理再进入授权管道的。如果顺序颠倒CORS的响应头加不上去浏览器照样拦截请求。3.3 验证跨域是否成功一个最朴素的测试办法配置完成后怎么确认自己没配错我习惯用两种方式验证。第一种直接在浏览器里按F12打开开发者工具在Console面板执行fetch(https://localhost:5001/weatherforecast) .then(res res.json()) .then(data console.log(data));如果页面源的端口是8080且该源已经在白名单内就能正常打印数据否则浏览器会报CORS错误。第二种用命令行工具模拟发送带Origin头的请求curl -i -H Origin: http://localhost:8080 https://localhost:5001/weatherforecast重点看响应头里有没有这两行Access-Control-Allow-Origin: http://localhost:8080 Access-Control-Allow-Credentials: true如果看到这两行说明配置生效了。3.4 预检请求与复杂请求很多人没搞明白的细节跨域请求分两种简单请求和预检请求。简单请求指的是GET、HEAD、POST且Content-Type仅限表单格式等这类请求浏览器直接发出。但凡是带了Authorization请求头、或者Content-Type是application/json的POST请求浏览器都会先发一个OPTIONS预检请求探一下服务器是否允许这个跨域请求。这就有个很关键的坑你配置了CORS策略但预检请求是否能通过往往会被忽略。如果你把AllowAnyHeader()加上了OPTIONS请求一般会自动成功。假如你手动做了更精细的限定比如只允许Authorization和Content-Type这两个头那么预检时如果带了其他自定义头就可能过不去。所以在跨域策略里默认建议用AllowAnyHeader()除非你对安全性有极高的定制要求。另外还有一个常见误区在Configure方法里启用了CORS策略却忘了AddCors。如果运行时报“未配置CORS策略”的错误先检查services.AddCors()这行有没有写。我见过不止一个同事是因为少了这行而排查了大半天。4. 响应压缩中间件优化传输体量提升接口响应速度4.1 为什么接口要做响应压缩接口返回的数据通常都是JSON文本里面充满了键名、重复的结构体、空格换行。这些东西不占内存但占网络带宽。尤其在移动端弱网环境下每多传1KB用户等待时间就会增加一点。压缩的意义就是让服务器在返回数据前先用Gzip或Brotli等压缩算法把内容“压扁”浏览器收到后自动解压再交给前端代码。整个过程对前端完全透明但传输的数据量能减少60%到80%。.NET Core 3.0开始内置了响应压缩中间件提供了Gzip和Brotli两种压缩提供器。默认同时启用Brotli和Gzip且Brotli拥有更高的优先级因为Brotli压缩率更高。如果客户端不支持Brotli中间件会自动回退到Gzip这个过程完全自动化。4.2 启用压缩中间件的配置方法在Startup.cs的ConfigureServices方法中添加public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddResponseCompression(options { options.EnableForHttps true; // 关键开关 options.Providers.AddBrotliCompressionProvider(); options.Providers.AddGzipCompressionProvider(); }); services.ConfigureBrotliCompressionProviderOptions(options { options.Level System.IO.Compression.CompressionLevel.Fastest; }); services.ConfigureGzipCompressionProviderOptions(options { options.Level System.IO.Compression.CompressionLevel.Fastest; }); }然后在Configure方法中注意必须在UseRouting之前把压缩中间件挂上public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseResponseCompression(); app.UseRouting(); app.UseCors(CommonPolicy); app.UseAuthorization(); app.UseEndpoints(endpoints { endpoints.MapControllers(); }); }顺序为什么这么关键因为压缩中间件必须在响应内容生成之前就介入。如果放在UseEndpoints之后响应已经发出去了压缩已经没有意义。还有很多人会忽略EnableForHttps这个选项——默认情况下HTTPS请求下的压缩是关闭的因为历史上HTTPS压缩曾被用于“压缩侧信道攻击”如CRIME/BREACH。但站在实际项目角度启用压缩能显著加速HTTPS接口的响应尤其是有敏感大数据量传输的场景风险是可控的所以我个人建议显式开启。4.3 压缩级别怎么选别再纠结了配置里我用了CompressionLevel.Fastest这是有讲究的。CompressionLevel有三个枚举值Optimal压缩最彻底生成的数据最小但CPU开销最高。Fastest压缩速度快CPU占用低压缩率相对低一点。SmallestSize和Optimal差不多但更偏最小体积。对于大多数WebApi场景返回的数据量在几KB到几百KB之间用Fastest是最均衡的选择。这样既降低了传输体积又不会因为服务端CPU忙不过来导致接口变慢。如果你在做的是日志服务或离线文件接口数据量大且对压缩率敏感那再考虑Optimal。4.4 一个容易踩的坑文件下载接口别乱压缩响应压缩中间件会默认压缩所有响应包括静态文件和文件下载接口。这通常会带来一个隐患某些PDF、图片、压缩包本身就是二进制格式再次压缩不仅没效果反而占用CPU。更加严重的是如果客户端传来的Accept-Encoding头不包含gzip或br中间件会自动跳过压缩这没问题。但如果你的下载接口本身就在手动设置Content-Type再叠加压缩会导致文件流被“透明”破坏客户端下载下来的文件可能打不开。我的建议是对于文件传输类接口可以在Controller或Action上加上换行[ResponseCache]做缓存控制或者干脆在中间件配置里通过MimeTypes限制仅压缩JSON和文本类响应不要全开。下面是改进配置services.AddResponseCompression(options { options.EnableForHttps true; options.MimeTypes ResponseCompressionDefaults.MimeTypes.Concat(new[] { text/plain, text/html, application/json, application/xml, text/css, application/javascript }); });这样只会压缩文本类内容二进制文件不碰稳得很。4.5 验证压缩效果的方法启动项目调用接口时用命令行工具加--compressed参数curl --compressed -i https://localhost:5001/weatherforecast关注响应头里的Content-Encoding: gzip或br看到这个就说明压缩生效了。同时对比加了压缩前后响应体的大小你会直观感受到差距——原本一个100KB的PHP对象压缩后可能就剩下20KB左右效果非常显著。5. JWT鉴权授权从登录发令牌到接口验权限5.1 JWT到底是什么用生活化类比讲透JWTJSON Web Token本质上就是一个“带有签名的身份证”。用户在登录时服务器验证完用户名密码签发一个Token给客户端里面包含了用户ID、过期时间等Claims。这个Token是自包含的服务器不需要在内存或数据库里存Session下次请求时客户端带上Token服务器验证签名后直接信任里面携带的身份信息。你可以把它想象成一张“盖章的通行证”你拿着它进大门保安不需要打电话回办公室确认只需要验证章是真的再看看章上盖的日期有没有过期。这就省去了服务端大量的会话管理开销。JWT长这样三段式结构用点号分隔eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NTY3Iiwicm9sZSI6ImFkbWluIn0.xxxxxxxxxxxxxxxxxxxxxxx第一段是Header头部说明签名算法第二段是Payload负载放着身份信息第三段是Signature签名用来防篡改。最后一段是核心它是用第一段Header里的算法你的密钥对前两段内容做HMACSHA256哈希的结果任何人改了一个字符签名就对不上服务器直接拒收。5.2 引入包与基础配置要在.NET 5里用JWT需要装一个官方包dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 5.0.17然后在appsettings.json中配置密钥和颁发方信息{ Jwt: { Issuer: Net5WebApiDemo, Audience: Net5WebApiClient, SecretKey: a-very-long-secret-key-at-least-32-characters, ExpireMinutes: 60 } }SecretKey是签名用的私钥这玩意绝对不能泄露到前端也不要写死在代码里。生产环境应该从环境变量或密钥管理系统里读取。密钥长度至少要32个字符否则HS256算法会提示太短。5.3 配置鉴权服务和中间件在Startup.cs的ConfigureServices中加入using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.Text; public void ConfigureServices(IServiceCollection services) { services.AddControllers(); // 响应压缩配置...见前文 var jwtSection Configuration.GetSection(Jwt); var secretKey jwtSection[SecretKey]; services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuer true, ValidateAudience true, ValidateLifetime true, ValidateIssuerSigningKey true, ValidIssuer jwtSection[Issuer], ValidAudience jwtSection[Audience], IssuerSigningKey new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)), ClockSkew TimeSpan.FromMinutes(1) }; }); services.AddAuthorization(); }ValidateLifetime负责检查Token是否过期ClockSkew是时间偏移容差默认是5分钟意思是即使Token过期了5分钟内也还能用。做API服务器一般建议调低到1分钟避免延迟误差被恶意利用。在Configure方法中加入app.UseRouting(); app.UseCors(CommonPolicy); // 注意先鉴权再授权 app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints { endpoints.MapControllers(); });这里有个顺序雷区UseAuthentication必须在UseAuthorization之前。因为授权的前提是先识别身份如果你的顺序写反了带Token的接口会被401策略拦住但Terminal里却查不出什么明显异常非常坑。5.4 写一个登录接口签发Token接下来写一个最简单的登录接口用来演示Token的签发。我通常会新建一个AuthControllerusing Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; [ApiController] [Route(api/[controller])] public class AuthController : ControllerBase { private readonly IConfiguration _config; public AuthController(IConfiguration config) { _config config; } [HttpPost(login)] public IActionResult Login([FromBody] LoginRequest request) { if (!IsValidUser(request.UserName, request.Password)) { return Unauthorized(new { message 用户名或密码错误 }); } var jwtSection _config.GetSection(Jwt); var claims new ListClaim { new Claim(ClaimTypes.Name, request.UserName), new Claim(ClaimTypes.Role, admin), new Claim(UserId, 10001) }; var key new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSection[SecretKey])); var creds new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token new JwtSecurityToken( issuer: jwtSection[Issuer], audience: jwtSection[Audience], claims: claims, expires: DateTime.Now.AddMinutes(int.Parse(jwtSection[ExpireMinutes])), signingCredentials: creds ); return Ok(new { token new JwtSecurityTokenHandler().WriteToken(token) }); } private bool IsValidUser(string userName, string password) { // 真实项目里这里应该是查数据库并对比密码哈希这里只是演示 return userName admin password 123456; } } public class LoginRequest { public string UserName { get; set; } public string Password { get; set; } }这个Token里包含三个Claim用户名、角色、用户ID。其中ClaimTypes.Role非常重要因为后面做角色授权时是靠它来判断的。5.5 在业务接口上启用认证和角色授权给WeatherForecastController或任意受保护接口加上特性[Authorize] // 表示必须带合法Token才能访问 [ApiController] [Route(api/[controller])] public class WeatherForecastController : ControllerBase { [HttpGet] public IEnumerableWeatherForecast Get() { var user User.Identity.Name; // 获取当前用户名 return Enumerable.Range(1, 5).Select(index new WeatherForecast { Date DateTime.Now.AddDays(index), TemperatureC Random.Shared.Next(-20, 55), Summary Summaries[Random.Shared.Next(Summaries.Length)] }).ToArray(); } [HttpGet(admin)] [Authorize(Roles admin)] // 要求Admin角色才能访问 public IActionResult AdminOnly() { return Ok(new { message 只有管理员能看见我 }); } }[Authorize]只做身份验证相当于说“只要是合法登录的人都可以进”。[Authorize(Roles admin)]则做角色授权相当于说“只有管理员能进”。如果你的业务场景里用户角色有普通用户、商家、管理员三层这招就非常有用了。5.6 如何在前端请求里带Token给前端同事的参考前端在登录成功后拿到token需要在每次请求时放在HTTP请求头里而不是放在URL的query参数里。标准做法是在请求头加Authorizationfetch(https://localhost:5001/weatherforecast, { headers: { Authorization: Bearer localStorage.getItem(token) } })注意格式Bearer后面有个空格再跟上Token字符串。JWT的Bearer认证模式就是这么约定的。少了Bearer这个前缀后端在解析时很容易报401。5.7 配置CORS时必须放行Authorization头到这里跨域和鉴权结合起来了。如果你已经配置了跨域策略但是忘了允许携带Authorization头那么前端请求即使带了Token浏览器在预检阶段就会拦截掉接口根本到不了后端。所以前面跨域配置里的AllowAnyHeader()在本部分就起到了关键作用——它已经包含了Authorization头。如果你把AllowAnyHeader换成了WithHeaders(Content-Type)这种写法就一定要追加policy.WithHeaders(Content-Type, Authorization);这个坑我踩过不止一次每次都是前端急得团团转服务端看日志却完全没有收到请求。记好CORS预检是所有跨域鉴权请求的“大门”这扇门拦住了后面所有东西都是白搭。5.8 完整的鉴权流程全景到这一步完整的鉴权链路就闭合了用户在客户端输入用户名密码请求/api/auth/login。服务端验证通过返回JWT Token。前端把Token存起来localStorage/sessionStorage或内存变量。后续每次请求前端把Token放到Authorization: Bearer xxx头里。请求经过CORS中间件预检通过进入认证中间件校验签名、过期时间、颁发方。认证通过后授权中间件根据Controller上的[Authorize(Roles...)]判断角色。通过后进入Action执行返回结果。响应在离开服务器前经过压缩中间件按需压缩。这一个流程串联下来才是“生产可用的WebApi”的真正形态。6. 常见问题与排查技巧实录实战中反复踩过的坑6.1 CORS配置明明写了却仍然跨域失败现象前端报Access to fetch at xxx from origin xxx has been blocked by CORS policy但明明已经在Startup里配置了AddCors和UseCors。排查步骤先确认UseCors和UseRouting的执行顺序后者必须在前且UseApplication之前。确认AddCors是不是写在ConfigureServices里了。如果使用环境变量区分配置确认当前运行的是哪个环境ConfigureServices里的策略名和UseCors(策略名)是否匹配。打开浏览器Network面板看被拦截的请求类型。如果是一条OPTIONS预检请求被拦重点检查AllowAnyHeader、AllowAnyMethod是否配置了。个人经验开发环境最省事的排错方法是临时改成一个特殊的开发策略SetIsOriginAllowed(origin true)先确认接口本身没问题再逐步收窄白名单。6.2 加了压缩后接口反而变慢了现象配置了压缩中间件之后发现小接口响应时间反而增加了。原因压缩本身消耗CPU如果你的接口返回体只有几百字节压缩算法处理的固定开销可能比节省下来的传输时间还多。响应压缩更适合大响应体场景。建议在中间件里配置MimeTypes限定需要压缩的类型对于特别小的响应可以主动在Content-Type上绕开。不要为了“开了压缩”这个动作而牺牲所有接口的性能。6.3 JWT过期时间设太短用户频繁掉线现象Token设置为30分钟过期结果用户高频操作时每隔半小时就要重新登录一次。原因项目里前端可能把Token放在内存里页面一刷新就没了或者后端没有实现“刷新Token”机制。个人建议项目初期可以用ExpireMinutes60起步线上高并发或对安全敏感的再缩短。进阶方案是双Token模式一个短期AccessToken比如15分钟一个长期RefreshToken比如7天AccessToken过期后通过RefreshToken换取新的AccessToken。这个方案以后可以单独开一篇讲。6.4 带Token请求返回401但Token明明没过期现象前端确认Token是刚登录拿到的但请求接口却返回401 Unauthorized。排查步骤检查请求头格式是不是Authorization: Bearer eyJ...少了Bearer空格最容易出事。检查服务器时间是否准确JWT是用时间戳判断过期时间的如果服务器时间慢了几分钟可能导致判错。检查Token里的Issuer/Audience和我们配置的是否一致只要有一方不匹配校验直接失败。确认每次重启服务后appsettings.json里的SecretKey是不是同一把。6.5 跨域加上之后SignalR或WebSocket也连不上了如果你项目里有SignalR的实时通信CORS配置也要为它额外做处理。SignalR的握手请求会走OPTIONS但后续的WebSocket连接不受同源策略限制“普通”部分约束。你需要在CORS策略里额外允许Access-Control-Allow-Credentials并且把AllowAnyHeader保持为true。否则你会发现HTTP接口的跨域通了但实时推送一直掉线。6.6 别忘了HTTPS的坑生产环境中API如果通过HTTPS对外提供服务而你本机开发和测试HTTP那么Token的传输安全性差异很大。千万不要把EnableForHttps当作永远都为true的开关也别图方便用HTTP生产环境托管API。线上环境务必全链路HTTPS否则Token会被中间人截获。7. 实操总结一套可以“抄作业”的标准配置模板最后我把自己平时用的标准组合整理成一份完整参考你可以直接对照修改Startup.cs核心代码简化版public class Startup { public IConfiguration Configuration { get; } public Startup(IConfiguration configuration) { Configuration configuration; } public void ConfigureServices(IServiceCollection services) { services.AddControllers(); // 1. 跨域 services.AddCors(options { options.AddPolicy(AllowSpecificOrigin, policy { policy.WithOrigins(https://your-frontend.com) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); }); // 2. 压缩 services.AddResponseCompression(options { options.EnableForHttps true; options.Providers.AddBrotliCompressionProvider(); options.Providers.AddGzipCompressionProvider(); }); // 3. JWT var jwtSection Configuration.GetSection(Jwt); var key Encoding.UTF8.GetBytes(jwtSection[SecretKey]); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuer true, ValidateAudience true, ValidateLifetime true, ValidateIssuerSigningKey true, ValidIssuer jwtSection[Issuer], ValidAudience jwtSection[Audience], IssuerSigningKey new SymmetricSecurityKey(key), ClockSkew TimeSpan.FromMinutes(1) }; }); services.AddAuthorization(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseResponseCompression(); app.UseRouting(); app.UseCors(AllowSpecificOrigin); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints { endpoints.MapControllers(); }); } }这套搭配里UseResponseCompression在UseRouting之前UseCors在UseAuthentication之前UseAuthentication在UseAuthorization之前。三个中间件四行顺序一条都不能错。7.1 一个真实项目里的“连环坑”复盘我印象很深的一次是在帮某团队做一个前后端分离的内部管理系统前端用的是Vue后端API就在.NET 5。当时跨域能通、接口能调、登录能发Token但所有需要带Token的接口全部401。查了很久代码后来发现前端一直用的是sessionStorage里的Token但用户登录后页面刷新慢了一拍Token还没写入sessionStorage请求就已经发出去了请求头自然为空。这种问题配置指标完全正常根源在前端时序。所以我想给所有后端的同行提个醒当CORS和JWT配置都正确但接口仍然401时先不要反复怀疑后端中间件而是在浏览器Network面板里看看这一次请求实际带没带Authorization头。排错大概率能省下半小时。7.2 安全加固的进阶建议如果你在做的是高安全的系统SymmetricSecurityKey这种对称签名方案可以逐步换成RSA非对称签名。非对称的好处是签名私钥只存在认证服务器API服务端仅持有公钥来验签。这样即便API服务器被拖库黑客也伪造不出Token。不过这个方案需要引入证书管理和令牌服务初期架构上用对称密钥完全够用。Token过期后用户强制重新登录的体验也可以做成“续签”机制。比如在网关层统一处理401响应如果检测到请求带RefreshToken就自动换新Token再重放一次原始请求。整个闭环做下来用户的登录体验和安全性都能兼顾。7.3 把这套组合练熟的价值跨域、压缩、JWT鉴权授权这三样拼起来就是一个能真正上线、能承受真实流量的WebApi基础底座。练熟这一套你再去看更大型的系统架构微服务网关、统一认证中心、API网关压缩等会发现很多设计思路都是一脉相承的。下一步可以自行扩展的方向我自己在线上项目里已经实践过的就有把JWT密钥存到独立配置中心支持热更新。在响应压缩外面再叠加一层缓存中间件进一步降低服务端计算压力。为不同的Controller配置不同的CORS策略比如管理后台和用户端分开。引入统一异常处理中间件让401和403的响应体验更友好。这些扩展在.NET 5/6/7里都有成熟的实现方式值得每个人结合自己的业务场景去打磨。如果你在实际配置中遇到这里没覆盖到的问题欢迎在评论区把现象和你的配置贴出来大家一起对比下很多疑难杂症都是在讨论里找到灵感的。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑