API 安全与认证
大约 11 分钟约 3233 字
API 安全与认证
简介
API 安全是 Web 应用安全的核心环节。涵盖 JWT 认证、OAuth2.0 授权、CORS 配置、输入验证、HTTPS、防攻击等。掌握这些安全实践是构建生产级 API 的必要条件。
特点
JWT 认证
Token 结构
JWT = Header.Payload.Signature
Header: { "alg": "HS256", "typ": "JWT" }
Payload: { "sub": "123", "name": "张三", "role": "admin", "exp": 1744000000 }
Signature: HMACSHA256(base64(header) + "." + base64(payload), secret)
Access Token — 短期(15-30分钟),用于 API 访问
Refresh Token — 长期(7-30天),用于刷新 Access Token.NET JWT 配置
/// <summary>
/// JWT 认证配置
/// </summary>
// 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:SecretKey"]!))
};
// SignalR 支持
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var token = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(token) && path.StartsWithSegments("/hubs"))
{
context.Token = token;
}
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();Token 生成
/// <summary>
/// JWT Token 生成服务
/// </summary>
public class TokenService
{
private readonly IConfiguration _config;
public TokenService(IConfiguration config)
{
_config = config;
}
// 生成 Access Token
public string GenerateAccessToken(User user)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Name, user.UserName),
new(ClaimTypes.Role, user.Role),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:SecretKey"]!));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(30),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
// 生成 Refresh Token
public string GenerateRefreshToken()
{
return Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
}
}登录接口
/// <summary>
/// 登录和 Token 刷新
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IUserService _userService;
private readonly TokenService _tokenService;
public AuthController(IUserService userService, TokenService tokenService)
{
_userService = userService;
_tokenService = tokenService;
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
var user = await _userService.ValidateAsync(request.UserName, request.Password);
if (user == null) return Unauthorized("用户名或密码错误");
var accessToken = _tokenService.GenerateAccessToken(user);
var refreshToken = _tokenService.GenerateRefreshToken();
// 存储 Refresh Token
await _userService.SaveRefreshTokenAsync(user.Id, refreshToken);
// 设置 HttpOnly Cookie
Response.Cookies.Append("refresh_token", refreshToken, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
Expires = DateTimeOffset.UtcNow.AddDays(30)
});
return Ok(new { AccessToken = accessToken, User = new { user.Id, user.UserName, user.Role } });
}
[HttpPost("refresh")]
public async Task<IActionResult> Refresh()
{
var refreshToken = Request.Cookies["refresh_token"];
if (string.IsNullOrEmpty(refreshToken)) return Unauthorized();
var user = await _userService.GetByRefreshTokenAsync(refreshToken);
if (user == null) return Unauthorized();
var newAccessToken = _tokenService.GenerateAccessToken(user);
var newRefreshToken = _tokenService.GenerateRefreshToken();
await _userService.SaveRefreshTokenAsync(user.Id, newRefreshToken);
Response.Cookies.Append("refresh_token", newRefreshToken, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
Expires = DateTimeOffset.UtcNow.AddDays(30)
});
return Ok(new { AccessToken = newAccessToken });
}
[HttpPost("logout")]
[Authorize]
public async Task<IActionResult> Logout()
{
var refreshToken = Request.Cookies["refresh_token"];
if (!string.IsNullOrEmpty(refreshToken))
{
await _userService.RevokeRefreshTokenAsync(refreshToken);
}
Response.Cookies.Delete("refresh_token");
return Ok();
}
}授权策略
基于角色的授权
/// <summary>
/// 授权策略配置
/// </summary>
// 基于角色
[Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id) { }
// 基于策略
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
options.AddPolicy("EditorOrAbove", policy => policy.RequireRole("Admin", "Editor"));
options.AddPolicy("CanDelete", policy =>
policy.Requirements.Add(new PermissionRequirement("delete")));
});
// 使用策略
[Authorize(Policy = "AdminOnly")]
public class AdminController : ControllerBase { }CORS 跨域
配置 CORS
/// <summary>
/// CORS 配置
/// </summary>
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
{
policy.WithOrigins("https://www.example.com", "https://app.example.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
// 开发环境宽松策略
options.AddPolicy("AllowDev", policy =>
{
policy.WithOrigins("http://localhost:5173")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
var app = builder.Build();
app.UseCors(app.Environment.IsDevelopment() ? "AllowDev" : "AllowFrontend");输入验证
防注入
/// <summary>
/// 输入验证 — 防止注入攻击
/// </summary>
// 1. 数据注解验证
public class CreateProductRequest
{
[Required]
[StringLength(200, MinimumLength = 1)]
[RegularExpression(@"^[a-zA-Z0-9\u4e00-\u9fa5\s\-_]+$",
ErrorMessage = "商品名称包含非法字符")]
public string Name { get; set; } = "";
[Range(0.01, 999999.99)]
public decimal Price { get; set; }
[StringLength(2000)]
public string Description { get; set; } = "";
}
// 2. SQL 注入防护 — 使用参数化查询
// 差
var sql = $"SELECT * FROM Users WHERE Name = '{name}'"; // SQL 注入!
// 好
var sql = "SELECT * FROM Users WHERE Name = @Name"; // 参数化
// EF Core 默认参数化,安全
// 3. XSS 防护 — 输出编码
// 前端使用 {{ }} 自动编码(Vue/Blazor)
// 后端对用户输入进行 HTML 编码
public static string SanitizeHtml(string input)
{
return System.Web.HttpUtility.HtmlEncode(input);
}HTTPS 与安全头
安全中间件
/// <summary>
/// 安全响应头
/// </summary>
app.Use(async (context, next) =>
{
// 防止 MIME 嗅探
context.Response.Headers.XContentTypeOptions = "nosniff";
// 防止点击劫持
context.Response.Headers.XFrameOptions = "DENY";
// XSS 保护
context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
// HSTS(强制 HTTPS)
context.Response.Headers.StrictTransportSecurity = "max-age=31536000; includeSubDomains";
await next();
});
// HTTPS 重定向
app.UseHttpsRedirection();
// HSTS(生产环境)
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}API 密钥认证
服务间认证
/// <summary>
/// API Key 认证 — 服务间调用
/// </summary>
public class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
private const string ApiKeyHeader = "X-API-Key";
public ApiKeyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
if (!context.Request.Headers.TryGetValue(ApiKeyHeader, out var key))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("API Key 缺失");
return;
}
var validKey = context.RequestServices
.GetRequiredService<IConfiguration>()["ApiKeys:Internal"];
if (key != validKey)
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("API Key 无效");
return;
}
await _next(context);
}
}安全检查清单
| 项目 | 措施 | 优先级 |
|---|---|---|
| HTTPS | 全站 HTTPS | 高 |
| 认证 | JWT + Refresh Token | 高 |
| 授权 | RBAC 角色权限 | 高 |
| 输入验证 | 参数化查询 + 注解验证 | 高 |
| CORS | 白名单策略 | 高 |
| 限流 | 防暴力破解 | 中 |
| 安全头 | HSTS/X-Frame-Options | 中 |
| 日志 | 审计日志 | 中 |
| 密码 | BCrypt/PBKDF2 哈希 | 高 |
API 密钥管理与密钥轮换
/// <summary>
/// API Key 认证中间件
/// </summary>
public class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
private const string ApiKeyHeaderName = "X-API-Key";
public ApiKeyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
if (!context.Request.Headers.TryGetValue(ApiKeyHeaderName, out var extractedApiKey))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("API Key 缺失");
return;
}
var configuration = context.RequestServices.GetRequiredService<IConfiguration>();
var validApiKeys = configuration.GetSection("ApiKeys").Get<List<ApiKeyConfig>>();
var apiKey = validApiKeys?.FirstOrDefault(k =>
k.Key == extractedApiKey && k.IsActive);
if (apiKey == null)
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("无效的 API Key");
return;
}
// 将 API Key 信息存入 HttpContext
context.Items["ApiKey"] = apiKey;
await _next(context);
}
}
public class ApiKeyConfig
{
public string Key { get; set; } = "";
public string Owner { get; set; } = "";
public string[] Scopes { get; set; } = Array.Empty<string>();
public bool IsActive { get; set; } = true;
public DateTime? ExpiresAt { get; set; }
}
// 注册中间件
app.UseMiddleware<ApiKeyMiddleware>();// appsettings.json — API Key 配置
{
"ApiKeys": [
{
"Key": "ak_prod_xxxxxxxxxxxx",
"Owner": "frontend-app",
"Scopes": ["read:users", "write:orders"],
"IsActive": true,
"ExpiresAt": null
},
{
"Key": "ak_partner_yyyyyyyyyyyy",
"Owner": "partner-service",
"Scopes": ["read:products"],
"IsActive": true,
"ExpiresAt": "2025-12-31T23:59:59Z"
}
]
}JWT 密钥轮换策略
/// <summary>
/// JWT 密钥轮换 — 支持同时验证旧密钥和新密钥
/// </summary>
public class KeyRotationService
{
private readonly IConfiguration _configuration;
private readonly ILogger<KeyRotationService> _logger;
public KeyRotationService(IConfiguration configuration, ILogger<KeyRotationService> logger)
{
_configuration = configuration;
_logger = logger;
}
/// <summary>
/// 获取 TokenValidationParameters,支持多密钥验证
/// </summary>
public TokenValidationParameters GetValidationParameters()
{
var currentKey = _configuration["Jwt:SecretKey"]!;
var previousKey = _configuration["Jwt:PreviousSecretKey"];
var signingKeys = new List<SecurityKey>
{
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(currentKey))
};
// 如果有旧密钥,也加入验证列表(平滑过渡)
if (!string.IsNullOrEmpty(previousKey))
{
signingKeys.Add(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(previousKey)));
}
return new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKeys = signingKeys,
ValidateIssuer = true,
ValidIssuer = _configuration["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = _configuration["Jwt:Audience"],
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(1), // 时间偏差容忍
};
}
/// <summary>
/// 执行密钥轮换
/// 步骤:
/// 1. 将当前密钥保存为 PreviousSecretKey
/// 2. 生成新密钥设为 SecretKey
/// 3. Access Token 短期(15分钟),旧 Token 自然过期
/// 4. 过渡期结束后移除 PreviousSecretKey
/// </summary>
public void RotateKey()
{
var newKey = GenerateSecureKey();
_logger.LogWarning("JWT 密钥轮换已执行,过渡期内新旧密钥同时有效");
// 实际项目中,密钥应存储在 Key Vault 或配置中心
}
private static string GenerateSecureKey()
{
var bytes = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(bytes);
return Convert.ToBase64String(bytes);
}
}细粒度权限控制(ABAC)
/// <summary>
/// 基于属性的访问控制(ABAC)
/// </summary>
public class AbacPolicy
{
public string Name { get; set; } = "";
public string Description { get; set; } = "";
public Func<AccessContext, bool> Evaluate { get; set; } = _ => false;
}
public class AccessContext
{
public ClaimsPrincipal User { get; set; } = null!;
public string Resource { get; set; } = "";
public string Action { get; set; } = "";
public Dictionary<string, object> Environment { get; set; } = new();
}
public class AbacAuthorizationService
{
private readonly List<AbacPolicy> _policies = new();
public void AddPolicy(AbacPolicy policy) => _policies.Add(policy);
public bool Authorize(AccessContext context)
{
// 至少有一个策略通过即可
return _policies.Any(p => p.Evaluate(context));
}
}
// 注册策略
var authService = new AbacAuthorizationService();
// 策略1:管理员可以访问所有资源
authService.AddPolicy(new AbacPolicy
{
Name = "AdminFullAccess",
Description = "管理员拥有完全访问权限",
Evaluate = ctx => ctx.User.IsInRole("Admin")
});
// 策略2:用户只能在工作时间访问敏感数据
authService.AddPolicy(new AbacPolicy
{
Name = "BusinessHoursOnly",
Description = "仅工作时间允许访问敏感资源",
Evaluate = ctx =>
{
var hour = DateTime.Now.Hour;
return ctx.User.IsInRole("User")
&& ctx.Action == "read"
&& hour >= 9 && hour < 18;
}
});
// 策略3:用户只能访问自己部门的数据
authService.AddPolicy(new AbacPolicy
{
Name = "DepartmentDataAccess",
Description = "用户只能访问本部门数据",
Evaluate = ctx =>
{
var userDept = ctx.User.FindFirst("department")?.Value;
var resourceDept = ctx.Environment.GetValueOrDefault("department")?.ToString();
return ctx.User.IsInRole("User")
&& userDept == resourceDept;
}
});安全响应头配置
/// <summary>
/// 安全响应头中间件
/// </summary>
public class SecurityHeadersMiddleware
{
private readonly RequestDelegate _next;
public SecurityHeadersMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// 防止 MIME 类型嗅探
context.Response.Headers["X-Content-Type-Options"] = "nosniff";
// 防止点击劫持
context.Response.Headers["X-Frame-Options"] = "DENY";
// XSS 保护(旧浏览器)
context.Response.Headers["X-XSS-Protection"] = "1; mode=block";
// Referrer 策略
context.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
// 内容安全策略(CSP)
context.Response.Headers["Content-Security-Policy"] =
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://cdn.example.com; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"connect-src 'self' https://api.example.com; " +
"frame-ancestors 'none'";
// 权限策略
context.Response.Headers["Permissions-Policy"] =
"camera=(), microphone=(), geolocation=(self)";
// HSTS(仅 HTTPS 环境下)
if (context.Request.IsHttps)
{
context.Response.Headers["Strict-Transport-Security"] =
"max-age=31536000; includeSubDomains; preload";
}
await _next(context);
}
}
// 注册
app.UseMiddleware<SecurityHeadersMiddleware>();SQL 注入防护深入
// ========== 错误:字符串拼接(存在 SQL 注入风险) ==========
// var query = "SELECT * FROM Users WHERE Name = '" + userName + "'";
// 攻击输入: ' OR '1'='1' --
// 实际执行: SELECT * FROM Users WHERE Name = '' OR '1'='1' --'
// ========== 正确:参数化查询 ==========
// Dapper 示例
public class UserRepository
{
private readonly IDbConnection _db;
public UserRepository(IDbConnection db) => _db = db;
// 参数化查询 — 安全
public User? FindByName(string name)
{
return _db.QueryFirstOrDefault<User>(
"SELECT * FROM Users WHERE Name = @Name",
new { Name = name });
}
// 参数化 LIKE 查询
public IEnumerable<User> SearchByName(string keyword)
{
return _db.Query<User>(
"SELECT * FROM Users WHERE Name LIKE @Pattern",
new { Pattern = $"%{keyword}%" });
}
// 批量插入 — 使用参数化
public void BulkInsert(IEnumerable<User> users)
{
_db.Execute(
"INSERT INTO Users (Name, Email, Role) VALUES (@Name, @Email, @Role)",
users.Select(u => new { u.Name, u.Email, u.Role }));
}
}
// 输入验证 — 第一道防线
public class UserInputValidator
{
// 使用正则验证输入格式
public static bool IsValidEmail(string email)
{
return Regex.IsMatch(email, @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$");
}
// 白名单验证 — 只允许安全字符
public static bool IsValidUsername(string username)
{
return Regex.IsMatch(username, @"^[a-zA-Z0-9_]{3,20}$");
}
// 清理 HTML 标签
public static string SanitizeHtml(string input)
{
return Regex.Replace(input, @"<[^>]*>", string.Empty);
}
}优点
缺点
总结
API 安全的核心:HTTPS 传输加密、JWT 无状态认证、RBAC 权限控制、参数化查询防注入。密码用 BCrypt 哈希,Token 短期 + Refresh 机制,CORS 白名单策略,生产环境务必开启限流和安全响应头。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
- 安全类主题的关键不只在认证成功,而在于权限边界、证书信任链和审计链路是否完整。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
- 明确令牌生命周期、刷新策略、作用域、Claims 和失败返回模型。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
- 只验证登录成功,不验证权限收敛和令牌失效场景。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
- 继续深入零信任、细粒度授权、证书自动化和密钥轮换。
适用场景
- 当你准备把《API 安全与认证》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《API 安全与认证》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《API 安全与认证》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《API 安全与认证》最大的收益和代价分别是什么?
