共计 3693 个字符,预计需要花费 10 分钟才能阅读完成。
OAuth2.0 已成为现代应用身份验证的黄金标准,从单页应用到微服务架构,它解决了安全授权和资源访问的核心问题。但在实际开发中,很多 C# 开发者仍在使用原始的 HttpClient 直接请求 Token,这可能导致连接泄漏、DNS 不更新等问题。本文将带你用 HttpClientFactory 实现更可靠的解决方案。

为什么 HttpClientFactory 是更好的选择
直接使用 HttpClient 时,每次实例化都会创建新的 TCP 连接,频繁开关连接会导致端口耗尽(著名的『端口耗尽问题』)。更棘手的是,默认的 HttpClient 不会遵循 DNS 记录的 TTL,可能导致访问失效的 IP 地址。
HttpClientFactory 通过以下机制解决这些问题:
- 内置连接池管理,重用 HTTP 连接
- 自动处理 DNS 刷新
- 提供命名客户端和类型化客户端两种模式
- 与 Polly 等库无缝集成实现重试策略
授权码模式实现(含 PKCE)
PKCE(Proof Key for Code Exchange)是应对授权码拦截攻击的重要扩展,特别适用于移动端和 SPA 应用。以下是关键步骤:
- 生成 code_verifier 和 code_challenge
using System.Security.Cryptography;
using System.Text;
// 生成随机的 code_verifier
var rng = RandomNumberGenerator.Create();
var bytes = new byte[32];
rng.GetBytes(bytes);
var codeVerifier = Convert.ToBase64String(bytes)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
// 计算 SHA256 哈希作为 code_challenge
using var sha256 = SHA256.Create();
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
var codeChallenge = Convert.ToBase64String(challengeBytes)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
- 构建授权请求 URL(注意将 client_id 和 redirect_uri 进行 URL 编码):
var authorizationRequest = $"{authority}/connect/authorize?" +
$"client_id={Uri.EscapeDataString(clientId)}&" +
$"redirect_uri={Uri.EscapeDataString(redirectUri)}&" +
$"response_type=code&" +
$"scope={Uri.EscapeDataString(scope)}&" +
$"code_challenge={codeChallenge}&" +
$"code_challenge_method=S256&" +
$"state={Guid.NewGuid()}"; // 防 CSRF
- 用授权码交换 Token:
// 使用类型化 HttpClient
public class AuthService
{
private readonly HttpClient _httpClient;
public AuthService(HttpClient httpClient)
{_httpClient = httpClient;}
public async Task<TokenResponse> ExchangeCodeAsync(string code, string codeVerifier)
{var request = new HttpRequestMessage(HttpMethod.Post, "connect/token");
var formData = new Dictionary<string, string>
{["grant_type"] = "authorization_code",
["code"] = code,
["redirect_uri"] = redirectUri,
["client_id"] = clientId,
["code_verifier"] = codeVerifier
};
request.Content = new FormUrlEncodedContent(formData);
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<TokenResponse>();}
}
客户端凭证模式实现
对于服务间认证,客户端凭证模式更简单直接:
public async Task<TokenResponse> GetClientCredentialsTokenAsync()
{var request = new HttpRequestMessage(HttpMethod.Post, "connect/token");
var formData = new Dictionary<string, string>
{["grant_type"] = "client_credentials",
["client_id"] = clientId,
["client_secret"] = clientSecret,
["scope"] = scope
};
request.Content = new FormUrlEncodedContent(formData);
// 建议在生产环境添加 Client 证书等额外认证
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<TokenResponse>();}
Token 自动刷新与缓存
使用 MemoryCache 实现 Token 自动刷新,避免频繁请求新 Token:
public class TokenService
{
private readonly IMemoryCache _cache;
private readonly AuthService _authService;
public TokenService(IMemoryCache cache, AuthService authService)
{
_cache = cache;
_authService = authService;
}
public async Task<string> GetAccessTokenAsync()
{
// 尝试从缓存获取
if (_cache.TryGetValue("access_token", out string accessToken))
return accessToken;
// 获取新 Token 并设置缓存过期(提前 5 分钟刷新)var tokenResponse = await _authService.GetClientCredentialsTokenAsync();
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromSeconds(tokenResponse.ExpiresIn - 300));
_cache.Set("access_token", tokenResponse.AccessToken, cacheOptions);
return tokenResponse.AccessToken;
}
}
生产环境检查清单
必须配置的 HTTP 安全头部
- Strict-Transport-Security (HSTS)
- Content-Security-Policy
- X-Content-Type-Options: nosniff
- X-Frame-Options: DENY
- Referrer-Policy: strict-origin-when-cross-origin
Token 有效期设置建议
- 访问令牌:短时效(15-60 分钟)
- 刷新令牌:长时效(7-30 天),但需要服务端有吊销机制
- 客户端凭证模式令牌:建议不超过 24 小时
关键监控指标
- 401/403 错误率突增
- Token 端点响应时间 P99
- 不同客户端的 Token 获取频率
开放性问题
当 Identity Server 采用集群部署时,由于各节点间的 Token 签发信息可能存在同步延迟,如何确保资源服务器能立即验证最新签发的 Token?常见的解决方案有:
- 使用集中式的 Token 验证端点
- 引入 Redis 等分布式缓存同步签发信息
- 配置 JWT 的容忍时钟偏移(ClockSkew)
你更倾向于哪种方案?或者有更好的实践?欢迎在评论区分享你的见解。
正文完
