API安全Skill api-security

这个技能提供全面的API安全指导,覆盖认证方法(如API密钥、OAuth、JWT)、速率限制策略、输入验证、CORS配置、安全头设置,以及防范OWASP API Top 10漏洞如BOLA和IDOR。适用于API开发者和安全专家进行API安全设计、审查和优化,关键词包括API安全、认证、速率限制、输入验证、CORS、安全头、OWASP、漏洞防护。

身份认证 0 次安装 0 次浏览 更新于 3/11/2026

name: API安全 description: 全面的API安全指南,涵盖认证方法、速率限制、输入验证、CORS、安全头以及防范OWASP API Top 10漏洞。适用于设计API认证、实现速率限制、配置CORS、设置安全头或审查API安全。 allowed-tools: Read, Glob, Grep, Task

API安全

全面的API安全指导,覆盖认证、授权、速率限制、验证以及防范常见API攻击。

何时使用此技能

在以下情况下使用此技能:

  • 选择API认证方法
  • 实现速率限制
  • 配置CORS策略
  • 设置安全头
  • 验证API输入
  • 防止数据泄露
  • 防范BOLA/IDOR攻击
  • 实现请求签名
  • 保护API网关

OWASP API安全Top 10 (2023)

排名 漏洞 描述 缓解措施
API1 损坏的对象级别授权 访问未授权对象 对象级别授权检查
API2 损坏的认证 认证缺陷 强认证、多因素认证
API3 损坏的对象属性级别授权 过度数据暴露、批量赋值 响应过滤、白名单
API4 无限制的资源消耗 通过资源耗竭的DoS攻击 速率限制、分页
API5 损坏的功能级别授权 访问未授权功能 功能级别授权检查
API6 无限制访问敏感业务流 滥用业务逻辑 速率限制、欺诈检测
API7 服务器端请求伪造 (SSRF) 服务器发起恶意请求 URL验证、白名单
API8 安全配置错误 不当配置 安全加固、自动化
API9 不当的库存管理 未知/未管理的API API库存、版本控制
API10 不安全的API消费 信任第三方API 验证外部响应

API认证方法

方法比较

方法 使用场景 优点 缺点
API密钥 简单服务、内部API 易于实现 无用户上下文、难以轮换
OAuth 2.0 Bearer令牌 用户委托访问 标准、范围化 令牌管理复杂
JWT 无状态认证 自包含、可扩展 大小、吊销挑战
mTLS 服务到服务 强身份、加密 证书管理
HMAC签名 请求完整性 防篡改 实现复杂

API密钥安全

using System.Security.Cryptography;
using Microsoft.AspNetCore.Http;

/// <summary>
/// API密钥认证处理器,使用ASP.NET Core中间件。
/// 使用CryptographicOperations.FixedTimeEquals进行时间安全比较。
/// </summary>
public sealed class ApiKeyAuthenticationHandler(
    IApiKeyValidator validator,
    ILogger<ApiKeyAuthenticationHandler> logger)
{
    private const string ApiKeyHeader = "X-API-Key";
    private const string ClientIdHeader = "X-Client-ID";

    public async Task<bool> ValidateAsync(HttpContext context, CancellationToken ct = default)
    {
        if (!context.Request.Headers.TryGetValue(ApiKeyHeader, out var apiKeyHeader))
        {
            logger.LogWarning("API密钥必需但未提供");
            return false;
        }

        var apiKey = apiKeyHeader.ToString();
        var clientId = context.Request.Headers[ClientIdHeader].ToString();

        // 哈希提供的密钥
        var providedHash = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(apiKey));

        // 检索此客户端的存储哈希
        var storedHash = await validator.GetKeyHashAsync(clientId, ct);
        if (storedHash is null)
        {
            logger.LogWarning("客户端 {ClientId} 无存储密钥", clientId);
            return false;
        }

        // 时间安全比较以防止时间攻击
        if (!CryptographicOperations.FixedTimeEquals(providedHash, storedHash))
        {
            logger.LogWarning("客户端 {ClientId} 的API密钥无效", clientId);
            return false;
        }

        return true;
    }
}

public interface IApiKeyValidator
{
    Task<byte[]?> GetKeyHashAsync(string clientId, CancellationToken ct = default);
}

// API密钥最佳实践:
// 1. 使用足够长、随机的密钥(32+字节)
// 2. 仅通过HTTPS传输
// 3. 存储哈希值,非明文
// 4. 实现密钥轮换
// 5. 将密钥范围限制到特定操作
// 6. 按密钥速率限制

请求签名 (HMAC)

using System.Security.Cryptography;
using System.Text;

/// <summary>
/// HMAC-SHA256请求签名器,用于API认证。
/// 生成和验证请求签名以实现防篡改请求。
/// </summary>
public sealed class RequestSigner
{
    private readonly string _apiKey;
    private readonly byte[] _secretKey;

    public RequestSigner(string apiKey, string secretKey)
    {
        _apiKey = apiKey;
        _secretKey = Encoding.UTF8.GetBytes(secretKey);
    }

    /// <summary>
    /// 为请求生成签名头。
    /// </summary>
    public Dictionary<string, string> SignRequest(string method, string path, string body = "")
    {
        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        var stringToSign = $"{method}
{path}
{timestamp}
{body}";

        using var hmac = new HMACSHA256(_secretKey);
        var signature = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign));

        return new Dictionary<string, string>
        {
            ["X-API-Key"] = _apiKey,
            ["X-Timestamp"] = timestamp,
            ["X-Signature"] = Convert.ToBase64String(signature)
        };
    }
}

/// <summary>
/// 验证HMAC-SHA256请求签名。
/// </summary>
public sealed class SignatureVerifier(ISecretKeyProvider secretProvider)
{
    private static readonly TimeSpan MaxClockSkew = TimeSpan.FromMinutes(5);

    /// <summary>
    /// 使用时间安全比较验证请求签名。
    /// </summary>
    public async Task<bool> VerifyAsync(
        string apiKey,
        string timestamp,
        string signature,
        string method,
        string path,
        string body = "",
        CancellationToken ct = default)
    {
        // 检查时间戳新鲜度(5分钟窗口)
        if (!long.TryParse(timestamp, out var requestTime))
            return false;

        var requestDateTime = DateTimeOffset.FromUnixTimeSeconds(requestTime);
        if (Math.Abs((DateTimeOffset.UtcNow - requestDateTime).TotalSeconds) > MaxClockSkew.TotalSeconds)
            return false;

        // 检索此API密钥的密钥
        var secretKey = await secretProvider.GetSecretAsync(apiKey, ct);
        if (secretKey is null)
            return false;

        // 重新生成预期签名
        var stringToSign = $"{method}
{path}
{timestamp}
{body}";
        using var hmac = new HMACSHA256(secretKey);
        var expected = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign));

        // 时间安全比较以防止时间攻击
        var provided = Convert.FromBase64String(signature);
        return CryptographicOperations.FixedTimeEquals(expected, provided);
    }
}

public interface ISecretKeyProvider
{
    Task<byte[]?> GetSecretAsync(string apiKey, CancellationToken ct = default);
}

速率限制

速率限制策略

策略 描述 使用场景
固定窗口 在固定时间窗口内计数请求 简单、可预测
滑动窗口 请求的滚动窗口 更平滑的限制
令牌桶 令牌随时间补充 允许突发
漏桶 请求以固定速率处理 平滑流量

实现(使用ASP.NET Core的令牌桶)

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.RateLimiting;
using StackExchange.Redis;
using System.Threading.RateLimiting;

/// <summary>
/// 令牌桶速率限制结果。
/// </summary>
public sealed record RateLimitResult(
    bool IsAllowed,
    int Remaining,
    long ResetTimeUnix,
    int RetryAfterSeconds = 0);

/// <summary>
/// 使用Redis的令牌桶速率限制器,用于分布式速率限制。
/// </summary>
public sealed class TokenBucketRateLimiter(IConnectionMultiplexer redis, int rate, int capacity)
{
    private readonly IDatabase _db = redis.GetDatabase();

    private const string LuaScript = """
        local key = KEYS[1]
        local rate = tonumber(ARGV[1])
        local capacity = tonumber(ARGV[2])
        local now = tonumber(ARGV[3])

        local bucket = redis.call('HMGET', key, 'tokens', 'last_update')
        local tokens = tonumber(bucket[1]) or capacity
        local last_update = tonumber(bucket[2]) or now

        local elapsed = now - last_update
        tokens = math.min(capacity, tokens + (elapsed * rate))

        local allowed = 0
        if tokens >= 1 then
            tokens = tokens - 1
            allowed = 1
        end

        redis.call('HMSET', key, 'tokens', tokens, 'last_update', now)
        redis.call('EXPIRE', key, math.ceil(capacity / rate) + 1)

        return {allowed, math.floor(tokens), math.ceil((1 - tokens) / rate)}
        """;

    public async Task<RateLimitResult> CheckAsync(string key, CancellationToken ct = default)
    {
        var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        var bucketKey = $"ratelimit:{key}";

        var result = (RedisResult[]?)await _db.ScriptEvaluateAsync(
            LuaScript,
            keys: [bucketKey],
            values: [rate, capacity, now]);

        if (result is null || result.Length < 3)
            return new RateLimitResult(false, 0, now, 60);

        var allowed = (int)result[0] == 1;
        var remaining = (int)result[1];
        var retryAfter = (int)result[2];

        return new RateLimitResult(
            IsAllowed: allowed,
            Remaining: remaining,
            ResetTimeUnix: now + (capacity - remaining) / rate,
            RetryAfterSeconds: allowed ? 0 : retryAfter);
    }
}

/// <summary>
/// ASP.NET Core速率限制中间件。
/// </summary>
public sealed class RateLimitingMiddleware(RequestDelegate next, TokenBucketRateLimiter limiter)
{
    public async Task InvokeAsync(HttpContext context)
    {
        var identifier = context.Request.Headers["X-API-Key"].FirstOrDefault()
            ?? context.Connection.RemoteIpAddress?.ToString()
            ?? "unknown";

        var result = await limiter.CheckAsync(identifier, context.RequestAborted);

        context.Response.Headers["X-RateLimit-Remaining"] = result.Remaining.ToString();
        context.Response.Headers["X-RateLimit-Reset"] = result.ResetTimeUnix.ToString();

        if (!result.IsAllowed)
        {
            context.Response.Headers["Retry-After"] = result.RetryAfterSeconds.ToString();
            context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            await context.Response.WriteAsJsonAsync(new { error = "速率限制超出" });
            return;
        }

        await next(context);
    }
}

// 或者,使用内置的ASP.NET Core速率限制(.NET 7+)
// 在Program.cs中:
// builder.Services.AddRateLimiter(options =>
// {
//     options.AddTokenBucketLimiter("api", config =>
//     {
//         config.TokenLimit = 100;
//         config.ReplenishmentPeriod = TimeSpan.FromSeconds(1);
//         config.TokensPerPeriod = 10;
//         config.QueueLimit = 0;
//     });
// });

输入验证

架构验证

using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;

/// <summary>
/// 具有验证的请求模型 - 通过显式属性防止批量赋值。
/// </summary>
public sealed partial class CreateUserRequest : IValidatableObject
{
    [Required]
    [StringLength(30, MinimumLength = 3)]
    [RegularExpression(@"^[a-zA-Z0-9_]+$", ErrorMessage = "用户名必须仅为字母数字和下划线")]
    public required string Username { get; init; }

    [Required]
    [EmailAddress]
    public required string Email { get; init; }

    [Required]
    [StringLength(128, MinimumLength = 12)]
    public required string Password { get; init; }

    [RegularExpression(@"^(user|admin|moderator)$")]
    public string Role { get; init; } = "user";

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        // 密码强度验证
        if (!UppercasePattern().IsMatch(Password))
            yield return new ValidationResult("密码必须包含大写字母", [nameof(Password)]);

        if (!LowercasePattern().IsMatch(Password))
            yield return new ValidationResult("密码必须包含小写字母", [nameof(Password)]);

        if (!DigitPattern().IsMatch(Password))
            yield return new ValidationResult("密码必须包含数字", [nameof(Password)]);

        if (!SpecialCharPattern().IsMatch(Password))
            yield return new ValidationResult("密码必须包含特殊字符", [nameof(Password)]);
    }

    [GeneratedRegex(@"[A-Z]")]
    private static partial Regex UppercasePattern();

    [GeneratedRegex(@"[a-z]")]
    private static partial Regex LowercasePattern();

    [GeneratedRegex(@"\d")]
    private static partial Regex DigitPattern();

    [GeneratedRegex(@"[!@#$%^&*(),.?"":{}|<>]")]
    private static partial Regex SpecialCharPattern();
}

// 在ASP.NET Core Minimal API中使用
// app.MapPost("/users", async (CreateUserRequest request) =>
// {
//     // 模型自动验证,额外字段被忽略(仅绑定定义的属性)
//     return Results.Created($"/users/{request.Username}", new { message = "用户已创建", username = request.Username });
// });

// 对于严格的批量赋值防止,使用JsonSerializerOptions:
// builder.Services.ConfigureHttpJsonOptions(options =>
// {
//     options.SerializerOptions.UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow;
// });

OpenAPI架构验证

# openapi.yaml
openapi: 3.0.3
info:
  title: 安全API
  version: 1.0.0

paths:
  /users:
    post:
      summary: 创建用户
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUser'
      responses:
        '201':
          description: 用户已创建

components:
  schemas:
    CreateUser:
      type: object
      required:
        - username
        - email
        - password
      additionalProperties: false  # 防止批量赋值
      properties:
        username:
          type: string
          minLength: 3
          maxLength: 30
          pattern: '^[a-zA-Z0-9_]+$'
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 12
          maxLength: 128
        role:
          type: string
          enum: [user, admin, moderator]
          default: user

CORS配置

安全的CORS设置

// 在Program.cs中 - ASP.NET Core CORS配置

// 错误:允许所有来源(不安全)
// builder.Services.AddCors(options => options.AddDefaultPolicy(policy => policy.AllowAnyOrigin()));

// 正确:白名单特定来源
builder.Services.AddCors(options =>
{
    options.AddPolicy("SecurePolicy", policy =>
    {
        policy.WithOrigins(
                "https://app.example.com",
                "https://admin.example.com")
            .WithMethods("GET", "POST", "PUT", "DELETE")
            .WithHeaders("Content-Type", "Authorization", "X-Request-ID")
            .WithExposedHeaders("X-RateLimit-Remaining", "X-Request-ID")
            .AllowCredentials()
            .SetPreflightMaxAge(TimeSpan.FromHours(24));
    });
});

// 应用到特定端点
app.MapControllers().RequireCors("SecurePolicy");

// 或全局应用
app.UseCors("SecurePolicy");

CORS头解释

目的 安全值
Access-Control-Allow-Origin 允许的来源 特定域(非*
Access-Control-Allow-Methods 允许的HTTP方法 仅所需方法
Access-Control-Allow-Headers 允许的请求头 特定头
Access-Control-Allow-Credentials 允许cookie/认证 仅在特定来源时为true
Access-Control-Max-Age 预检缓存时间 86400(24小时)
Access-Control-Expose-Headers 客户端可读的头 自定义头

带有验证的动态CORS

using Microsoft.AspNetCore.Cors.Infrastructure;

/// <summary>
/// 自定义CORS策略提供者,动态验证来源。
/// </summary>
public sealed class DynamicCorsPolicyProvider(IConfiguration configuration) : ICorsPolicyProvider
{
    private static readonly FrozenSet<string> AllowedOrigins = new HashSet<string>
    {
        "https://app.example.com",
        "https://admin.example.com",
        "https://staging.example.com",
    }.ToFrozenSet();

    public Task<CorsPolicy?> GetPolicyAsync(HttpContext context, string? policyName)
    {
        var origin = context.Request.Headers.Origin.ToString();

        // 从配置加载额外来源(可选)
        var configuredOrigins = configuration.GetSection("Cors:AllowedOrigins")
            .Get<string[]>() ?? [];

        var allAllowed = AllowedOrigins.Union(configuredOrigins).ToHashSet();

        if (string.IsNullOrEmpty(origin) || !allAllowed.Contains(origin))
        {
            return Task.FromResult<CorsPolicy?>(null);  // 无CORS头
        }

        var policy = new CorsPolicyBuilder()
            .WithOrigins(origin)
            .WithMethods("GET", "POST", "PUT", "DELETE")
            .WithHeaders("Content-Type", "Authorization", "X-Request-ID")
            .AllowCredentials()
            .Build();

        return Task.FromResult<CorsPolicy?>(policy);
    }
}

// 在Program.cs中注册:
// builder.Services.AddSingleton<ICorsPolicyProvider, DynamicCorsPolicyProvider>();
// builder.Services.AddCors();
// app.UseCors();

安全头

基本安全头

using Microsoft.AspNetCore.Http;

/// <summary>
/// 中间件,向所有响应添加安全头。
/// </summary>
public sealed class SecurityHeadersMiddleware(RequestDelegate next)
{
    public async Task InvokeAsync(HttpContext context)
    {
        // 在响应发送前添加安全头
        context.Response.OnStarting(() =>
        {
            var headers = context.Response.Headers;

            // 防止MIME类型嗅探
            headers["X-Content-Type-Options"] = "nosniff";

            // 点击劫持保护
            headers["X-Frame-Options"] = "DENY";

            // XSS过滤器(旧版浏览器)
            headers["X-XSS-Protection"] = "1; mode=block";

            // 严格传输安全
            headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload";

            // 内容安全策略(针对API响应)
            headers["Content-Security-Policy"] = "default-src 'none'; frame-ancestors 'none'";

            // 引用者策略
            headers["Referrer-Policy"] = "strict-origin-when-cross-origin";

            // 权限策略
            headers["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()";

            // 认证请求的缓存控制
            if (context.Request.Headers.ContainsKey("Authorization"))
            {
                headers["Cache-Control"] = "no-store, no-cache, must-revalidate, private";
                headers["Pragma"] = "no-cache";
            }

            return Task.CompletedTask;
        });

        await next(context);
    }
}

// 扩展方法,便于注册
public static class SecurityHeadersExtensions
{
    public static IApplicationBuilder UseSecurityHeaders(this IApplicationBuilder app)
        => app.UseMiddleware<SecurityHeadersMiddleware>();
}

// 在Program.cs中使用:
// app.UseSecurityHeaders();

按响应类型的头配置

API响应 错误响应 文件下载
Content-Type application/json application/json 特定MIME
X-Content-Type-Options nosniff nosniff nosniff
Cache-Control no-store(敏感) no-store 变化
Content-Disposition - - attachment

防范BOLA/IDOR

对象级别授权

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

/// <summary>
/// 对象级别访问控制的授权过滤器(BOLA/IDOR保护)。
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public sealed class AuthorizeResourceAttribute(string resourceType, string paramName = "id")
    : Attribute, IAsyncAuthorizationFilter
{
    public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        var userId = context.HttpContext.User.FindFirst("sub")?.Value;
        if (userId is null)
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        // 从路由数据获取资源ID
        if (!context.RouteData.Values.TryGetValue(paramName, out var resourceIdObj)
            || resourceIdObj is not string resourceId)
        {
            context.Result = new BadRequestObjectResult(new { error = "需要资源ID" });
            return;
        }

        // 从DI解析授权服务
        var authService = context.HttpContext.RequestServices
            .GetRequiredService<IResourceAuthorizationService>();

        if (!await authService.CanAccessAsync(userId, resourceType, resourceId))
        {
            context.Result = new ForbidResult();
        }
    }
}

/// <summary>
/// 检查资源级别访问的服务。
/// </summary>
public interface IResourceAuthorizationService
{
    Task<bool> CanAccessAsync(string userId, string resourceType, string resourceId);
}

public sealed class ResourceAuthorizationService(ApplicationDbContext db) : IResourceAuthorizationService
{
    public async Task<bool> CanAccessAsync(string userId, string resourceType, string resourceId)
    {
        return resourceType switch
        {
            "document" => await db.Documents.AnyAsync(d =>
                d.Id == resourceId &&
                (d.OwnerId == userId || d.SharedWithUsers.Any(u => u.Id == userId))),

            "account" => await db.Accounts.AnyAsync(a =>
                a.Id == resourceId && a.UserId == userId),

            _ => false
        };
    }
}

// 在控制器中使用
[ApiController]
[Route("api/documents")]
public sealed class DocumentsController(ApplicationDbContext db) : ControllerBase
{
    [HttpGet("{id}")]
    [Authorize]
    [AuthorizeResource("document", "id")]
    public async Task<IActionResult> GetDocument(string id)
    {
        // 如果到达这里,用户已授权
        var document = await db.Documents.FindAsync(id);
        return Ok(document);
    }
}

防止枚举

using System.ComponentModel.DataAnnotations;

// 选项1:使用UUID(推荐)
// EF Core实体,GUID作为主键
public sealed class Document
{
    [Key]
    public Guid Id { get; init; } = Guid.NewGuid();

    public required string Title { get; set; }
    public required string OwnerId { get; init; }
}

// 选项2:使用Hashids混淆顺序ID
// NuGet: HashidsNet
using HashidsNet;

/// <summary>
/// ID混淆服务,防止枚举攻击。
/// </summary>
public sealed class IdObfuscator
{
    private readonly Hashids _hashids;

    public IdObfuscator(IConfiguration config)
    {
        var salt = config["Security:HashidsSalt"]
            ?? throw new InvalidOperationException("未配置HashidsSalt");
        _hashids = new Hashids(salt, minHashLength: 8);
    }

    public string Encode(int id) => _hashids.Encode(id);
    public string Encode(long id) => _hashids.EncodeLong(id);

    public int? DecodeInt(string hash)
    {
        var result = _hashids.Decode(hash);
        return result.Length > 0 ? result[0] : null;
    }

    public long? DecodeLong(string hash)
    {
        var result = _hashids.DecodeLong(hash);
        return result.Length > 0 ? result[0] : null;
    }
}

// 在控制器中使用
[ApiController]
[Route("api/documents")]
public sealed class DocumentsController(
    ApplicationDbContext db,
    IdObfuscator idObfuscator) : ControllerBase
{
    [HttpGet("{hashId}")]
    public async Task<IActionResult> GetDocument(string hashId)
    {
        var docId = idObfuscator.DecodeInt(hashId);
        if (docId is null)
            return BadRequest(new { error = "无效ID" });

        var document = await db.Documents.FindAsync(docId.Value);
        if (document is null)
            return NotFound();

        // ... 授权检查
        return Ok(document);
    }
}

// 在Program.cs中注册:
// builder.Services.AddSingleton<IdObfuscator>();

防止数据暴露

响应过滤

using System.Text.Json.Serialization;

// 完整实体(内部 - 存储在数据库中)
public sealed class UserEntity
{
    public required string Id { get; init; }
    public required string Email { get; set; }
    public required string PasswordHash { get; set; }  // 永不暴露!
    public required string Role { get; set; }
    public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
    public DateTime LastLogin { get; set; }
    public int FailedLoginAttempts { get; set; }
    public string? InternalNotes { get; set; }
}

// 公共响应DTO(有限字段)
public sealed record UserResponse(
    string Id,
    string Email,
    string Role,
    DateTime CreatedAt);

// 管理员响应DTO(扩展字段)
public sealed record UserAdminResponse(
    string Id,
    string Email,
    string Role,
    DateTime CreatedAt,
    DateTime LastLogin,
    int FailedLoginAttempts) : UserResponse(Id, Email, Role, CreatedAt);

// 映射器扩展
public static class UserMapper
{
    public static UserResponse ToPublicResponse(this UserEntity user) =>
        new(user.Id, user.Email, user.Role, user.CreatedAt);

    public static UserAdminResponse ToAdminResponse(this UserEntity user) =>
        new(user.Id, user.Email, user.Role, user.CreatedAt, user.LastLogin, user.FailedLoginAttempts);
}

// 带有基于角色的响应过滤的控制器
[ApiController]
[Route("api/users")]
[Authorize]
public sealed class UsersController(ApplicationDbContext db) : ControllerBase
{
    [HttpGet("{userId}")]
    public async Task<IActionResult> GetUser(string userId)
    {
        var user = await db.Users.FindAsync(userId);
        if (user is null)
            return NotFound();

        // 基于角色返回不同视图
        var isAdmin = User.IsInRole("admin");
        return Ok(isAdmin ? user.ToAdminResponse() : user.ToPublicResponse());
    }
}

// 或者,对于简单情况使用JsonIgnore(不推荐用于复杂场景)
// public sealed class UserDto
// {
//     [JsonIgnore]
//     public string PasswordHash { get; init; } = null!;  // 永不序列化
// }

GraphQL安全

// 使用Hot Chocolate GraphQL服务器(NuGet: HotChocolate.AspNetCore)
using HotChocolate;
using HotChocolate.Authorization;
using HotChocolate.Data;
using HotChocolate.Resolvers;

// GraphQL类型,带有字段级别授权
public sealed class UserType
{
    public string Id { get; init; } = null!;

    // 带有条件屏蔽的字段解析器
    public string? GetEmail([Service] IHttpContextAccessor httpContext)
    {
        var currentUserId = httpContext.HttpContext?.User.FindFirst("sub")?.Value;
        var isAdmin = httpContext.HttpContext?.User.IsInRole("admin") ?? false;

        // 仅当查看自己资料或为管理员时显示邮箱
        if (currentUserId == Id || isAdmin)
            return Email;

        return null;  // 为其他用户屏蔽
    }

    // 不暴露:PasswordHash、InternalNotes等。
    // 不要在GraphQL类型中包含它们

    [GraphQLIgnore]
    public string Email { get; init; } = null!;
}

// 带有分页限制和授权的查询
public sealed class Query
{
    [UseDbContext(typeof(ApplicationDbContext))]
    [UsePaging(MaxPageSize = 100, DefaultPageSize = 25)]  // 限制结果
    [UseFiltering]
    [UseSorting]
    [Authorize]  // 要求认证
    public IQueryable<UserType> GetUsers([ScopedService] ApplicationDbContext db)
        => db.Users.Select(u => new UserType { Id = u.Id, Email = u.Email });

    [UseDbContext(typeof(ApplicationDbContext))]
    [Authorize]
    public async Task<UserType?> GetUser(
        [ScopedService] ApplicationDbContext db,
        string id)
    {
        var user = await db.Users.FindAsync(id);
        return user is null ? null : new UserType { Id = user.Id, Email = user.Email };
    }
}

// 在Program.cs中注册:
// builder.Services
//     .AddGraphQLServer()
//     .AddQueryType<Query>()
//     .AddAuthorization()
//     .AddFiltering()
//     .AddSorting()
//     .AddProjections()
//     .SetPagingOptions(new PagingOptions { MaxPageSize = 100, DefaultPageSize = 25 });

快速决策树

您有什么API安全问题?

  1. 选择认证方法 → 参见认证方法表格
  2. 实现速率限制 → 使用Redis的令牌桶(最灵活)
  3. 配置CORS → 白名单特定来源,绝不使用*
  4. 设置安全头 → 应用所有基本安全头
  5. 验证输入 → 使用DataAnnotations/FluentValidation进行严格绑定
  6. 防止BOLA → 每个端点上的对象级别授权
  7. 防止数据暴露 → 使用类型化模型的响应过滤

安全清单

认证

  • [ ] 所有端点的强认证
  • [ ] API密钥存储为哈希值,非明文
  • [ ] 敏感操作的请求签名
  • [ ] 令牌过期和轮换

授权

  • [ ] 对象级别授权检查
  • [ ] 功能级别授权检查
  • [ ] 无顺序ID(使用UUID或哈希)
  • [ ] 基于权限的响应过滤

速率限制

  • [ ] 所有端点的速率限制
  • [ ] 每客户端速率限制
  • [ ] 每个端点类型的适当限制
  • [ ] 429响应上的Retry-After头

输入验证

  • [ ] 所有输入的架构验证
  • [ ] 拒绝额外字段(批量赋值保护)
  • [ ] 文件上传验证(大小、类型、内容)
  • [ ] SQL注入预防(参数化查询)

头和CORS

  • [ ] CORS白名单(无通配符)
  • [ ] 所有响应上的安全头
  • [ ] 强制HTTPS(HSTS)
  • [ ] 敏感数据的缓存控制

参考文献

相关技能

技能 关系
authentication-patterns JWT、OAuth实现细节
authorization-models RBAC、ABAC用于API授权
secure-coding 输入验证、注入预防

版本历史

  • v1.0.0 (2025-12-26): 初始版本,包含OWASP API Top 10、认证、速率限制、CORS、头

最后更新: 2025-12-26