×

Axios 拦截器不用写了——.NET 项目换 Cookie 认证后,前端代码彻底清爽了

独孤求败 独孤求败 发表于2026-06-18 11:26:33 浏览35 评论0

抢沙发发表评论

你是不是还在用 localStorage 存 Token,然后每个请求手动塞 Authorization 头?页面里万一被注入了 XSS 脚本,整个 Token 直接裸奔。

换成 HttpOnly 的 Cookie 呢?浏览器自动携带,脚本压根读不到,XSS 攻击者直接扑空。前端不用在每个请求里手动塞 Token 了,拦截器里那些 Token 注入、刷新、过期跳转的逻辑统统可以删掉。

这篇不聊虚的,直接带你过一遍 .NET 6+ 里 Cookie 认证的安全配置、登录/退出逻辑,以及那些最容易踩的坑。代码贴好,拿去就能落地。

本文定位:

本文使用的是 .NET 自带的、由 Data Protection 安全加密的 Cookie 认证票据(Ticket),而非独立的 JWT Bearer 中间件。相比 JWT,这套方案更加开箱即用,省去了自己生成、签名、验证 JWT 的繁琐逻辑,同时利用 HttpOnly 标记防止 XSS 窃取凭证。

文中代码均基于 ASP.NET Core 自带的 CookieAuthenticationHandler,不涉及独立的 JWT Bearer 中间件。

Cookie 认证是什么

在 ASP.NET Core 的语境下,基于 Cookie 的身份认证,本质上是一种认证方案(Authentication Scheme)——每次请求时,Cookie 认证中间件从 Cookie 中读取加密票据,解密并还原出 ClaimsPrincipal 对象,赋值给 HttpContext.User,使后续业务逻辑能够识别当前请求对应的用户身份。

官方文档的定义很直接:这是一种"由 Cookie 构造用户身份"的认证方案。背后做的事情大致是:用户登录成功后,服务器将用户标识(Claims)序列化、加密后写入 Cookie;后续请求中,Cookie 认证中间件从 Cookie 中还原出 ClaimsPrincipal,赋值给 HttpContext.User

官方文档:


https://learn.microsoft.com/zh-cn/aspnet/core/security/authentication/cookie
图片

核心机制:

1. 客户端承载的认证票据: HTTP 本身是无状态的,Cookie 在客户端存储加密的认证票据,使每次请求都能携带身份信息,从而让服务端能够"识别"请求来源。ASP.NET Core 的 Cookie 认证中间件封装了 Cookie 的读写、加密、过期管理等底层细节,开发者只需关注业务逻辑。

2. 声明式身份标识(Claims-based Identity): 用户信息以 Claim 的形式存储在 Cookie 中,框架自动完成序列化/反序列化。认证只管"你是谁",授权再判断"你能做什么"。

3. 内置安全防护机制: Cookie 认证默认启用 Data Protection API 进行加密,防止篡改。同时提供 HttpOnlySecureSameSite 等安全属性的配置入口。

4. 与 ASP.NET Core 生态深度集成: Cookie 认证与 [Authorize] 特性、授权策略、会话管理等机制无缝协作,在传统 MVC 和 Razor Pages 项目中几乎是开箱即用的标准方案。

需要明确的是:Cookie 认证和 Session 是两回事。

- Cookie 认证:认证票据存储在 客户端(Cookie 中),服务端不保存会话状态。
 - Session:会话数据存储在 服务端(内存/Redis),客户端只存 Session ID。

两者可以独立使用,也可以组合使用(Cookie 认证 + Session 存储额外数据)。本文讨论的是 Cookie 认证本身,不涉及 Session。

怎么引入

需要安装的包

在 .NET 6 及以上 版本中,Microsoft.AspNetCore.Authentication.Cookies 通常已经包含在 Web SDK 的共享框架中,一般不需要单独安装。如果项目模板较旧或需要显式引用,执行以下命令:

dotnet add package Microsoft.AspNetCore.Authentication.Cookies
图片

Program.cs 完整配置

以下是一个完整的 Cookie 认证配置,包含安全属性设置,并根据环境动态决定 SecurePolicy

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
var builder = WebApplication.CreateBuilder(args);
// ============ 1. 添加认证服务 ============
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        // 登录页路径(未认证时跳转)
        options.LoginPath = "/Account/Login";
        // 无权限时跳转
        options.AccessDeniedPath = "/Account/AccessDenied";
        // ===== Cookie 安全配置 =====
        options.Cookie.HttpOnly = true;    // 禁止 JavaScript 访问
        // 根据环境动态决定:本地开发 SameAsRequest,生产环境 Always
        options.Cookie.SecurePolicy = builder.Environment.IsDevelopment()
            ? CookieSecurePolicy.SameAsRequest
            : CookieSecurePolicy.Always;
        options.Cookie.SameSite = SameSiteMode.Lax;
        options.Cookie.Name = ".MyApp.Auth";
        // ===== 会话过期配置 =====
        options.ExpireTimeSpan = TimeSpan.FromHours(2);
        options.SlidingExpiration = true;
        // ===== 前后端分离场景:禁止 302 重定向,直接返回 401 =====
        options.Events = new CookieAuthenticationEvents
        {
            OnRedirectToLogin = context =>
            {
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                return Task.CompletedTask;
            },
            OnRedirectToAccessDenied = context =>
            {
                context.Response.StatusCode = StatusCodes.Status403Forbidden;
                return Task.CompletedTask;
            }
        };
    });
// ============ 2. 添加授权服务 ============
builder.Services.AddAuthorization();
// ============ 3. 添加控制器支持 ============
builder.Services.AddControllersWithViews();
// ============ 4. CORS 配置(跨域场景需要) ============
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend"policy =>
    {
        // 注意:URL 结尾千万不能加斜杠 "/"
        // 错误:https://your-frontend.com/
        // 正确:https://your-frontend.com
        // 加了斜杠会导致 .NET CORS 中间件识别失败,浏览器报 CORS 错误
        policy.WithOrigins(
                "https://你的前端域名.com",  // 生产环境
                "http://localhost:3000",     // 本地开发(React/Vue/Next.js 默认)
                "http://localhost:5173"      // 本地开发(Vite 默认)
            )
            .AllowCredentials()  // 必须加上,允许携带 Cookie
            .AllowAnyHeader()
            .AllowAnyMethod();
    });
});
// ============ 5. HSTS + HTTPS 强制 ============
builder.Services.AddHsts(options =>
{
    options.MaxAge = TimeSpan.FromDays(365);
    options.IncludeSubDomains = true;
    options.Preload = true;
});
var app = builder.Build();
// ============ 6. 中间件顺序 ============
if (!app.Environment.IsDevelopment())
{
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseCors("AllowFrontend");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();

各安全属性的作用:

属性
作用
推荐值
HttpOnly
防止客户端脚本访问 Cookie,降低 XSS 攻击风险
true
SecurePolicy
限制 Cookie 仅在 HTTPS 连接中传输
开发:SameAsRequest,生产:Always
SameSite
限制跨站请求携带 Cookie,可降低 CSRF 风险
SameSiteMode.Lax
ExpireTimeSpan
认证会话有效期
根据业务需求设置
SlidingExpiration
每次请求刷新过期时间,延长会话
建议开启

关于 SecurePolicy 动态配置的说明:

生产环境强制使用 CookieSecurePolicy.Always,确保 Cookie 仅通过 HTTPS 传输,防止中间人攻击。

本地开发时,通过 builder.Environment.IsDevelopment() 自动切换为 CookieSecurePolicy.SameAsRequest,避免本地 HTTP 调试时 Cookie 无法写入。读者无需手动修改配置文件,直接复制代码即可在本地和生产环境正常运行。

关于 SameSite=Lax 的说明:

SameSite=Lax 可降低跨站请求携带 Cookie 的概率,对 GET 请求的 CSRF 有一定防护作用,但不能完全替代 CSRF Token涉及状态变更的请求(POST/PUT/DELETE),仍建议配合 [ValidateAntiForgeryToken] 使用。

关于 OnRedirectToLogin 的说明:

在前后端分离项目中,如果未认证,框架默认会尝试 302 重定向到登录页。前端 Ajax 请求遇到 302 会导致 CORS 错误或无法正确处理。重写 OnRedirectToLogin 直接返回 401,前端可以统一在响应拦截器中处理。

关于 CORS 配置中 WithOrigins 的说明:

本地开发时,前端域名通常是 http://localhost:3000 或 http://localhost:5173。上方的代码示例中已将本地开发域名一并列出,读者可根据实际端口调整。

⚠️ CORS 高频踩坑点:WithOrigins 中的 URL 结尾千万不能加斜杠 /

这是 .NET CORS 中间件最常见的错误之一:

// 错误写法(末尾带斜杠)
policy.WithOrigins("https://your-frontend.com/")
// 正确写法(末尾不带斜杠)
policy.WithOrigins("https://your-frontend.com")

如果 URL 末尾加了斜杠,.NET 的 CORS 中间件在比对请求来源时会识别失败,浏览器报 CORS 错误。复制代码时务必检查。

验证是否接入成功

1. 在任意 Controller 中给 Action 加上 [Authorize] 特性,未登录时访问该地址应返回 401(前后端分离模式)或跳转到配置的 LoginPath
 2. 登录成功后检查浏览器开发者工具的 Application/Cookies 面板,应能看到名为 .MyApp.Auth(或自定义名称)的 Cookie。

快速上手

以下示例均基于 .NET 6+,假设已按上一节完成配置。

示例一:登录接口(返回 JSON,适配前后端分离)

用户提交账号密码,验证通过后调用 SignInAsync 写入认证 Cookie,并返回 JSON 状态,由前端控制页面跳转

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
[ApiController]
[Route("api/[controller]")]
public class AccountController : ControllerBase
{
    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginRequest request)
    {
        // 实际项目中应从数据库验证
        if (request.Username != "admin" || request.Password != "123456")
        {
            return Unauthorized(new { success = false, message = "用户名或密码错误" });
        }
        // 构造用户身份声明
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, request.Username),
            new Claim(ClaimTypes.Role, "Admin")
        };
        var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
        var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
        // 写入认证 Cookie
        await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal);
        // 返回 JSON,由前端控制跳转
        return Ok(new
        {
            success = true,
            message = "登录成功",
            returnUrl = request.ReturnUrl
        });
    }
}
public class LoginRequest
{
    public string Username { getset; } = string.Empty;
    public string Password { getset; } = string.Empty;
    public string? ReturnUrl { getset; }  // 前端可传登录后跳转地址,不传则走前端默认
}

预期输出: 登录成功后,浏览器收到一个带有 HttpOnlySecure(生产环境)、SameSite=Lax 标志的认证 Cookie,后续请求自动携带。

验证步骤: 在受保护的 Action 上加 [Authorize],未登录访问返回 401;登录后再次访问可正常进入。

示例二:前端代码前后对比

改造前(localStorage 存 Token,axios 手动塞 Header):

// 每个请求都要手动处理
const token = localStorage.getItem('token');
axios.get('/api/data', {
    headers: {
        'Authorization'`Bearer ${token}`
    }
});

改造后(HttpOnly Cookie,同域场景):

// 同域场景:浏览器自动携带,前端无感知
axios.get('/api/data');

跨域场景必须配置:

如果前端和后端不同域,浏览器默认不会携带 Cookie。需要在 axios 中开启 withCredentials

// 方式一:全局开启(推荐)
axios.defaults.withCredentials = true;
// 方式二:单个请求开启
axios.get('https://api.yourdomain.com/api/data', {
    withCredentials: true  // 关键:告诉浏览器携带 Cookie
});

跨域场景注意事项:

开启 withCredentials: true 后,前端所有跨域请求都会携带 Cookie。此时后端 CORS 必须配置具体域名(不能用 *),并显式设置 .AllowCredentials()

如果后端未正确配置,浏览器会报:

The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*'

同时,开启后如果请求返回 401,浏览器不会在控制台显示完整的错误信息(跨域安全策略),排查时需要结合后端日志。

如果项目中有几十甚至上百个 API 调用,省下来的代码量和维护成本相当可观。更重要的是,HttpOnly 标记让脚本无法读取 Cookie,XSS 攻击者拿不到认证凭证。

示例三:退出登录——必须写,不能忘

很多项目只做了登录,退出时只清前端页面、不处理后端 Cookie,导致 Cookie 依然有效,存在安全隐患。正确的做法是在后端调用 SignOutAsync 清除认证 Cookie。

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
[Authorize]  // 必须加,防止匿名用户盲调
public class AccountController : ControllerBase
{
    [HttpPost("logout")]
    public async Task<IActionResult> Logout()
    {
        // SignOutAsync 会通知浏览器删除认证 Cookie,无需手动 Delete
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        return Ok(new { success = true, message = "已退出登录" });
    }
}

为什么必须写退出接口:

SignOutAsync 会向浏览器发送删除 Cookie 的响应头(Set-Cookie 过期时间为过去时间),通知浏览器清除对应的 Cookie。
 - 如果不调用 SignOutAsync,即使前端清除了页面状态,Cookie 仍然有效,攻击者可能利用已过期的会话进行重放攻击
 - 必须加上 [Authorize],防止匿名用户盲调该接口。
 - 不需要额外调用 Response.Cookies.Delete,两者重复且可能因 PathDomain 不一致导致删除失败。
 - 千万不要遍历 Request.Cookies.Keys 全量删除——同一个域名下可能还有前端框架的用户行为追踪 Cookie、主题配置、多语言标识、第三方统计脚本 Cookie 等,一并删除会导致页面其他功能异常。

示例四:Cookie 大小控制——避免"登录失败但没报错"

ASP.NET Core 的 Cookie 认证默认会将整个 ClaimsPrincipal 序列化进 Cookie。当 Claim 过多时,Cookie 可能超过浏览器限制(通常单个 Cookie 约 4KB),导致静默认证失败。

问题代码(容易导致 Cookie 过大):

var claims = new List<Claim>
{
    new Claim(ClaimTypes.Name, username),
    new Claim(ClaimTypes.Email, email),
    // 不要一股脑把几十个权限都塞进 Claim
    // 每个 permission 都是一个 Claim,累积起来轻松超过 4KB
};

改进方案——精简 Claim:

var claims = new List<Claim>
{
    new Claim(ClaimTypes.Name, username),
    new Claim(ClaimTypes.Role, "Admin"),
    // 只存必要的身份标识,权限检查走服务端数据库或缓存
};

监控 Cookie 大小的方法: 在浏览器开发者工具的 Application → Cookies 中查看 Cookie 的 Value 长度。如果接近 4000 字符,就需要精简。

常见报错与修复

报错 1:401 Unauthorized 而非跳转登录页

  • 触发条件:

     在前后端分离项目中,未登录时前端 Ajax 请求收到 401,而非预期的重定向。
  • 修复方式:

     在 AddCookie 配置中重写 OnRedirectToLogin,直接返回 401(见 Program.cs 配置)。

报错 2:跨域请求中 Cookie 未携带

  • 触发条件:

     前端和后端不同域,前端未配置 withCredentials: true 或后端未配置 .AllowCredentials()
  • 修复方式:

前端(axios):

// 全局开启
axios.defaults.withCredentials = true;

后端(.NET):

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend"policy =>
    {
        policy.WithOrigins(
                "https://你的前端域名.com",
                "http://localhost:3000",
                "http://localhost:5173"
            )
            .AllowCredentials()  // 必须加上
            .AllowAnyHeader()
            .AllowAnyMethod();
    });
});

报错 3:本地开发 HTTP 环境下 Cookie 写不进去

  • 触发条件:SecurePolicy

     设置为 Always,但本地是 HTTP。
  • 修复方式:

     使用 builder.Environment.IsDevelopment() 动态切换为 SameAsRequest(见 Program.cs 配置)。

报错 4:分布式部署下用户反复被踢下线

  • 触发条件:

     多实例部署,各实例 DataProtection 密钥不同,A 节点加密的 Cookie 在 B 节点无法解密。
  • 修复方式:

     集中存储 DataProtection 密钥(见下文"实战建议")。

报错 5:登录成功后 Axios 解析 HTML 报错

  • 触发条件:

     前后端分离项目中,登录接口返回了 Redirect 或 View()
  • 修复方式:

     登录接口统一返回 Ok(new { success = true }),由前端控制跳转(见示例一)。

报错 6:CORS 报错 "The 'Access-Control-Allow-Origin' header..."

  • 触发条件:WithOrigins

     中的 URL 末尾加了斜杠 /
  • 修复方式:

     去掉 URL 末尾的斜杠,WithOrigins("https://your-frontend.com") 而非 WithOrigins("https://your-frontend.com/")

适用场景

分布式场景:

Cookie 认证本身是服务端无状态的(解密和验证在每次请求中进行),但解密依赖 Data Protection 的密钥。在分布式部署(多实例)时,需要确保所有实例使用相同的密钥环(Key Ring),否则会出现"在某台机器登录,另一台机器解密失败"的问题。可以通过将密钥存储在 Redis 或 Azure Key Vault 中解决。

单体应用(含 Razor Pages/MVC):

这是 Cookie 认证最经典的适用场景。前后端同域、页面跳转为主、天然支持浏览器自动携带,几乎不需要额外配置就能跑起来,是 .NET 6+ 单体项目的首选认证方案。

前置条件与风险提示:

- 生产环境建议启用 HTTPS(配合 SecurePolicy = Always + HSTS),否则 Cookie 在明文传输中可能被截获。
 - 建议开启 HttpOnly,否则 XSS 漏洞可直接窃取认证票据。
 - 需根据业务合规要求(如 GDPR)配置 Cookie 同意策略。
 - 纯 API 服务(无浏览器前端):不需要 Cookie 认证,应考虑 JWT Bearer 或 API Key。
 - 移动端 App:没有 Cookie 存储机制,不应依赖 Cookie 认证。

实战建议

坑位一:分布式部署下认证票据解密失败(P0 级别)

ASP.NET Core 的 Cookie 认证底层使用 Data Protection API 对票据进行加密和解密。在单机部署时,密钥默认存储在本地用户目录;在多实例部署时,不同实例的密钥不同,导致 A 节点加密的 Cookie 在 B 节点无法解密,用户会被反复踢下线。

  • 触发条件:

     项目从单机扩展到多实例部署,未同步 Data Protection 密钥。
  • 错误表现:

     用户在 A 实例登录后,请求被负载均衡转发到 B 实例,B 实例无法解密 Cookie,返回 401 并重定向到登录页,用户反复被踢下线。
  • 修复策略:

     使用 PersistKeysToFileSystem 将密钥存储在共享目录,或使用 PersistKeysToAzureBlobStorage / PersistKeysToStackExchangeRedis 集中存储。

builder.Services.AddDataProtection()
    .PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect("redis-connection"))
    .SetApplicationName("SharedAppName");

坑位二:退出接口未加 [Authorize] 导致安全漏洞

  • 触发条件:

     退出登录接口没有 [Authorize] 特性。
  • 错误表现:

     匿名用户或爬虫可无限制地盲调 Logout 接口,空耗服务器资源。
  • 修复策略:

     退出接口加上 [Authorize] 特性(见示例三)。

坑位三:前后端分离下登录接口返回 Redirect 导致前端报错

  • 触发条件:

     在前后端分离项目中,登录接口返回了 Redirect 或 View()
  • 错误表现:

     Axios 尝试解析 HTML 页面,触发跨域重定向错误或 JSON 解析失败。
  • 修复策略:

     登录接口统一返回 Ok(new { success = true }),由前端控制页面跳转(见示例一)。

坑位四:Cookie 体积超限导致静默失败

  • 触发条件:

     将用户菜单树、全部权限点、用户资料等大量数据一股脑塞进 Claim。
  • 错误表现:

     登录时一切正常,但部分请求(特别是 POST)出现 400 错误或直接无响应,浏览器控制台报 431 Request Header Fields Too Large 或 Cookie 被静默截断。
  • 修复策略:

     Claim 中只保留必要的身份标识(UserId、UserName、Role)。其他数据在登录成功后通过服务端缓存存储,以 UserId 为 Key 按需加载。

坑位五:SameSite=Lax 不能完全替代 CSRF Token

  • 触发条件:

     仅依赖 SameSite=Lax 防护 CSRF,但攻击者通过 POST 表单或其他方式跨站提交请求。
  • 错误表现:

     敏感操作(如修改密码、转账)被 CSRF 攻击。
  • 修复策略:SameSite=Lax

     可降低跨站请求携带 Cookie 的概率,对 GET 请求的 CSRF 有一定防护作用,但不能完全替代 CSRF Token。涉及状态变更的请求(POST/PUT/DELETE),仍建议配合 [ValidateAntiForgeryToken] 使用。

坑位六:CORS WithOrigins URL 末尾多了斜杠

  • 触发条件:WithOrigins("https://your-frontend.com/")

     末尾带了斜杠。
  • 错误表现:

     浏览器报 CORS 错误,Access-Control-Allow-Origin 不匹配。
  • 修复策略:

     去掉 URL 末尾的斜杠。

选型建议

更适合 Cookie 认证的场景:

- 传统 MVC / Razor Pages 项目,页面跳转为主,且前后端同域部署。
 - 团队期望框架自动处理 Cookie 读写、加密、过期轮转,不希望手动维护 Token 的存储和携带逻辑。
 - 并发量中等(日均百万级请求以内),且对认证延迟敏感度不高的业务系统(如后台管理、企业 OA)。
 - 希望前端代码尽量精简,不想在每个请求里手动塞 Token。

不太适合的场景:

- 纯 API 服务(无浏览器前端):不需要 Cookie 认证,应考虑 JWT Bearer 或 API Key。
 - 移动端 App:没有 Cookie 存储机制,不应依赖 Cookie 认证。
 - 需要极致的前后端分离(前端独立部署到 CDN,后端 API 在另一域名),跨域携带 Cookie 需要额外配置 SameSite=None; Secure

团队规模 / 架构
推荐方案
原因
< 3 人小团队 / 单体项目
Cookie 认证
最低心智负担,框架开箱即用
5-10 人团队 / 2-3 个服务
网关层统一认证
页面跳转场景下体验更好
运维能力一般
单机部署用 Cookie
无需额外维护密钥同步基础设施
上线周期紧迫
Cookie 认证
dotnet new mvc --auth Individual
 几乎零配置

总结收尾

回到开头的问题:localStorage 存 Token 确实省了几行前端代码,但 XSS 面前它脆得像张纸。换成 HttpOnly 的 Cookie 认证,攻击者脚本拿不到票据,安全感瞬间拉满。前端不需要在每个请求里手动塞 Token 了,代码量肉眼可见地减少。

但这不等于无脑配置一把梭——Cookie 安全属性要逐个确认,生产环境 SecurePolicy 必须根据环境动态配置,退出接口要加 [Authorize],分布式部署下 DataProtection 密钥的同步也得提前规划好。CORS 配置时 WithOrigins 的 URL 末尾千万别加斜杠。

可直接执行的三条落地建议: 1. 确认认证 Cookie 开启了 HttpOnly + SecurePolicy 根据环境正确配置(开发 SameAsRequest / 生产 Always),配合 UseHsts()
 2. 检查退出接口是否添加了 [Authorize] 并调用了 SignOutAsync,不要额外手动 Delete,更不要用 foreach 遍历全删。
 3. 若项目已上线且有多实例,排查 DataProtection 密钥是否集中存储(Redis / Azure Key Vault / 共享目录)。


群贤毕至

访客