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);
}
速率限制
速率限制策略
| 策略 |
描述 |
使用场景 |
| 固定窗口 |
在固定时间窗口内计数请求 |
简单、可预测 |
| 滑动窗口 |
请求的滚动窗口 |
更平滑的限制 |
| 令牌桶 |
令牌随时间补充 |
允许突发 |
| 漏桶 |
请求以固定速率处理 |
平滑流量 |
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安全问题?
- 选择认证方法 → 参见认证方法表格
- 实现速率限制 → 使用Redis的令牌桶(最灵活)
- 配置CORS → 白名单特定来源,绝不使用
*
- 设置安全头 → 应用所有基本安全头
- 验证输入 → 使用DataAnnotations/FluentValidation进行严格绑定
- 防止BOLA → 每个端点上的对象级别授权
- 防止数据暴露 → 使用类型化模型的响应过滤
安全清单
认证
- [ ] 所有端点的强认证
- [ ] 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