4. Cookie认证工程实践

Cookie 认证是 ASP.NET Core 中最经典、也最容易“看起来能跑、实际上不安全”的认证方式。它的核心机制是:用户提交凭据后,服务端用 HttpContext.SignInAsync 生成一个加密的认证票据(AuthenticationTicket),序列化后写入浏览器 Cookie;后续请求携带该 Cookie,认证中间件解密并重建 ClaimsPrincipal,从而完成身份识别。理解这条链路,才能正确配置登录、登出、过期与安全属性。

一、登录:从凭据校验到写入认证 Cookie

登录的本质是“校验凭据 + 签发票据”。在 Program.cs 中先注册 Cookie 认证方案:

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/Account/Login";
        options.LogoutPath = "/Account/Logout";
        options.AccessDeniedPath = "/Account/Denied";
        options.Cookie.Name = ".App.Auth";
        options.Cookie.HttpOnly = true;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.Cookie.SameSite = SameSiteMode.Lax;
        options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
        options.SlidingExpiration = true;
    });

登录端点中,先验证用户名与密码(示例中假设校验通过),再构造声明并签发:

var claims = new List<Claim>
{
    new(ClaimTypes.NameIdentifier, user.Id.ToString()),
    new(ClaimTypes.Name, user.UserName),
    new(ClaimTypes.Role, "Admin")
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme,
    new ClaimsPrincipal(identity),
    new AuthenticationProperties
    {
        IsPersistent = rememberMe,               // 勾选“记住我”则持久化
        ExpiresUtc = DateTimeOffset.UtcNow.AddDays(7),
        AllowRefresh = true
    });

这里有两个关键点:IsPersistent=false 时 Cookie 是会话级(关闭浏览器即失效);IsPersistent=true 且设置 ExpiresUtc 时才会写入持久化过期时间。注意 ExpiresUtc 与 ExpireTimeSpan 的关系:前者是绝对过期上限,后者是票据有效期,滑动过期在两者之间取较小约束。

二、登出与滑动过期

登出只需调用 SignOutAsync,它会删除认证 Cookie 并触发 OnSigningOut 事件:

await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

滑动过期(SlidingExpiration)是 Cookie 认证的默认行为之一:当请求经过认证且票据剩余有效期超过一半时,中间件会重新签发一张新票据,把过期时间向后顺延 ExpireTimeSpan。例如 ExpireTimeSpan=30min,用户在 20 分钟时发起请求,剩余 10 分钟小于 15 分钟(一半),票据会被刷新,过期时间重置为新的 30 分钟。这带来“活跃用户不掉线”的体验,但也意味着只要持续访问,会话可以无限延长。若业务要求绝对上限,应同时设置 ExpiresUtc,或在 OnValidatePrincipal 中自行判断首次登录时间并拒绝续期。

Cookie 认证:登录 → 滑动过期 → 登出 浏览器 认证中间件 票据存储 POST /Login(凭据) SignInAsync 生成票据 Set-Cookie: .App.Auth(HttpOnly/Secure/SameSite) 请求携带 Cookie(第20分钟) 剩余10min < 15min 触发续期 重签票据,过期顺延30min 刷新 Set-Cookie POST /Logout SignOutAsync 删除票据

三、Cookie 策略与安全属性

Cookie 的安全属性直接决定认证票据是否会被窃取或滥用,工程中应逐项确认:

属性推荐值作用与风险
HttpOnlytrue禁止 JavaScript 读取,降低 XSS 窃取 Cookie 风险
SecureAlways(生产)仅 HTTPS 传输,防止明文链路泄露
SameSiteLax / Strict限制跨站携带,缓解 CSRF
Domain / Path最小范围避免 Cookie 被无关子域或路径共享
ExpireTimeSpan15~60 分钟控制票据有效期,配合滑动过期

SameSite 的选择需要结合业务:Strict 最安全,但从外部站点跳转回本站时不会携带 Cookie,用户可能表现为“已登录却显示未登录”;Lax 允许顶级导航的 GET 请求携带 Cookie,是多数站点的平衡选择;None 必须同时设置 Secure,否则浏览器会拒绝写入,常用于需要跨站携带 Cookie 的嵌入场景。

生产环境务必启用 HTTPS 并设置 Cookie.SecurePolicy = CookieSecurePolicy.Always。若在反向代理后部署,还需正确配置 ForwardedHeaders,否则应用可能误判请求为 HTTP,导致 Secure Cookie 无法写入。

四、可执行的配置检查清单

下面这份清单可直接用于代码评审或上线前自查,字段均对应本节真实配置项:

检查项期望验证方式
Cookie.Name自定义且不含敏感信息浏览器 Application 面板查看
HttpOnlytrueDevTools 中该列为 ✓
Secure生产为 ✓HTTPS 下检查 Set-Cookie
SameSiteLax 或 Strict响应头 Set-Cookie 属性
滑动过期按业务决定是否开启观察多次请求后过期时间是否顺延
登出Cookie 被清除调用 /Logout 后 Cookie 消失

最后提醒一个常见误区:SignOutAsync 只清除当前浏览器的认证 Cookie,并不会使服务端已签发的票据立即失效。若需要“强制下线”或“改密后立即失效”,应结合安全戳(SecurityStamp)在 OnValidatePrincipal 中校验,或使用服务端会话存储。掌握登录、登出、滑动过期与安全属性的配合关系,Cookie 认证才能既好用又可靠。