ASP.NET Core 鉴权与授权
大约 9 分钟约 2727 字
ASP.NET Core 鉴权与授权
简介
在 ASP.NET Core 中,鉴权(Authentication)和授权(Authorization)是两个层次不同但紧密关联的能力:鉴权解决“你是谁”,授权解决“你能做什么”。真实项目里,Cookie、JWT、Claims、Role、Policy、Resource-based Authorization 往往需要一起设计,否则很容易出现“能登录但没权限”“有 Token 但访问仍失败”“角色很多却依旧无法表达细粒度权限”等问题。
特点
实现
鉴权 vs 授权:先分清职责
Authentication(鉴权):
- 识别当前请求来自谁
- 常见载体:Cookie、JWT、Access Token
- 结果:生成 ClaimsPrincipal
Authorization(授权):
- 判断当前用户是否允许执行某个操作
- 常见手段:Role、Claim、Policy、Resource-based Authorization
- 结果:Allow / Deny最常见误区:
- 用户登录成功 ≠ 一定有权限
- Token 有效 ≠ 一定能访问所有接口
- Role 足够 ≠ 不需要更细粒度的 PolicyCookie 认证(适合传统 Web / 后台系统)
using Microsoft.AspNetCore.Authentication.Cookies;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/login";
options.AccessDeniedPath = "/forbidden";
options.Cookie.Name = "sunnyfan.auth";
options.ExpireTimeSpan = TimeSpan.FromHours(8);
options.SlidingExpiration = true;
options.Events.OnRedirectToLogin = async context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.ContentType = "application/json; charset=utf-8";
await context.Response.WriteAsJsonAsync(new { code = 401, message = "未登录" });
};
options.Events.OnRedirectToAccessDenied = async context =>
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
context.Response.ContentType = "application/json; charset=utf-8";
await context.Response.WriteAsJsonAsync(new { code = 403, message = "无权限" });
};
});
builder.Services.AddAuthorization();using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.Security.Claims;
[ApiController]
[Route("api/account")]
public class AccountController : ControllerBase
{
[HttpPost("login")]
public async Task<IActionResult> LoginAsync(LoginRequest request)
{
// 示例:真实项目应校验用户密码
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, "1001"),
new(ClaimTypes.Name, request.Account),
new(ClaimTypes.Email, "sunnyfan@example.com"),
new(ClaimTypes.Role, "Admin"),
new("permission", "orders.read")
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
principal,
new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddHours(8)
});
return Ok(new { message = "登录成功" });
}
[HttpPost("logout")]
public async Task<IActionResult> LogoutAsync()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok(new { message = "退出成功" });
}
}
public record LoginRequest(string Account, string Password);var app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();JWT 认证(适合前后端分离 API)
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = builder.Configuration["Jwt:Audience"],
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"]!)),
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(30)
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = async context =>
{
context.NoResult();
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.ContentType = "application/json; charset=utf-8";
await context.Response.WriteAsJsonAsync(new { code = 401, message = "Token 验证失败" });
},
OnChallenge = async context =>
{
context.HandleResponse();
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.ContentType = "application/json; charset=utf-8";
await context.Response.WriteAsJsonAsync(new { code = 401, message = "未提供有效 Token" });
}
};
});public class JwtTokenService
{
private readonly IConfiguration _configuration;
public JwtTokenService(IConfiguration configuration)
{
_configuration = configuration;
}
public string GenerateToken(string userId, string userName, IEnumerable<string> roles)
{
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, userId),
new(ClaimTypes.Name, userName),
new("permission", "orders.read")
};
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:SecretKey"]!));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddHours(2),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}{
"Jwt": {
"Issuer": "sunnyfan.local",
"Audience": "sunnyfan.api",
"SecretKey": "replace-with-a-long-random-secret-key"
}
}授权:Role、Claim、Policy
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdmin", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("CanManageOrders", policy =>
policy.RequireClaim("permission", "orders.manage"));
options.AddPolicy("ChinaOnly", policy =>
policy.RequireClaim(ClaimTypes.Country, "China", "Chinese"));
});[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
[HttpGet("{id}")]
[Authorize]
public IActionResult Get(int id)
{
return Ok(new { id, status = "Paid" });
}
[HttpDelete("{id}")]
[Authorize(Policy = "CanManageOrders")]
public IActionResult Delete(int id)
{
return Ok(new { message = $"订单 {id} 已删除" });
}
}app.MapGet("/api/admin/dashboard", [Authorize(Roles = "Admin")] () =>
{
return Results.Ok(new { users = 100, orders = 200 });
});Resource-based Authorization(资源级授权)
using Microsoft.AspNetCore.Authorization;
public class OrderOwnerRequirement : IAuthorizationRequirement
{
}
public class OrderOwnerHandler : AuthorizationHandler<OrderOwnerRequirement, Order>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OrderOwnerRequirement requirement,
Order resource)
{
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == resource.UserId.ToString())
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
public class Order
{
public int Id { get; set; }
public long UserId { get; set; }
}builder.Services.AddSingleton<IAuthorizationHandler, OrderOwnerHandler>();[HttpGet("mine/{id}")]
public async Task<IActionResult> GetMyOrder(
int id,
[FromServices] IAuthorizationService authorizationService)
{
var order = new Order { Id = id, UserId = 1001 };
var result = await authorizationService.AuthorizeAsync(User, order, new OrderOwnerRequirement());
if (!result.Succeeded)
return Forbid();
return Ok(order);
}优点
Claims 转换与增强
自定义 Claims 转换
/// <summary>
/// Claims 转换 — 在认证后添加/修改/删除 Claims
/// </summary>
public class ClaimsTransformation : IClaimsTransformation
{
private readonly IUserService _userService;
public ClaimsTransformation(IUserService userService)
{
_userService = userService;
}
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
if (principal.Identity is not ClaimsIdentity identity)
return principal;
var userIdClaim = identity.FindFirst(ClaimTypes.NameIdentifier);
if (userIdClaim == null)
return principal;
var userId = int.Parse(userIdClaim.Value);
// 从数据库加载用户的额外 Claims
var permissions = await _userService.GetPermissionsAsync(userId);
foreach (var perm in permissions)
{
// 避免重复添加
if (!identity.HasClaim("permission", perm))
{
identity.AddClaim(new Claim("permission", perm));
}
}
// 添加部门信息
var department = await _userService.GetDepartmentAsync(userId);
if (department != null)
{
identity.AddClaim(new Claim("department", department.Name));
identity.AddClaim(new Claim("department_id", department.Id.ToString()));
}
return principal;
}
}
// 注册
builder.Services.AddScoped<IClaimsTransformation, ClaimsTransformation>();JWT 高级配置
多密钥支持与密钥轮换
/// <summary>
/// 多密钥 JWT 验证 — 支持密钥轮换
/// </summary>
public class MultiKeyJwtBearerOptions
{
public static void Configure(JwtBearerOptions options, IConfiguration config)
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = config["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = config["Jwt:Audience"],
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(30),
// 支持多个签名密钥
IssuerSigningKeys = GetSigningKeys(config)
};
}
private static List<SecurityKey> GetSigningKeys(IConfiguration config)
{
var keys = new List<SecurityKey>();
// 当前密钥
var currentKey = config["Jwt:SecretKey"];
if (!string.IsNullOrEmpty(currentKey))
{
keys.Add(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(currentKey)));
}
// 历史密钥(用于验证旧 Token)
var previousKey = config["Jwt:PreviousSecretKey"];
if (!string.IsNullOrEmpty(previousKey))
{
keys.Add(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(previousKey)));
}
return keys;
}
}
// 使用非对称密钥(RS256)— 更安全
public class RsaJwtConfiguration
{
public static void Configure(JwtBearerOptions options, IConfiguration config)
{
// 从文件或密钥存储加载 RSA 公钥
var publicKey = File.ReadAllText(config["Jwt:PublicKeyPath"]!);
var rsa = RSA.Create();
rsa.ImportFromPem(publicKey);
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = config["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = config["Jwt:Audience"],
ValidateLifetime = true,
IssuerSigningKey = new RsaSecurityKey(rsa)
};
}
}
// 生成 Token 时使用 RSA 私钥
public class RsaTokenService
{
private readonly RSA _privateKey;
public RsaTokenService(IConfiguration config)
{
_privateKey = RSA.Create();
_privateKey.ImportFromPem(File.ReadAllText(config["Jwt:PrivateKeyPath"]!));
}
public string GenerateToken(IEnumerable<Claim> claims, TimeSpan lifetime)
{
var credentials = new SigningCredentials(
new RsaSecurityKey(_privateKey),
SecurityAlgorithms.RsaSha256);
var token = new JwtSecurityToken(
issuer: "https://auth.example.com",
audience: "my-api",
claims: claims,
expires: DateTime.UtcNow.Add(lifetime),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}多认证方案共存
同时支持 Cookie 和 JWT
/// <summary>
/// 多认证方案 — 同一应用同时支持 Cookie 和 JWT
/// </summary>
builder.Services.AddAuthentication(options =>
{
// 默认方案:优先 JWT,回退到 Cookie
options.DefaultScheme = "CookieOrJwt";
options.DefaultChallengeScheme = "CookieOrJwt";
})
.AddCookie(options =>
{
options.LoginPath = "/login";
options.Cookie.Name = "app.auth";
options.ExpireTimeSpan = TimeSpan.FromHours(8);
})
.AddJwtBearer("Bearer", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = builder.Configuration["Jwt:Audience"],
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"]!))
};
})
.AddPolicyScheme("CookieOrJwt", "CookieOrJwt", options =>
{
// 动态选择方案:有 Authorization 头用 JWT,否则用 Cookie
options.ForwardDefaultSelector = context =>
{
var authHeader = context.Request.Headers.Authorization.FirstOrDefault();
if (authHeader?.StartsWith("Bearer ") == true)
return "Bearer";
return CookieAuthenticationDefaults.AuthenticationScheme;
};
});
// 针对不同方案登录
[HttpPost("login/web")]
public async Task<IActionResult> WebLogin(LoginRequest request)
{
// Cookie 认证
var claims = BuildClaims(request);
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
return Ok();
}
[HttpPost("login/api")]
public IActionResult ApiLogin(LoginRequest request)
{
// JWT 认证
var token = _jwtService.GenerateToken(request);
return Ok(new { token });
}自定义认证 Handler
API Key 认证
/// <summary>
/// 自定义 API Key 认证 — 适合服务间调用
/// </summary>
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
public ApiKeyAuthenticationHandler(
IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// 从 Header 读取 API Key
if (!Request.Headers.TryGetValue("X-Api-Key", out var apiKeyHeader))
{
return AuthenticateResult.NoResult();
}
var apiKey = apiKeyHeader.ToString();
if (string.IsNullOrEmpty(apiKey))
{
return AuthenticateResult.NoResult();
}
// 验证 API Key
var expectedKey = Options.ApiKey;
if (apiKey != expectedKey)
{
return AuthenticateResult.Fail("无效的 API Key");
}
// 构建 Claims
var claims = new[]
{
new Claim(ClaimTypes.Name, "ApiService"),
new Claim("api_key", apiKey),
new Claim("auth_type", "apikey")
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
public string ApiKey { get; set; } = "";
}
// 注册
builder.Services.AddAuthentication()
.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>("ApiKey", null);
// 使用
[Authorize(AuthenticationSchemes = "ApiKey")]
[ApiController]
[Route("api/internal")]
public class InternalApiController : ControllerBase
{
[HttpGet("sync")]
public IActionResult Sync() => Ok("内部同步完成");
}最小权限设计实践
基于权限的 API 设计
/// <summary>
/// 最小权限设计:默认拒绝,按需开放
/// </summary>
// 全局默认策略 — 所有请求必须认证
builder.Services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
// 匿名访问端点 — 明确标记
app.MapGet("/api/public/info", () => "公开信息")
.AllowAnonymous();
app.MapGet("/api/public/status", () => Results.Ok(new { Status = "Healthy" }))
.AllowAnonymous();
// 权限粒度设计
builder.Services.AddAuthorization(options =>
{
// 功能权限
options.AddPolicy("Orders.View", policy =>
policy.RequireClaim("permission", "orders.view"));
options.AddPolicy("Orders.Create", policy =>
policy.RequireClaim("permission", "orders.create"));
options.AddPolicy("Orders.Delete", policy =>
policy.RequireClaim("permission", "orders.delete"));
// 数据权限(需要自定义 Handler)
options.AddPolicy("Orders.Own", policy =>
policy.Requirements.Add(new OwnerRequirement()));
// 组合权限
options.AddPolicy("Orders.Manage", policy =>
policy.RequireClaim("permission", "orders.view")
.RequireClaim("permission", "orders.create")
.RequireClaim("permission", "orders.delete"));
});
// 前端获取当前用户权限(用于按钮/菜单控制)
app.MapGet("/api/my/permissions", (HttpContext http) =>
{
var permissions = http.User.Claims
.Where(c => c.Type == "permission")
.Select(c => c.Value)
.ToList();
var roles = http.User.Claims
.Where(c => c.Type == ClaimTypes.Role)
.Select(c => c.Value)
.ToList();
return Results.Ok(new
{
Permissions = permissions,
Roles = roles,
UserName = http.User.Identity?.Name
});
}).RequireAuthorization();缺点
总结
ASP.NET Core 鉴权与授权最重要的是先分清“认证”和“授权”两个层次,再根据系统形态选择 Cookie 还是 JWT,并在 Role、Claim、Policy、资源授权之间建立合适的权限模型。真正稳定的系统,不是“能登录”就够了,而是用户身份、权限边界、失败响应和安全审计都要一并设计。
关键知识点
- Authentication 解决身份确认,Authorization 解决权限判断。
- Cookie 更适合传统 Web 会话,JWT 更适合前后端分离 API。
- Role 适合粗粒度权限,Policy / Claim 更适合细粒度控制。
- 复杂业务最终往往需要资源级授权,而不只是角色判断。
项目落地视角
- 后台管理系统通常 Cookie + Role / Policy 足够稳定。
- 前后端分离 API 更常用 JWT + Policy / Claim。
- 多租户或高权限系统通常需要资源级权限和审计日志。
- 企业系统上线前必须验证未登录、已登录无权限、Token 过期等边界场景。
常见误区
- 用户拿到 Token 就默认拥有全部接口权限。
- 所有权限都写死在角色里,后期越来越难维护。
- 把认证异常和授权异常统一成一种错误返回。
- 只做 happy path 登录,不测过期、锁定、吊销和无权限路径。
进阶路线
- 学习 ASP.NET Core Identity、OpenIddict、Duende 等身份体系。
- 补齐刷新令牌、登出失效、黑名单和多端会话策略。
- 建立统一权限模型(菜单、接口、资源、数据级权限)。
- 将认证授权和日志、审计、监控、风控联动起来。
适用场景
- 后台管理系统。
- 前后端分离 API。
- SignalR 实时系统。
- 多角色、多权限、多租户的业务系统。
落地建议
- 先设计权限模型,再写
[Authorize],不要反过来。 - 为未登录和无权限分别定义稳定错误响应。
- JWT Secret、Issuer、Audience 全都走安全配置源管理。
- 对关键权限接口补充集成测试和审计日志。
排错清单
- 接口总是 401:先检查是否正确启用了
UseAuthentication()。 - 登录成功但接口仍 403:先检查 Policy / Claim / Role 是否匹配。
- JWT 不生效:检查签名密钥、Issuer、Audience、过期时间。
- Cookie 登录后无状态:检查 SameSite、域名、HTTPS 和跨域配置。
