安全与认证授权

系统讲解 ASP.NET Core 应用的安全体系建设,覆盖 Identity 身份管理、JWT 认证、OAuth2 与 OIDC 协议、角色与策略授权、SQL 注入与数据保护,以及安全响应头与 CORS 的落地实践。

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访问资源用的令牌
Scopesopenid/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 头
CSRFAntiForgeryToken / 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——让安全成为架构的一部分而非事后补丁。把敏感信息外置、把权限下沉到资源、把密钥交给专用存储,这套体系才能在真实攻防中站住。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「csharp」更多文章

  1. .NET 机器学习实战
  2. 内存剖析与 dump 分析
  3. 分布式事务与 Saga 编排