authentication
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAuthentication & Authorization
身份验证与授权
Core Principles
核心原则
- Use ASP.NET Identity for user management — Don't build your own user store. Identity handles password hashing, lockout, two-factor, email confirmation, and (since .NET 10) built-in passkey/WebAuthn support for passwordless login.
- JWT for APIs, cookies for web apps — APIs use Bearer token authentication; Blazor/MVC apps use cookie authentication.
- Policy-based authorization over roles — Policies are testable, composable, and more expressive than .
[Authorize(Roles = "Admin")] - Never store secrets in code — Use user secrets in development, Azure Key Vault / environment variables in production.
- 使用ASP.NET Identity进行用户管理 — 不要自行构建用户存储。Identity负责密码哈希、账户锁定、双因素认证、邮件确认,并且(从.NET 10开始)内置了passkey/WebAuthn支持,实现无密码登录。
- API使用JWT,Web应用使用Cookie — API采用Bearer令牌身份验证;Blazor/MVC应用采用Cookie身份验证。
- 优先基于策略的授权而非角色 — 策略可测试、可组合,比更具表达性。
[Authorize(Roles = "Admin")] - 绝不在代码中存储密钥 — 开发环境使用用户机密,生产环境使用Azure Key Vault或环境变量。
Patterns
模式
JWT Bearer Authentication
JWT承载身份验证
csharp
// Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)),
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddAuthorization();csharp
// Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)),
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddAuthorization();Token Generation
令牌生成
Use from — it is the maintained, span-based handler that ASP.NET Core itself validates with. (System.IdentityModel.Tokens.Jwt) is the legacy stack.
JsonWebTokenHandlerMicrosoft.IdentityModel.JsonWebTokensJwtSecurityTokenHandlercsharp
public sealed class TokenService(IConfiguration config, TimeProvider clock)
{
private static readonly JsonWebTokenHandler TokenHandler = new();
public string GenerateToken(User user, IEnumerable<string> roles)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"]!));
var now = clock.GetUtcNow();
var descriptor = new SecurityTokenDescriptor
{
Issuer = config["Jwt:Issuer"],
Audience = config["Jwt:Audience"],
IssuedAt = now.UtcDateTime,
Expires = now.AddHours(1).UtcDateTime,
Claims = new Dictionary<string, object>
{
[JwtRegisteredClaimNames.Sub] = user.Id,
[JwtRegisteredClaimNames.Email] = user.Email!,
[JwtRegisteredClaimNames.Name] = user.UserName!,
["roles"] = roles.ToArray()
},
SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
};
return TokenHandler.CreateToken(descriptor);
}
}使用中的 — 这是ASP.NET Core自身用于验证的、基于Span的维护版本处理器。(System.IdentityModel.Tokens.Jwt)是旧版栈。
Microsoft.IdentityModel.JsonWebTokensJsonWebTokenHandlerJwtSecurityTokenHandlercsharp
public sealed class TokenService(IConfiguration config, TimeProvider clock)
{
private static readonly JsonWebTokenHandler TokenHandler = new();
public string GenerateToken(User user, IEnumerable<string> roles)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"]!));
var now = clock.GetUtcNow();
var descriptor = new SecurityTokenDescriptor
{
Issuer = config["Jwt:Issuer"],
Audience = config["Jwt:Audience"],
IssuedAt = now.UtcDateTime,
Expires = now.AddHours(1).UtcDateTime,
Claims = new Dictionary<string, object>
{
[JwtRegisteredClaimNames.Sub] = user.Id,
[JwtRegisteredClaimNames.Email] = user.Email!,
[JwtRegisteredClaimNames.Name] = user.UserName!,
["roles"] = roles.ToArray()
},
SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
};
return TokenHandler.CreateToken(descriptor);
}
}Policy-Based Authorization
基于策略的授权
csharp
// Define policies
builder.Services.AddAuthorizationBuilder()
.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"))
.AddPolicy("CanManageOrders", policy => policy
.RequireAuthenticatedUser()
.RequireClaim("permission", "orders:write"))
.AddPolicy("MinimumAge", policy => policy
.AddRequirements(new MinimumAgeRequirement(18)));
// Custom requirement + handler
public class MinimumAgeRequirement(int minimumAge) : IAuthorizationRequirement
{
public int MinimumAge => minimumAge;
}
public class MinimumAgeHandler(TimeProvider clock) : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
var dateOfBirthClaim = context.User.FindFirst("date_of_birth");
if (dateOfBirthClaim is not null &&
DateOnly.TryParse(dateOfBirthClaim.Value, out var dob) &&
dob.AddYears(requirement.MinimumAge) <= DateOnly.FromDateTime(clock.GetUtcNow().DateTime))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}csharp
// 定义策略
builder.Services.AddAuthorizationBuilder()
.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"))
.AddPolicy("CanManageOrders", policy => policy
.RequireAuthenticatedUser()
.RequireClaim("permission", "orders:write"))
.AddPolicy("MinimumAge", policy => policy
.AddRequirements(new MinimumAgeRequirement(18)));
// 自定义要求 + 处理器
public class MinimumAgeRequirement(int minimumAge) : IAuthorizationRequirement
{
public int MinimumAge => minimumAge;
}
public class MinimumAgeHandler(TimeProvider clock) : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
var dateOfBirthClaim = context.User.FindFirst("date_of_birth");
if (dateOfBirthClaim is not null &&
DateOnly.TryParse(dateOfBirthClaim.Value, out var dob) &&
dob.AddYears(requirement.MinimumAge) <= DateOnly.FromDateTime(clock.GetUtcNow().DateTime))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}Protecting Endpoints
保护端点
csharp
// Protect an entire group
app.MapGroup("/api/admin")
.WithTags("Admin")
.RequireAuthorization("AdminOnly")
.MapAdminEndpoints();
// Protect individual endpoints
group.MapPost("/", CreateOrder)
.RequireAuthorization("CanManageOrders");
// Allow anonymous on a protected group
group.MapGet("/public-info", GetPublicInfo)
.AllowAnonymous();csharp
// 保护整个组
app.MapGroup("/api/admin")
.WithTags("Admin")
.RequireAuthorization("AdminOnly")
.MapAdminEndpoints();
// 保护单个端点
group.MapPost("/", CreateOrder)
.RequireAuthorization("CanManageOrders");
// 在受保护组中允许匿名访问
group.MapGet("/public-info", GetPublicInfo)
.AllowAnonymous();OpenID Connect (External Identity Provider)
OpenID Connect(外部身份提供商)
csharp
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = builder.Configuration["Oidc:Authority"];
options.ClientId = builder.Configuration["Oidc:ClientId"];
options.ClientSecret = builder.Configuration["Oidc:ClientSecret"];
options.ResponseType = "code";
options.SaveTokens = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
});csharp
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = builder.Configuration["Oidc:Authority"];
options.ClientId = builder.Configuration["Oidc:ClientId"];
options.ClientSecret = builder.Configuration["Oidc:ClientSecret"];
options.ResponseType = "code";
options.SaveTokens = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
});Accessing Current User
获取当前用户
csharp
// In minimal API handlers — inject ClaimsPrincipal or HttpContext
group.MapGet("/me", (ClaimsPrincipal user) =>
{
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
var email = user.FindFirstValue(ClaimTypes.Email);
return TypedResults.Ok(new { userId, email });
}).RequireAuthorization();csharp
// 在极简API处理器中 — 注入ClaimsPrincipal或HttpContext
group.MapGet("/me", (ClaimsPrincipal user) =>
{
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
var email = user.FindFirstValue(ClaimTypes.Email);
return TypedResults.Ok(new { userId, email });
}).RequireAuthorization();Anti-patterns
反模式
Don't Use Role Strings Everywhere
不要在任何地方使用角色字符串
csharp
// BAD — magic strings, hard to refactor, not testable
[Authorize(Roles = "Admin,SuperAdmin,Manager")]
public class AdminController { }
// GOOD — policy-based
builder.Services.AddAuthorizationBuilder()
.AddPolicy("AdminAccess", p => p.RequireRole("Admin", "SuperAdmin", "Manager"));
group.MapGet("/", Handler).RequireAuthorization("AdminAccess");csharp
// 错误做法 — 魔法字符串,难以重构,无法测试
[Authorize(Roles = "Admin,SuperAdmin,Manager")]
public class AdminController { }
// 正确做法 — 基于策略
builder.Services.AddAuthorizationBuilder()
.AddPolicy("AdminAccess", p => p.RequireRole("Admin", "SuperAdmin", "Manager"));
group.MapGet("/", Handler).RequireAuthorization("AdminAccess");Don't Store Secrets in appsettings.json
不要在appsettings.json中存储密钥
json
// BAD — committed to source control
{
"Jwt": {
"Key": "super-secret-key-12345"
}
}bash
undefinedjson
// 错误做法 — 会提交到源代码控制
{
"Jwt": {
"Key": "super-secret-key-12345"
}
}bash
// 正确做法 — 开发环境使用用户机密
dotnet user-secrets set "Jwt:Key" "super-secret-key-12345"GOOD — use user secrets in development
不要跳过令牌验证
dotnet user-secrets set "Jwt:Key" "super-secret-key-12345"
undefinedcsharp
// 错误做法 — 禁用验证
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false, // 不要这样做
ValidateAudience = false, // 不要这样做
ValidateLifetime = false, // 绝对不要这样做
};
// 正确做法 — 验证所有内容(完整设置请参考上方JWT承载身份验证模式)Don't Skip Token Validation
决策指南
csharp
// BAD — disabling validation
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false, // DON'T
ValidateAudience = false, // DON'T
ValidateLifetime = false, // DEFINITELY DON'T
};
// GOOD — validate everything (see JWT Bearer Authentication pattern above for full setup)| 场景 | 推荐方案 |
|---|---|
| REST API | JWT承载身份验证 |
| Blazor Server / MVC | Cookie身份验证 |
| 外部身份提供商 | OpenID Connect |
| 用户注册/登录 | ASP.NET Identity |
| 无密码登录 | ASP.NET Identity passkeys(WebAuthn,.NET 10起内置) |
| 权限检查 | 基于策略的授权 |
| 多租户API | 基于声明的租户声明 |
| API间通信 | 客户端凭证(OAuth 2.0) |
| 简单API密钥 | 自定义 |
Decision Guide
—
| Scenario | Recommendation |
|---|---|
| REST API | JWT Bearer authentication |
| Blazor Server / MVC | Cookie authentication |
| External identity provider | OpenID Connect |
| User registration / login | ASP.NET Identity |
| Passwordless login | ASP.NET Identity passkeys (WebAuthn, built-in since .NET 10) |
| Permission checking | Policy-based authorization |
| Multi-tenant API | Claims-based with tenant claim |
| API-to-API communication | Client credentials (OAuth 2.0) |
| Simple API keys | Custom |
—