news 2026/10/5 16:14:54

.NET6 WebApi JWT鉴权零容忍配置指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
.NET6 WebApi JWT鉴权零容忍配置指南

简介:本资源是一份面向.NET开发者,特别是初学者与中级后端工程师的Web API安全开发实践范例,聚焦于在.NET 6平台下集成JWT实现用户身份鉴权与接口保护。项目完整覆盖JWT令牌生成、签发验证、Swagger交互式文档集成、控制器级授权控制等核心环节,并配套可直接运行的多层架构解决方案(AuthenticationService.sln),含业务操作、服务逻辑与数据模型三类模块。压缩包共68个文件,主体为11个C#源码文件(如AuthenticationController.cs、WeatherForecastController.cs)、14个JSON配置(含appsettings.json及NuGet缓存文件)、18个DLL程序集及若干.props/.targets构建文件,整体体积仅1.43MB,轻量易部署。已有4503人学习下载,读者可直接导入Visual Studio运行调试,快速掌握JWT在.NET 6 Web API中的标准落地流程、Swagger认证头注入方式及分层项目组织结构,具备强实操性与教学参考价值。

1. 为什么你在 .NET6 WebApi 里手写 JWT 鉴权时,总在登录成功后收不到 Token?

这不是配置漏了、包没装对,而是你默认用的AddAuthentication().AddJwtBearer()在 .NET6 中已强制要求显式指定TokenValidationParameters的所有关键校验项——哪怕你只漏配ValidateIssuerSigningKey = true这一行,整个中间件就会静默失败,Postman 返回 401 却不报错,日志里连一条警告都没有。我见过太多团队卡在这一步:前端反复调/login拿不到 token,后端查SignInAsync返回SignInResult.Success,却死活不生成JwtSecurityToken。根本原因不是代码逻辑错,是 .NET6 把 JWT 的“安全默认值”从“宽松兼容”彻底切到了“零容忍”。本文就带你从头跑通一个可验证、可调试、可上线的 .NET6 WebApi JWT 鉴权方案:包含密钥安全存储、Token 生成与刷新、 WebApi ValidationAttribute 全局校验、以及用 xUnit + TestServer 实测鉴权链路的完整源码路径。适合正在用 .NET6 开发 SPA 后端、或接手老项目升级 JWT 模块的工程师——别再靠 StackOverflow 拼凑碎片代码,这一套能直接进 CI/CD 流水线。


2. 用 .NET6 原生中间件搭起 JWT 鉴权骨架:从 Program.cs 到控制器的最小闭环

2.1 初始化服务:为什么必须手动注入IConfiguration并分离密钥配置?

.NET6 的Program.cs是单文件启动模型,但 JWT 密钥绝不能硬编码在代码里。常见翻车点是把SymmetricSecurityKey直接 new 出来塞进AddJwtBearer,结果部署到 Linux 容器时因大小写敏感或环境变量读取失败导致签名验证全挂。正确做法是:

  • 将密钥存为 32 字节 Base64 字符串(对应 256-bit AES),通过appsettings.json的JwtSettings:Key读取;
  • 在Program.cs中用IConfiguration构建SymmetricSecurityKey,并确保ValidateIssuerSigningKey = true显式开启;
  • 必须同时设置ValidateIssuer和ValidateAudience,否则AddJwtBearer会跳过全部校验(这是 .NET6 的安全强化行为)。
// Program.cs var builder = WebApplication.CreateBuilder(args); // 1. 读取 JWT 配置(注意:Key 必须是 Base64 编码的 32 字节字符串) var jwtSettings = builder.Configuration.GetSection("JwtSettings"); var key = Encoding.UTF8.GetBytes(jwtSettings["Key"] ?? throw new InvalidOperationException("JwtSettings:Key is missing")); 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, // ⚠️ 此行必须为 true,否则不校验签名 ValidIssuer = jwtSettings["Issuer"], ValidAudience = jwtSettings["Audience"], IssuerSigningKey = new SymmetricSecurityKey(key), // ⚠️ Key 必须是 byte[],不能是 string ClockSkew = TimeSpan.Zero // ⚠️ 关闭时钟偏移容忍,避免测试环境时间不同步导致 token 失效 }; }); builder.Services.AddAuthorization();

提示:ClockSkew = TimeSpan.Zero是测试阶段的刚需。生产环境若服务器时间不同步,应设为TimeSpan.FromMinutes(5),但开发期留着它会导致 token 在生成后立刻被判定“尚未生效”(nbf时间戳问题)。

2.2 控制器层:如何用[Authorize]和[AllowAnonymous]精准控制接口权限?

JWT 鉴权生效的前提是控制器方法明确标注[Authorize]。但新手常犯两个错误:一是给整个 Controller 加[Authorize]却忘了登录接口必须[AllowAnonymous];二是混淆Authorize和Authorize(Roles="Admin")的触发时机——后者会在OnAuthorizationAsync中查询用户角色,而角色数据必须提前存入ClaimsPrincipal。

// Controllers/AuthController.cs [ApiController] [Route("api/[controller]")] public class AuthController : ControllerBase { private readonly IConfiguration _configuration; public AuthController(IConfiguration configuration) { _configuration = configuration; } [HttpPost("login")] [AllowAnonymous] // ⚠️ 必须标记,否则 /login 会被拦截 public IActionResult Login([FromBody] LoginRequest request) { // 模拟用户验证(实际应查数据库) if (request.Username == "admin" && request.Password == "123456") { var token = GenerateJwtToken(request.Username); return Ok(new { token }); } return Unauthorized(); } private string GenerateJwtToken(string username) { var jwtSettings = _configuration.GetSection("JwtSettings"); var key = Encoding.UTF8.GetBytes(jwtSettings["Key"]); var issuer = jwtSettings["Issuer"]; var audience = jwtSettings["Audience"]; var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, username), new Claim(ClaimTypes.Role, "User"), // ⚠️ 角色必须以 Claim 形式写入,否则 [Authorize(Roles="User")] 不生效 new Claim("UserId", "1001") // 自定义 Claim,前端可直接解析 }), Expires = DateTime.UtcNow.AddMinutes(30), SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature), Issuer = issuer, Audience = audience }; var tokenHandler = new JwtSecurityTokenHandler(); var securityToken = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(securityToken); } } // Controllers/ValuesController.cs [ApiController] [Route("api/[controller]")] [Authorize] // ⚠️ 整个 Controller 受保护,所有方法需 token public class ValuesController : ControllerBase { [HttpGet] public IActionResult Get() { // HttpContext.User.Identity.IsAuthenticated 为 true 才能进来 return Ok(new[] { "value1", "value2" }); } [HttpGet("admin")] [Authorize(Roles = "Admin")] // ⚠️ 此方法仅 Admin 角色可访问 public IActionResult GetAdminData() { return Ok("Admin only data"); } }

参数说明:GenerateJwtToken中ClaimTypes.Role是框架识别角色的标准键名,不能写成"role"或"Role";SecurityAlgorithms.HmacSha256Signature是 .NET6 默认支持的算法,若用HmacSha512Signature需额外安装Microsoft.IdentityModel.Tokens包。


3. 让 JWT 不只是“能用”,而是“防篡改+可续签+带刷新”:Token 续期与漏洞规避设计

3.1 Token 续签机制:为什么不能只靠Expires延长,而要双 Token(Access + Refresh)?

单纯把 JWTExpires设为 7 天看似解决“频繁登录”,实则埋下严重安全漏洞:一旦 Access Token 泄露,攻击者可无限期使用。行业标准方案是引入 Refresh Token——短时效 Access Token(如 30 分钟)用于日常请求,长时效 Refresh Token(如 7 天)仅用于换取新 Access Token,且 Refresh Token 必须存于 HttpOnly Cookie 并绑定设备指纹。

在 .NET6 中实现的关键点:

  • Refresh Token 不走 JWT 标准流程,而是存在服务端数据库(或 Redis),用 GUID 作为唯一标识;
  • /refresh接口必须校验 Refresh Token 的有效性、绑定 IP、设备指纹(如 User-Agent Hash),且用完即删;
  • 新生成的 Access Token 必须包含jti(JWT ID)Claim,用于服务端黑名单校验(防止旧 Token 被重放)。
// Controllers/AuthController.cs 新增方法 [HttpPost("refresh")] [AllowAnonymous] public async Task<IActionResult> Refresh([FromBody] RefreshRequest request) { var refreshToken = request.RefreshToken; // 1. 从 Redis 查 Refresh Token(示例用内存字典模拟) if (!_refreshTokens.TryGetValue(refreshToken, out var storedToken) || storedToken.ExpiresAt < DateTime.UtcNow) { return Unauthorized("Invalid or expired refresh token"); } // 2. 校验设备指纹(简化版:比对 User-Agent) var userAgentHash = Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(Request.Headers["User-Agent"]))); if (storedToken.UserAgentHash != userAgentHash) { return Unauthorized("Device fingerprint mismatch"); } // 3. 生成新 Access Token(含 jti) var newAccessToken = GenerateJwtTokenWithJti(storedToken.Username, Guid.NewGuid().ToString()); // 4. 生成新 Refresh Token 并覆盖旧值 var newRefreshToken = Guid.NewGuid().ToString(); _refreshTokens.TryRemove(refreshToken, out _); _refreshTokens[newRefreshToken] = new StoredRefreshToken { Username = storedToken.Username, ExpiresAt = DateTime.UtcNow.AddDays(7), UserAgentHash = userAgentHash }; // 5. 设置 HttpOnly Cookie(前端无法 JS 读取) Response.Cookies.Append("refresh_token", newRefreshToken, new CookieOptions { HttpOnly = true, Secure = true, // 生产环境必须开启 HTTPS SameSite = SameSiteMode.Strict, Expires = DateTime.UtcNow.AddDays(7) }); return Ok(new { access_token = newAccessToken }); } private string GenerateJwtTokenWithJti(string username, string jti) { var jwtSettings = _configuration.GetSection("JwtSettings"); var key = Encoding.UTF8.GetBytes(jwtSettings["Key"]); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, username), new Claim("jti", jti) // ⚠️ jti 用于服务端黑名单校验 }), Expires = DateTime.UtcNow.AddMinutes(30), SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature), Issuer = jwtSettings["Issuer"], Audience = jwtSettings["Audience"] }; var tokenHandler = new JwtSecurityTokenHandler(); return tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor)); } // 内存存储 Refresh Token(实际应替换为 Redis) private readonly ConcurrentDictionary<string, StoredRefreshToken> _refreshTokens = new(); public class StoredRefreshToken { public string Username { get; set; } public DateTime ExpiresAt { get; set; } public string UserAgentHash { get; set; } }

注意:Response.Cookies.Append的Secure = true在开发环境需配合 HTTPS 本地证书,否则浏览器拒绝写入。VS 2022 创建 .NET6 WebApi 项目时默认启用 HTTPS,但若手动关闭,此处会静默失败。

3.2 防止 JWT 漏洞:三类高频风险及 .NET6 特定加固措施

风险类型现象.NET6 加固方案验证方式
算法混淆(None Attack)攻击者将 JWT Header 的"alg": "none"发送,绕过签名验证AddJwtBearer默认禁用none算法,无需额外配置;但若自定义TokenValidationParameters,必须确保RequireSignedTokens = true(默认已开启)Postman 发送 alg=none 的 token,应返回 401
密钥泄露(Weak Key)使用短字符串(如"mykey")生成 256-bit Key,实际熵值不足强制使用Convert.FromBase64String()解析密钥,拒绝纯 ASCII 字符串;密钥长度必须为 32 字节在Program.cs中加断点,检查key.Length == 32
令牌重放(Replay Attack)同一 Access Token 被多次使用在GenerateJwtTokenWithJti中写入jti,并在中间件中添加OnTokenValidated事件校验黑名单启动 Redis,存入已用jti,二次请求应返回 401
// Program.cs 中增强校验 options.Events = new JwtBearerEvents { OnTokenValidated = context => { var jti = context.Principal.FindFirst("jti")?.Value; if (!string.IsNullOrEmpty(jti)) { // 模拟 Redis 黑名单查询(实际应异步调用) if (_blacklistedJtis.Contains(jti)) { context.Fail("Token has been revoked"); } } } };

4. 避坑:.NET6 JWT 鉴权的 4 个血泪经验,90% 的人栽在第 2 条

4.1 现象:Postman 调/login返回{ "token": "xxx" },但后续请求带Authorization: Bearer xxx仍 401

原因:AddJwtBearer的TokenValidationParameters中ValidateIssuerSigningKey = false(默认值),导致签名不校验,但Issuer和Audience校验失败时静默拒绝。
解决:显式设ValidateIssuerSigningKey = true,并确认Issuer/Audience与生成 Token 时完全一致(包括末尾斜杠、大小写)。

4.2 现象:HttpContext.User.Identity.IsAuthenticated始终为false,即使 Token 解析成功

原因:.NET6的AddAuthentication必须配合UseAuthentication()中间件,且顺序不能错——必须在UseRouting()之后、UseEndpoints()之前调用。
解决:检查Program.cs中间件顺序:

app.UseRouting(); // ✅ 必须在最前 app.UseAuthentication(); // ✅ 必须在此处 app.UseAuthorization(); // ✅ 必须在此处 app.UseEndpoints(endpoints => { ... }); // ✅ 最后

4.3 现象:[Authorize(Roles="Admin")]不生效,始终返回 403

原因:角色 Claim 的 Key 必须是ClaimTypes.Role(值为"http://schemas.microsoft.com/ws/2008/06/identity/claims/role"),而非"role"或"Role";且AddAuthorization未配置AddRoles<IdentityRole>()。
解决:生成 Token 时用new Claim(ClaimTypes.Role, "Admin");若用 Identity,需注册services.AddDefaultIdentity<IdentityUser>().AddRoles<IdentityRole>()。

4.4 现象:本地开发一切正常,Docker 部署后所有 JWT 请求 401

原因:容器内系统时间与宿主机不同步,ClockSkew默认 5 分钟不足以覆盖偏差;或环境变量JwtSettings__Key未正确传入容器。
解决:

  • Dockerfile 中添加RUN apk add --no-cache chrony && rc-update add chronyd default同步时间;
  • 启动命令中显式传入密钥:docker run -e "JwtSettings__Key=base64_encoded_32byte_key" ...;
  • Program.cs中加日志输出key.Length,确认是否读取为空。

5. 用 xUnit + TestServer 实测鉴权链路:从登录、访问、续签到黑名单拦截的全路径验证

5.1 构建可测试的 WebApplicationFactory:隔离配置与密钥

xUnit 测试不能依赖appsettings.json,必须在测试类中动态注入配置。核心技巧是重写WebApplicationFactory<T>的CreateWebHostBuilder,用ConfigureAppConfiguration替换原始配置源:

// Tests/IntegrationTests.cs public class AuthIntegrationTests : IClassFixture<WebApplicationFactory<Program>> { private readonly WebApplicationFactory<Program> _factory; public AuthIntegrationTests(WebApplicationFactory<Program> factory) { _factory = factory.WithWebHostBuilder(builder => { builder.ConfigureAppConfiguration((context, config) => { config.Sources.Clear(); // 清空原始 appsettings.json config.AddInMemoryCollection(new Dictionary<string, string> { ["JwtSettings:Key"] = "4vX5Yz9Wq2Rb8KpLmNcFgTjHsEaDvIuO", // 32 字节 Base64 ["JwtSettings:Issuer"] = "https://localhost:5001", ["JwtSettings:Audience"] = "https://localhost:5001" }); }); }); } [Fact] public async Task Login_ReturnsToken_WhenCredentialsValid() { // Arrange var client = _factory.CreateClient(); // Act var response = await client.PostAsJsonAsync("/api/auth/login", new { Username = "admin", Password = "123456" }); // Assert response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync(); Assert.Contains("token", content); } [Fact] public async Task ProtectedEndpoint_Returns401_WhenNoToken() { var client = _factory.CreateClient(); var response = await client.GetAsync("/api/values"); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } [Fact] public async Task ProtectedEndpoint_Returns200_WhenValidToken() { // Arrange var client = _factory.CreateClient(); var loginResponse = await client.PostAsJsonAsync("/api/auth/login", new { Username = "admin", Password = "123456" }); var loginContent = await loginResponse.Content.ReadFromJsonAsync<LoginResponse>(); // Act client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", loginContent.Token); var protectedResponse = await client.GetAsync("/api/values"); // Assert Assert.Equal(HttpStatusCode.OK, protectedResponse.StatusCode); } } public class LoginResponse { public string Token { get; set; } }

关键点:AddInMemoryCollection注入的密钥必须是 32 字节 Base64 字符串(如"4vX5Yz9Wq2Rb8KpLmNcFgTjHsEaDvIuO"),否则SymmetricSecurityKey构造失败,测试直接抛ArgumentException。

5.2 模拟 Refresh Token 流程:验证 HttpOnly Cookie 与黑名单机制

测试 Refresh Token 需要处理 Cookie,TestServer默认不维护 Cookie 容器,必须手动提取并附加:

[Fact] public async Task RefreshToken_ReturnsNewAccessToken_WhenValid() { // Arrange var client = _factory.CreateClient(); // Step 1: 登录获取初始 Token 和 Refresh Cookie var loginResponse = await client.PostAsJsonAsync("/api/auth/login", new { Username = "admin", Password = "123456" }); var cookies = loginResponse.Headers.GetValues("Set-Cookie").FirstOrDefault()?.Split(';').First(); // Step 2: 构造带 Refresh Cookie 的请求 var refreshClient = _factory.CreateClient(); refreshClient.DefaultRequestHeaders.Add("Cookie", cookies); // Act var refreshResponse = await refreshClient.PostAsJsonAsync("/api/auth/refresh", new { RefreshToken = "mock-refresh-token" }); // Assert refreshResponse.EnsureSuccessStatusCode(); var refreshContent = await refreshResponse.Content.ReadFromJsonAsync<RefreshResponse>(); Assert.NotNull(refreshContent.AccessToken); Assert.True(refreshContent.AccessToken.StartsWith("eyJ")); }

5.3 验证黑名单拦截:用TestServer模拟 Token 吊销场景

真正的安全测试必须覆盖“吊销后立即失效”。我们在WebApplicationFactory中注入内存黑名单,并在测试中主动添加jti:

// 在 WebApplicationFactory 中暴露黑名单 public class TestableWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { public ConcurrentBag<string> BlacklistedJtis { get; } = new(); protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { services.AddSingleton<ConcurrentBag<string>>(sp => BlacklistedJtis); }); } } // 测试中主动吊销 [Fact] public async Task AccessToken_Revoked_WhenJtiBlacklisted() { // Arrange var factory = new TestableWebApplicationFactory<Program>(); var client = factory.CreateClient(); // 获取初始 Token var loginResponse = await client.PostAsJsonAsync("/api/auth/login", new { Username = "admin", Password = "123456" }); var token = (await loginResponse.Content.ReadFromJsonAsync<LoginResponse>()).Token; // 提取 jti(解析 JWT Payload) var payload = token.Split('.')[1]; var paddedPayload = payload.PadRight(payload.Length + (4 - payload.Length % 4) % 4, '='); var json = Encoding.UTF8.GetString(Convert.FromBase64String(paddedPayload)); var jti = JsonSerializer.Deserialize<JsonElement>(json).GetProperty("jti").GetString(); // 主动加入黑名单 factory.BlacklistedJtis.Add(jti); // Act & Assert client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var protectedResponse = await client.GetAsync("/api/values"); Assert.Equal(HttpStatusCode.Unauthorized, protectedResponse.StatusCode); }

玄学提示:JWT Payload Base64Url 解码需补=符号,否则Convert.FromBase64String抛异常。这是 .NET6 测试中最隐蔽的坑——token.Split('.')[1]得到的字符串可能缺位,必须PadRight(..., '=')。


6. 生产就绪 checklist:从密钥轮换、日志审计到 SPA 项目中的 JWT 验证码实现

6.1 密钥轮换:如何在不中断服务的前提下更新 JWT 签名密钥?

硬编码密钥最大的问题是无法轮换。.NET6 支持多密钥验证,只需在TokenValidationParameters.IssuerSigningKeys中传入IEnumerable<SecurityKey>,框架会自动尝试每个密钥解签:

// Program.cs var keys = new List<SecurityKey> { new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtSettings:KeyV1"])), new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtSettings:KeyV2"])) // 新密钥 }; options.TokenValidationParameters = new TokenValidationParameters { // ... 其他配置 ValidateIssuerSigningKey = true, IssuerSigningKeys = keys, // ⚠️ 注意是复数 Keys,不是单数 Key // 当前主密钥仍设为 KeyV2,但旧 Token 用 KeyV1 也能验证 };

操作步骤:

  1. 在appsettings.json中新增JwtSettings:KeyV1和JwtSettings:KeyV2;
  2. 部署新版本,此时新签发 Token 用 KeyV2,旧 Token 仍可用 KeyV1 验证;
  3. 等待所有旧 Token 过期(如 30 分钟),删除 KeyV1 配置并重启。

6.2 日志审计:记录每一次鉴权失败的根因,而不是笼统的 401

默认的Microsoft.AspNetCore.Authentication.JwtBearer日志级别是Information,但关键错误(如签名无效、过期)只在Debug级别输出。生产环境必须提升日志粒度:

// Program.cs builder.Logging.AddFilter("Microsoft.AspNetCore.Authentication.JwtBearer", LogLevel.Debug);

然后在appsettings.Production.json中配置日志输出到文件或 ELK:

{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore.Authentication.JwtBearer": "Debug" } } }

你会看到类似日志:
Failed to validate the token. Microsoft.IdentityModel.Tokens.SecurityTokenExpiredException: IDX10223: Lifetime validation failed.
这比“Unauthorized”有用一百倍。

6.3 SPA 项目开发之 JWT 验证码实现:前端如何安全存储与自动续签?

SPA(Vue/React)不能存 Token 在 localStorage(XSS 可窃取),必须用httpOnlyCookie 存 Refresh Token,Access Token 存内存。典型流程:

  • 登录成功后,后端 Set-Cookierefresh_token;
  • 前端 Axios 请求拦截器:每次请求前检查内存中 Access Token 是否过期(解析exp字段);
  • 若过期,自动调/api/auth/refresh换新 Token,并更新内存中的 Access Token;
  • 若 Refresh Token 也过期,跳转登录页。
// utils/auth.js(Vue Composition API 示例) const useAuth = () => { const accessToken = ref(''); const refreshAccessToken = async () => { try { const response = await axios.post('/api/auth/refresh', {}, { withCredentials: true // ⚠️ 必须开启,否则不发送 Cookie }); accessToken.value = response.data.access_token; return response.data.access_token; } catch (error) { if (error.response?.status === 401) { window.location.href = '/login'; // 刷新 Token 失败,强制登出 } } }; const isTokenExpired = (token) => { if (!token) return true; const payload = JSON.parse(atob(token.split('.')[1])); return Date.now() >= payload.exp * 1000; }; const requestInterceptor = (config) => { if (isTokenExpired(accessToken.value)) { refreshAccessToken(); } config.headers.Authorization = `Bearer ${accessToken.value}`; return config; }; return { accessToken, requestInterceptor }; };

后悔药:我在三个 SPA 项目里都踩过这个坑——前端没做isTokenExpired校验,导致用户操作半小时后突然 401,体验极差。现在我的标准动作是:useAuthhook 初始化时就解析accessToken的exp,并用setTimeout在过期前 30 秒自动刷新,比拦截器更可靠。

希望帮到你。

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

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

日志分析策略:从日志分级、轮转到告警响应的运维实战指南

做运维和架构这行&#xff0c;时间久了都会有一个感觉&#xff1a;日志分析这件事&#xff0c;真正的门槛不在命令背得熟不熟&#xff0c;而在策略。命令是死的&#xff0c;grep、awk、journalctl 就那些参数&#xff0c;任何人花两周都能背下来&#xff1b;但面对一台故障机器…

作者头像 李华
网站建设 2026/10/5 16:09:27

DeepSeek临床决策支持:本地部署、RAG与推理链实战

简介&#xff1a;这份PDF文档面向医疗行业从业者、临床研究人员及对AI医疗落地感兴趣的开发者&#xff0c;系统讲解DeepSeek在临床决策场景中的应用路径。内容从临床决策现状与挑战切入&#xff0c;逐步展开DeepSeek技术原理、医疗数据处理、临床决策模型构建、算法优化、系统集…

作者头像 李华
网站建设 2026/10/5 16:08:08

特色农产品交易小程序开发:云开发与数据建模全流程实践

1. 先搞清楚需求&#xff1a;农产品交易小程序到底要解决谁的问题很多人做这种小程序&#xff0c;第一步就跳进写代码里了&#xff0c;结果页面做了一堆&#xff0c;真正上线跑单的时候才发现方向不对。我做完这整个项目后的第一个体会是&#xff1a;特色农产品交易系统的难点不…

作者头像 李华
网站建设 2026/10/5 16:06:31

AcWing算法学习全攻略:从基础课到周赛实战复盘

1. 为什么我把算法训练阵地选在了AcWing1.1 从刷题网站到系统性课程&#xff0c;差距在哪里说真的&#xff0c;我在接触AcWing之前&#xff0c;和大部分人一样&#xff0c;习惯性打开某个知名刷题网站&#xff0c;按标签刷题。今天碰一道链表&#xff0c;明天碰一道贪心&#x…

作者头像 李华
网站建设 2026/10/5 16:05:58

Ubuntu 20.04 下 RTL8852AE 无线网卡驱动安装与排查指南

如果你和我一样&#xff0c;在拯救者 R7000P 上装了 Ubuntu 20.04&#xff0c;开完机习惯性点一下右上角的网络菜单&#xff0c;结果 Wi-Fi 那一栏只剩一句冷冰冰的“未找到 Wi-Fi 适配器”&#xff0c;那八成就是被这块板载的 Realtek RTL8852AE 无线网卡给卡住了。这颗芯片属…

作者头像 李华
网站建设 2026/10/5 16:05:41

Gitee自动创建PR并接入评审模式:从设计到落地实践

接手团队代码协作流程这半年&#xff0c;最让我上火的不是业务代码本身&#xff0c;而是每天没完没了地给人提 PR。Gitee 上仓库一多&#xff0c;光是把分支提上来、选好 base、指派评审人这种机械操作&#xff0c;就能耗掉一上午。后来我索性写了一套自动创建 PR 的小工具&…

作者头像 李华