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 策略与安全属性
Cookie 的安全属性直接决定认证票据是否会被窃取或滥用,工程中应逐项确认:
| 属性 | 推荐值 | 作用与风险 |
|---|---|---|
| HttpOnly | true | 禁止 JavaScript 读取,降低 XSS 窃取 Cookie 风险 |
| Secure | Always(生产) | 仅 HTTPS 传输,防止明文链路泄露 |
| SameSite | Lax / Strict | 限制跨站携带,缓解 CSRF |
| Domain / Path | 最小范围 | 避免 Cookie 被无关子域或路径共享 |
| ExpireTimeSpan | 15~60 分钟 | 控制票据有效期,配合滑动过期 |
SameSite 的选择需要结合业务:Strict 最安全,但从外部站点跳转回本站时不会携带 Cookie,用户可能表现为“已登录却显示未登录”;Lax 允许顶级导航的 GET 请求携带 Cookie,是多数站点的平衡选择;None 必须同时设置 Secure,否则浏览器会拒绝写入,常用于需要跨站携带 Cookie 的嵌入场景。
生产环境务必启用 HTTPS 并设置 Cookie.SecurePolicy = CookieSecurePolicy.Always。若在反向代理后部署,还需正确配置 ForwardedHeaders,否则应用可能误判请求为 HTTP,导致 Secure Cookie 无法写入。
四、可执行的配置检查清单
下面这份清单可直接用于代码评审或上线前自查,字段均对应本节真实配置项:
| 检查项 | 期望 | 验证方式 |
|---|---|---|
| Cookie.Name | 自定义且不含敏感信息 | 浏览器 Application 面板查看 |
| HttpOnly | true | DevTools 中该列为 ✓ |
| Secure | 生产为 ✓ | HTTPS 下检查 Set-Cookie |
| SameSite | Lax 或 Strict | 响应头 Set-Cookie 属性 |
| 滑动过期 | 按业务决定是否开启 | 观察多次请求后过期时间是否顺延 |
| 登出 | Cookie 被清除 | 调用 /Logout 后 Cookie 消失 |
最后提醒一个常见误区:SignOutAsync 只清除当前浏览器的认证 Cookie,并不会使服务端已签发的票据立即失效。若需要“强制下线”或“改密后立即失效”,应结合安全戳(SecurityStamp)在 OnValidatePrincipal 中校验,或使用服务端会话存储。掌握登录、登出、滑动过期与安全属性的配合关系,Cookie 认证才能既好用又可靠。