4.8 JWT、Cookie 与 Policy 授权
认证解决“当前请求是谁发起的”,授权解决“这个身份是否允许执行当前操作”。ASP.NET Core 把两者拆成 Authentication 和 Authorization 两套机制:前者把请求转换为 ClaimsPrincipal,后者根据角色、Claim、Policy 或自定义规则做权限判断。
学习目标
- 能区分 JWT Bearer、Cookie 和 Policy 授权的职责。
- 能配置基础 JWT Bearer 认证和 Cookie 认证。
- 能用角色、Claim 和 Policy 保护 Minimal API 或 Controller。
- 能正确处理 401、403、Token 过期和前端登录态失效。
应用场景
- React 或移动端调用 Web API,使用
Authorization: Bearer <token>。 - 服务端渲染页面或同站点 Web 应用使用 Cookie 维持登录状态。
- 管理员接口只允许
Admin角色访问。 - 只有任务负责人可以编辑自己的任务。
核心概念
| 概念 | 说明 |
|---|---|
| Authentication Scheme | 认证方案,例如 JWT Bearer、Cookie、OpenID Connect |
ClaimsPrincipal | 当前用户身份对象,包含一个或多个身份和 Claim |
| Claim | 用户身份或权限声明,例如用户 ID、邮箱、角色、租户 ID |
| Role | 一类特殊权限声明,适合粗粒度角色控制 |
| Policy | 命名授权规则,可组合角色、Claim 和自定义 Requirement |
| 401 / 403 | 401 表示未认证或凭据无效,403 表示已认证但权限不足 |
案例:配置 JWT Bearer
JWT Bearer 常用于前后端分离 API。后端验证签名、签发者、接收方和过期时间;验证通过后,框架会把 Token 中的 Claim 转成 HttpContext.User。
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
var jwt = builder.Configuration.GetSection("Jwt");
var signingKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwt["SigningKey"]!));
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwt["Issuer"],
ValidAudience = jwt["Audience"],
IssuerSigningKey = signingKey,
ClockSkew = TimeSpan.FromMinutes(1)
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
JWT 的签名密钥必须放在安全配置中,例如用户机密、环境变量或云密钥管理服务。不要把生产密钥提交到仓库。
示例:配置 Cookie 认证
Cookie 认证常用于同站点 Web 应用。浏览器自动携带 Cookie,因此更容易受到 CSRF 影响,需要配合 SameSite、HTTPS 和防伪令牌策略。
using Microsoft.AspNetCore.Authentication.Cookies;
builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "todo_auth";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
options.LoginPath = "/login";
options.AccessDeniedPath = "/access-denied";
options.SlidingExpiration = true;
});
builder.Services.AddAuthorization();
API 项目如果使用 Cookie,前端跨域调用还要同时处理 CORS 凭据、CSRF 防护和 401/403 返回格式。前后端分离项目更常见的是 JWT 或基于后端会话的 BFF 模式。
示例:Policy 授权
Policy 把权限规则集中命名,避免每个接口手写重复判断。角色适合粗粒度控制,Claim 适合表达更具体的身份属性。
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("CanManageTodos", policy =>
policy.RequireAuthenticatedUser()
.RequireClaim("permission", "todos:write"));
});
app.MapPost("/todos", CreateTodoAsync)
.RequireAuthorization("CanManageTodos");
app.MapDelete("/admin/todos/{id:int}", DeleteTodoAsync)
.RequireAuthorization("AdminOnly");
Controller 中也可以使用同一个策略。
[ApiController]
[Route("api/admin/todos")]
[Authorize(Policy = "AdminOnly")]
public sealed class AdminTodosController : ControllerBase
{
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(
int id,
TodoAdminService service,
CancellationToken cancellationToken)
{
await service.DeleteAsync(id, cancellationToken);
return NoContent();
}
}
示例:读取当前用户
业务代码不要信任请求体里的 userId。当前用户应从认证上下文读取,再传入应用服务。
app.MapGet("/me/todos", async (
ClaimsPrincipal user,
TodoQueryService service,
CancellationToken cancellationToken) =>
{
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId is null)
{
return Results.Unauthorized();
}
var todos = await service.GetForUserAsync(userId, cancellationToken);
return Results.Ok(todos);
})
.RequireAuthorization();
更复杂的“只能编辑自己的任务”通常不应只靠角色判断,而应在应用服务或自定义 Authorization Handler 中结合资源所有者一起校验。
重点难点
UseAuthentication()必须在UseAuthorization()之前,否则授权阶段拿不到正确用户身份。- 401 和 403 要分清:未登录、Token 无效、Token 过期通常是 401;登录了但权限不够是 403。
- JWT 一旦签发,在过期前通常不能天然撤销;需要较短有效期、刷新令牌或服务端吊销列表配合。
- Cookie 自动随请求发送,便利但要考虑 CSRF;JWT 常放在 Header 中,便利于 API,但要防止 XSS 泄露。
- 前端路由守卫只改善体验,不能替代后端授权。
常见误区
| 误区 | 推荐做法 |
|---|---|
| 把用户 ID 从请求体传给后端 | 从 ClaimsPrincipal 读取当前用户 |
| 只校验前端按钮是否隐藏 | 后端每个敏感接口都要授权 |
| 把 JWT 密钥写进源码 | 使用用户机密、环境变量或密钥管理服务 |
| 角色越多越好 | 角色做粗粒度分组,细粒度权限用 Claim 或 Policy |
| 看到 403 就清空登录态 | 401 才通常需要重新登录,403 应提示权限不足 |
练习
- 给
POST /todos增加CanManageTodos策略。 - 在 Token 中加入
permission=todos:write,验证没有该 Claim 时返回 403。 - 把一个管理员 Controller 改成
[Authorize(Roles = "Admin")]。 - 设计前端对 401 和 403 的不同处理逻辑。
延伸阅读
- 本手册:依赖注入与中间件
- 本手册:配置、Options 与日志
- 本手册:认证、授权与 CORS