1. ASP.NET Core 身份体系
一句话总结: ASP.NET Core Identity 提供用户注册、登录、角色等开箱即用的身份管理,配合 EF Core 存储与 Cookie 认证构成默认方案。
AddIdentity + AddEntityFrameworkStores 注册身份体系:用户(IdentityUser)、角色(IdentityRole)、密码哈希、Token 服务一应俱全。默认认证方案是 Cookie,登录成功后浏览器携带认证 Cookie,[Authorize] 特性据此放行。
builder.Services.AddDbContext<IdentityDbContext>(opt =>
opt.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
{
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.User.RequireUniqueEmail = true;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<IdentityDbContext>();
var app = builder.Build();
app.UseAuthentication(); // 建立用户身份
app.UseAuthorization(); // 执行授权策略
// 受保护的页面/接口
app.MapGet("/account", (ClaimsPrincipal user) =>
$"你好,{user.Identity?.Name}")
.RequireAuthorization();
| 组件 | 作用 |
|---|---|
| IdentityUser | 用户实体,可扩展自定义字段 |
| IdentityRole | 角色实体 |
| UserManager | 用户创建、验证、锁定 |
| RoleManager | 角色管理 |
| SignInManager | 登录/登出、外部登录 |
避坑: Identity 默认的密码规则别关掉——至少要求长度与复杂度。Cookie 认证适合「自有前端 + 同域」的场景;如果是 API 给移动端或跨域前端调用,应该换 JWT 认证。UserManager 查用户时按
FindByEmailAsync用邮箱索引,别在循环里逐条查。
2. JWT 认证
一句话总结: JWT 把用户信息签成无状态令牌放在客户端,服务端验签即可放行,适合 API 与分布式场景,但要处理好密钥与过期。
JWT(JSON Web Token)由 Header、Payload、Signature 三段 Base64 组成,签名保证完整性。AddAuthentication().AddJwtBearer 配置验签参数,请求带 Authorization: Bearer <token> 即被认证。因为无状态,它天然适合多实例负载均衡的场景。
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "orders-api",
ValidateAudience = true,
ValidAudience = "orders-clients",
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)),
ClockSkew = TimeSpan.FromMinutes(1)
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// 签发令牌
public static string CreateToken(IdentityUser user, string secret)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id),
new Claim(JwtRegisteredClaimNames.Email, user.Email ?? "")
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
var token = new JwtSecurityToken(
issuer: "orders-api",
audience: "orders-clients",
claims: claims,
expires: DateTime.UtcNow.AddHours(2),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
return new JwtSecurityTokenHandler().WriteToken(token);
}
| 参数 | 说明 |
|---|---|
| ValidIssuer/Audience | 校验签发方与受众 |
| ValidateLifetime | 校验过期 |
| IssuerSigningKey | 对称密钥,须与签发一致 |
| ClockSkew | 时钟偏差容限,默认 5 分钟偏大 |
避坑: JWT 密钥要足够长且从安全配置读取(环境变量/Vault),绝不能写死在代码或提交进 git。默认
ClockSkew是 5 分钟,把它调小(如 1 分钟)避免令牌过期后仍可用。JWT 无法主动吊销——需要紧急踢人时用短过期 + 刷新令牌,或用黑名单存储。
3. OAuth2 与 OIDC
一句话总结: OAuth2 解决「授权第三方访问资源」,OIDC 在 OAuth2 之上加「身份认证」,企业场景常借外部 IdP(Azure AD、IdentityServer)统一登录。
OIDC(OpenID Connect)让应用把认证委托给身份提供者(IdP),用户跳转登录后拿回 id_token 与授权码,应用再换取 access_token。ASP.NET Core 的 AddOpenIdConnect 一步接入;自建 IdP 可用 IdentityServer/Duende。
builder.Services.AddAuthentication()
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://login.example.com"; // IdP 地址
options.ClientId = "orders-web";
options.ClientSecret = "client-secret";
options.ResponseType = "code"; // 授权码模式
options.Scope.Add("openid");
options.Scope.Add("profile");
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
});
| 概念 | 作用 |
|---|---|
| Authorization Code | 授权码,换 token 更安全 |
| id_token | 用户身份声明(JWT) |
| access_token | 访问资源用的令牌 |
| Scopes | openid/profile/email 权限声明 |
| UserInfo Endpoint | 补充用户声明 |
避坑: OAuth2 的令牌是给资源服务器验的,不是给浏览器看的数据。授权码模式比隐式模式安全——浏览器只短暂持有授权码。
SaveTokens = true时令牌存在 Cookie 里,注意 Cookie 加密与大小;用 OIDC 就别同时自造一套用户体系,身份来源要单一。
4. 角色与策略授权
一句话总结:
[Authorize]用角色做粗粒度控制,策略授权用声明与处理器做细粒度控制,统一在授权中间件里执行。
角色授权看「你是谁」(角色),策略授权看「你能不能做这件事」(声明/资源)。策略把规则集中声明,处理器(IAuthorizationHandler)承载判断逻辑,业务里加 [Authorize(Policy = "order.delete")] 即可。
builder.Services.AddAuthorization(options =>
{
// 基于角色的策略
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
// 基于声明的策略
options.AddPolicy("CanDeleteOrder", policy =>
policy.RequireAssertion(ctx =>
ctx.User.IsInRole("Admin") ||
ctx.User.HasClaim(c => c.Type == "order.scope" && c.Value == "delete")));
// 基于资源的策略
options.AddPolicy("OwnOrder", policy =>
policy.Requirements.Add(new OrderOwnerRequirement()));
});
public class OrderOwnerRequirement : IAuthorizationRequirement { }
public class OrderOwnerHandler(IOrderRepository repo) : AuthorizationHandler<OrderOwnerRequirement>
{
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext ctx, OrderOwnerRequirement req)
{
var orderId = ctx.Resource?.ToString();
var order = await repo.GetByIdAsync(int.Parse(orderId ?? "0"));
if (order is not null && order.OwnerId == ctx.User.FindFirstValue(ClaimTypes.NameIdentifier))
ctx.Succeed(req);
}
}
// 使用
[Authorize(Policy = "CanDeleteOrder")]
app.MapDelete("/orders/{id:int}", (int id) => ...);
| 授权类型 | 粒度 | 适用 |
|---|---|---|
| [Authorize] | 已登录即可 | 登录门槛 |
| [Authorize(Roles=…)] | 角色级 | 后台权限 |
| 策略 + 处理器 | 声明/资源级 | 数据级权限 |
避坑: 角色是「静态的桶」,业务权限常是「动态的关系」(订单属于谁、项目成员能看哪些)。数据级权限务必用策略 + 资源处理器,在处理器里查资源再决定,而不是前端传个角色名就信。授权失败返回 403(Forbid),认证失败才是 401。
5. 防注入与数据保护
一句话总结: 把外部输入当「数据」而非「代码」,用参数化查询防 SQL 注入、用编码防 XSS、用 Data Protection API 做敏感数据加密。
SQL 注入源于字符串拼接 SQL;EF Core 的参数化 LINQ 天然免疫,但原生 SQL 与 ExecuteSqlRaw 要警惕。XSS 则靠输出编码与 CSP。.NET 的 Data Protection API 用于加解密 Cookie、令牌等需要持久化的敏感数据,密钥由系统托管。
// 危险:字符串拼接 SQL
// var sql = $"SELECT * FROM Orders WHERE Id = {id}"; // 绝不这样做
// 安全:EF Core 参数化
var order = await _db.Orders
.FirstOrDefaultAsync(o => o.Id == id);
// 安全:原生 SQL 用参数
var rows = await _db.Database
.ExecuteSqlInterpolatedAsync($"UPDATE Orders SET Status={status} WHERE Id={id}");
// 数据保护:加密敏感字段
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("/app/keys"));
public class TokenProtector(IDataProtectionProvider provider)
{
private readonly IDataProtector _protector =
provider.CreateProtector("TokenProtector.v1");
public string Protect(string plain) => _protector.Protect(plain);
public string Unprotect(string cipher) => _protector.Unprotect(cipher);
}
| 威胁 | 防御 |
|---|---|
| SQL 注入 | 参数化查询 / EF LINQ |
| XSS | 输出编码 + CSP 头 |
| CSRF | AntiForgeryToken / SameSite Cookie |
| 敏感数据存储 | Data Protection API 加密 |
| 开放重定向 | 校验回跳地址同域 |
避坑:
ExecuteSqlRaw用FormattableString重载(ExecuteSqlInterpolated),它把插值转成参数而不是拼接。Data Protection 密钥要跨实例共享(PersistKeysToFileSystem 挂共享卷),否则多副本部署时 Cookie 无法互相解密。密码哈希用 Identity 自带的 PBKDF2,别自己造哈希。
6. 安全响应头与 CORS
一句话总结: 安全响应头(CSP、X-Frame-Options 等)是浏览器侧的纵深防御,CORS 精确控制跨域访问,两者配合减少 XSS 与跨域滥用。
即使后端输入校验严密,也应该给浏览器加防线:Content-Security-Policy 限制脚本来源,X-Frame-Options 防点击劫持,X-Content-Type-Options 防 MIME 嗅探。CORS 则用 AddCors + UseCors 配置允许的前端源。
app.Use(async (context, next) =>
{
context.Response.Headers["Content-Security-Policy"] =
"default-src 'self'; script-src 'self'; img-src 'self' data:;";
context.Response.Headers["X-Content-Type-Options"] = "nosniff";
context.Response.Headers["X-Frame-Options"] = "DENY";
context.Response.Headers["Referrer-Policy"] = "no-referrer";
await next();
});
// CORS:只允许可信前端源
builder.Services.AddCors(options =>
{
options.AddPolicy("frontend", policy =>
policy.WithOrigins("https://app.example.com")
.AllowCredentials()
.WithMethods("GET", "POST")
.WithHeaders("Authorization", "Content-Type"));
});
app.UseCors("frontend");
| 响应头 | 作用 |
|---|---|
| Content-Security-Policy | 限制脚本/资源来源 |
| X-Frame-Options | 防点击劫持 |
| X-Content-Type-Options | 防 MIME 嗅探 |
| Referrer-Policy | 控制 Referer 泄漏 |
| Strict-Transport-Security | 强制 HTTPS |
避坑: CORS 不是安全边界——浏览器会执行限制,但恶意客户端不受 CORS 约束。真正权限控制靠认证授权,CORS 只是「浏览器礼仪」。别
AllowAnyOrigin+AllowCredentials组合(浏览器会拒绝),也别把 CORS 源设成*后仍带 Cookie。
7. 密钥管理与合规
一句话总结: 密钥管理是安全的「最后一公里」:连接串、JWT 密钥、第三方凭证都要进安全存储并支持轮换,配合审计日志满足合规。
代码库里的连接串、appsettings.json 里的密钥是常见泄漏点。做法:User Secrets 仅限本地开发;生产用环境变量或 Azure Key Vault / HashiCorp Vault;CI 里密钥走 Secret 存储。密钥要支持轮换,轮换后旧密钥保留一个过渡窗口避免服务中断。
// 开发:User Secrets
// dotnet user-secrets set "Jwt:Key" "dev-only-key"
// 生产:环境变量(Docker/K8s Secret 注入)
// Jwt__Key 会被绑定到 configuration["Jwt:Key"]
builder.Services.AddSingleton<IJwtKeyProvider>(sp =>
{
var key = builder.Configuration["Jwt:Key"]
?? throw new InvalidOperationException("Jwt:Key 缺失");
return new JwtKeyProvider(key);
});
// 敏感配置不进配置文件,用 IConfiguration 抽象读取
var conn = builder.Configuration.GetConnectionString("Default");
// 实际连接串来自环境变量或 Vault,而非 appsettings.json
| 层级 | 场景 | 手段 |
|---|---|---|
| 本地开发 | 个人环境 | User Secrets |
| 测试/生产 | 部署环境 | 环境变量 / Secret 文件 |
| 云环境 | 托管密钥 | Key Vault / Vault |
| 证书 | TLS/mTLS | 证书管理 + 轮换 |
避坑: 密钥轮换别「硬切」——新密钥生效时旧密钥还要能验签一段时间,否则在途令牌全部失效。审计日志记录「谁在何时做了什么敏感操作」,但别把密钥本身写进日志。加解密操作要集中在一个安全模块,别散落在业务代码里。
8. 总结
| 主题 | 要点 |
|---|---|
| Identity | 用户/角色/密码哈希开箱即用,适合自有前端 |
| JWT | 无状态令牌,验签放行,密钥安全托管 |
| OAuth2/OIDC | 委托外部 IdP,授权码模式优先 |
| 授权 | 角色粗粒度 + 策略/处理器细粒度 |
| 防注入 | 参数化查询、编码、Data Protection |
| 响应头/CORS | 浏览器纵深防御 + 精确跨域 |
| 密钥管理 | 环境变量/Vault 存储,支持轮换 |
安全不是「某个中间件」,而是一整套纵深防御:认证解决「你是谁」,授权解决「你能做什么」,防注入解决「输入别当代码」,数据保护解决「存储别裸奔」,安全头解决「浏览器别被利用」,密钥管理解决「最后一道防线别失守」。ASP.NET Core 把这些都做成了声明式 + 约定式——AddAuthentication().AddJwtBearer()、[Authorize(Policy=...)]、IDataProtector——让安全成为架构的一部分而非事后补丁。把敏感信息外置、把权限下沉到资源、把密钥交给专用存储,这套体系才能在真实攻防中站住。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。