API 安全与 OAuth2 实战
大约 8 分钟约 2486 字
API 安全与 OAuth2 实战
简介
API 安全涵盖认证(Authentication)和授权(Authorization)两个层面。OAuth2 和 OpenID Connect 是现代 API 安全的标准协议。理解 JWT 令牌验证、OAuth2 授权码流程和细粒度权限控制,有助于构建安全的 API 服务。
特点
JWT 令牌详解
JWT 结构与验证
// JWT(JSON Web Token)结构:Header.Payload.Signature
// Header: { "alg": "RS256", "typ": "JWT" }
// Payload: { "sub": "user123", "name": "张三", "role": "admin", "exp": 1700000000 }
// Signature: RS256(base64(header) + "." + base64(payload), privateKey)
// ASP.NET Core JWT 配置
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// 方式 1: 使用 OIDC 发现端点(推荐)
options.Authority = "https://auth.example.com";
options.Audience = "my-api";
// 方式 2: 手动配置
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "https://auth.example.com",
ValidateAudience = true,
ValidAudience = "my-api",
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(30), // 时钟偏差容忍
ValidateIssuerSigningKey = true,
IssuerSigningKey = new RsaSecurityKey(rsaPublicKey),
// 映射 Claim
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role
};
// 自定义事件
options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
// 令牌验证后的自定义逻辑
var claims = context.Principal!.Claims;
// 检查用户是否被禁用
var userId = claims.FindFirstValue(JwtClaimTypes.Subject);
var userRepo = context.HttpContext.RequestServices.GetRequiredService<IUserRepository>();
var user = await userRepo.GetByIdAsync(Guid.Parse(userId!));
if (user?.IsDisabled == true)
{
context.Fail("用户已被禁用");
return;
}
// 添加自定义 Claim
var identity = (ClaimsIdentity)context.Principal.Identity!;
identity.AddClaim(new Claim("tenant_id", user?.TenantId.ToString() ?? ""));
},
OnAuthenticationFailed = context =>
{
if (context.Exception is SecurityTokenExpiredException)
{
context.Response.Headers.Append("Token-Expired", "true");
}
return Task.CompletedTask;
},
OnChallenge = context =>
{
// 自定义 401 响应
context.HandleResponse();
context.Response.StatusCode = 401;
return context.Response.WriteAsJsonAsync(new
{
type = "https://example.com/errors/unauthorized",
title = "Unauthorized",
status = 401,
detail = "需要有效的访问令牌"
});
}
};
});
app.UseAuthentication();
app.UseAuthorization();手动签发 JWT
// 用于测试或内部服务的 JWT 签发
public class TokenService
{
private readonly RsaSecurityKey _privateKey;
private readonly string _issuer;
private readonly string _audience;
public TokenService(RsaSecurityKey privateKey, string issuer, string audience)
{
_privateKey = privateKey;
_issuer = issuer;
_audience = audience;
}
public string GenerateToken(User user, TimeSpan? expiration = null)
{
var expires = DateTime.UtcNow.Add(expiration ?? TimeSpan.FromHours(1));
var claims = new List<Claim>
{
new(JwtClaimTypes.Subject, user.Id.ToString()),
new(JwtClaimTypes.Name, user.Name),
new(JwtClaimTypes.Email, user.Email),
new(JwtClaimTypes.JwtId, Guid.NewGuid().ToString()),
new("tenant_id", user.TenantId.ToString())
};
// 添加角色
foreach (var role in user.Roles)
{
claims.Add(new Claim(JwtClaimTypes.Role, role));
}
// 添加权限
foreach (var permission in user.Permissions)
{
claims.Add(new Claim("permission", permission));
}
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = expires,
Issuer = _issuer,
Audience = _audience,
SigningCredentials = new SigningCredentials(_privateKey, SecurityAlgorithms.RsaSha256),
// 加密令牌(可选)
EncryptingCredentials = new EncryptingCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("encryption-key-32-bytes-long!")),
JwtConstants.DirectKeyUseAlg,
SecurityAlgorithms.Aes256CbcHmacSha512)
};
var handler = new JwtSecurityTokenHandler();
var token = handler.CreateEncodedJwt(tokenDescriptor);
return token;
}
// Refresh Token
public (string AccessToken, string RefreshToken) GenerateTokenPair(User user)
{
var accessToken = GenerateToken(user, TimeSpan.FromMinutes(15));
var refreshToken = GenerateRefreshToken();
return (accessToken, refreshToken);
}
private string GenerateRefreshToken()
{
var bytes = new byte[64];
RandomNumberGenerator.Fill(bytes);
return Convert.ToBase64String(bytes);
}
}OAuth2 授权流程
授权码流程(Authorization Code)
// OAuth2 授权码流程(适合有后端的 Web 应用)
// 1. 客户端 → 授权服务器:重定向到授权页面
// 2. 用户登录并授权
// 3. 授权服务器 → 客户端:回调带 authorization_code
// 4. 客户端后端 → 授权服务器:用 code 换 token
// 5. 客户端后端 → API:带 access_token 访问
// Duende IdentityServer 配置(授权服务器端)
// dotnet add package Duende.IdentityServer
// 客户端配置
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = "https://auth.example.com";
options.ClientId = "web-app";
options.ClientSecret = "secret";
options.ResponseType = "code"; // 授权码模式
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("my-api");
options.Scope.Add("offline_access"); // Refresh Token
options.SaveTokens = true; // 保存令牌到 Cookie
options.GetClaimsFromUserInfoEndpoint = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role
};
// 自动刷新令牌
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = context =>
{
// 存储令牌
var accessToken = context.TokenEndpointResponse?.AccessToken;
var refreshToken = context.TokenEndpointResponse?.RefreshToken;
return Task.CompletedTask;
}
};
});客户端凭证流程
// OAuth2 客户端凭证流程(适合服务间通信)
// 1. 服务 A → 授权服务器:client_id + client_secret
// 2. 授权服务器 → 服务 A:access_token
// 3. 服务 A → 服务 B:带 access_token 调用 API
public class ServiceTokenClient
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _config;
private readonly IMemoryCache _cache;
private readonly SemaphoreSlim _semaphore = new(1, 1);
public ServiceTokenClient(IHttpClientFactory httpClientFactory, IConfiguration config, IMemoryCache cache)
{
_httpClientFactory = httpClientFactory;
_config = config;
_cache = cache;
}
public async Task<string> GetTokenAsync(CancellationToken ct = default)
{
const string cacheKey = "service_access_token";
if (_cache.TryGetValue(cacheKey, out string? token))
{
return token!;
}
await _semaphore.WaitAsync(ct);
try
{
// 双重检查
if (_cache.TryGetValue(cacheKey, out token))
{
return token!;
}
var client = _httpClientFactory.CreateClient("AuthServer");
var response = await client.RequestClientCredentialsTokenAsync(
new ClientCredentialsTokenRequest
{
Address = $"{_config["Auth:Authority"]}/connect/token",
ClientId = _config["Auth:ClientId"]!,
ClientSecret = _config["Auth:ClientSecret"]!,
Scope = "my-api"
}, ct);
if (response.IsError)
{
throw new InvalidOperationException($"获取令牌失败: {response.Error}");
}
// 缓存令牌(提前 1 分钟过期)
_cache.Set(cacheKey, response.AccessToken,
TimeSpan.FromSeconds(response.ExpiresIn - 60));
return response.AccessToken;
}
finally
{
_semaphore.Release();
}
}
}细粒度授权
基于策略的权限控制
// 1. 基于角色的授权
[Authorize(Roles = "Admin")]
[HttpGet("admin/dashboard")]
public IActionResult Dashboard() => Ok();
// 2. 基于策略的授权(推荐)
builder.Services.AddAuthorizationBuilder()
// 基于角色
.AddPolicy("AdminOnly", policy =>
policy.RequireRole("admin"))
// 基于声明
.AddPolicy("EmailVerified", policy =>
policy.RequireClaim("email_verified", "true"))
// 基于权限
.AddPolicy("CanCreateOrder", policy =>
policy.RequireClaim("permission", "order:create"))
// 组合要求
.AddPolicy("OrderManager", policy =>
policy.RequireRole("manager")
.RequireClaim("department", "sales"))
// 自定义要求
.AddPolicy("TenantAccess", policy =>
policy.AddRequirements(new TenantAccessRequirement()))
// 基于年龄
.AddPolicy("AdultOnly", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(18)));
// 3. 自定义授权要求
public class TenantAccessRequirement : IAuthorizationRequirement { }
public class TenantAccessHandler : AuthorizationHandler<TenantAccessRequirement>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ITenantService _tenantService;
public TenantAccessHandler(IHttpContextAccessor httpContextAccessor, ITenantService tenantService)
{
_httpContextAccessor = httpContextAccessor;
_tenantService = tenantService;
}
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
TenantAccessRequirement requirement)
{
var httpContext = _httpContextAccessor.HttpContext!;
var tenantId = httpContext.GetRouteValue("tenantId")?.ToString();
if (string.IsNullOrEmpty(tenantId))
{
context.Fail();
return Task.CompletedTask;
}
var userTenantId = context.User.FindFirstValue("tenant_id");
var userRole = context.User.FindFirstValue(JwtClaimTypes.Role);
// 管理员可以访问所有租户
if (userRole == "super-admin")
{
context.Succeed(requirement);
return Task.CompletedTask;
}
// 普通用户只能访问自己的租户
if (userTenantId == tenantId)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
context.Fail();
return Task.CompletedTask;
}
}
// 注册 Handler
builder.Services.AddScoped<IAuthorizationHandler, TenantAccessHandler>();
// 使用
[Authorize(Policy = "TenantAccess")]
[HttpGet("/api/tenants/{tenantId}/data")]
public IActionResult GetTenantData(string tenantId) => Ok();
// 4. Minimal API 授权
app.MapGet("/api/admin/users", () => "Admin Users")
.RequireAuthorization("AdminOnly");
app.MapGet("/api/orders", () => "Orders")
.RequireAuthorization(policy =>
{
policy.RequireRole("user", "admin");
policy.RequireClaim("permission", "order:read");
});
// 5. 资源级授权(基于数据的权限判断)
public class OrderAuthorizationHandler : AuthorizationHandler<OrderOperationRequirement, Order>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OrderOperationRequirement requirement,
Order resource)
{
var userId = context.User.FindFirstValue(JwtClaimTypes.Subject);
var userRole = context.User.FindFirstValue(JwtClaimTypes.Role);
return requirement.Operation switch
{
"view" when userRole == "admin" || resource.UserId.ToString() == userId
=> Task.FromResult(context.Succeed(requirement)),
"edit" when resource.UserId.ToString() == userId && resource.Status == OrderStatus.Created
=> Task.FromResult(context.Succeed(requirement)),
"cancel" when resource.UserId.ToString() == userId || userRole == "admin"
=> Task.FromResult(context.Succeed(requirement)),
_ => Task.CompletedTask
};
}
}安全防护
CORS 与安全头
// CORS 配置
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSpecific", policy =>
{
policy.WithOrigins("https://app.example.com", "https://admin.example.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Authorization", "Content-Type", "X-API-Version")
.AllowCredentials()
.SetPreflightMaxAge(TimeSpan.FromMinutes(10));
});
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
app.UseCors("AllowSpecific");
// 安全头中间件
app.Use(async (context, next) =>
{
context.Response.Headers.XContentTypeOptions = "nosniff";
context.Response.Headers.XFrameOptions = "DENY";
context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
context.Response.Headers.Add("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
context.Response.Headers.Add("Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
context.Response.Headers.Add("Referrer-Policy", "strict-origin-when-cross-origin");
context.Response.Headers.Add("Permissions-Policy",
"camera=(), microphone=(), geolocation=()");
await next();
});优点
缺点
总结
JWT 令牌通过 AddJwtBearer() 配置验证参数,支持 OIDC 发现端点自动获取公钥。OAuth2 授权码流程适合 Web 应用(带用户交互),客户端凭证流程适合服务间通信。基于策略的授权(AddPolicy)比基于角色更灵活,支持自定义 IAuthorizationHandler 实现资源级权限控制。安全防护包括 CORS 限制、安全响应头和 HTTPS 强制。建议使用短过期 Access Token + Refresh Token 模式。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
- 安全类主题的关键不只在认证成功,而在于权限边界、证书信任链和审计链路是否完整。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
- 明确令牌生命周期、刷新策略、作用域、Claims 和失败返回模型。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
- 只验证登录成功,不验证权限收敛和令牌失效场景。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
- 继续深入零信任、细粒度授权、证书自动化和密钥轮换。
适用场景
- 当你准备把《API 安全与 OAuth2 实战》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《API 安全与 OAuth2 实战》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《API 安全与 OAuth2 实战》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《API 安全与 OAuth2 实战》最大的收益和代价分别是什么?
