跳到主要内容

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 / 403401 表示未认证或凭据无效,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 认证常用于同站点 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 的不同处理逻辑。

延伸阅读