在现代 web 应用程序中,OAuth 协议是授权和认证的主流选择。为了与多个授权提供商进行无缝对接,我们需要一个易于扩展和维护的 OAuth 解决方案。本文将介绍如何构建一个灵活的、支持多提供商的 OAuth 系统,包括动态 API 调用、路径参数替换、查询参数处理等功能。
目录结构
我们将创建一个简单而清晰的目录结构,以便于管理我们的代码和配置。以下是本解决方案的整体目录结构:
核心模块:OAuth.Core
ProviderConfiguration 数据模型
在 OAuth.Core
模块,我们定义了 ProviderConfiguration
类,该类用于配置各个 OAuth 提供商的基本信息和 API 详情。
using System.Collections.Generic;
namespace OAuth.Core.Models
{
/// <summary>
/// 单个 OAuth 提供商的配置
/// </summary>
public class ProviderConfiguration
{
public string ProviderName { get; set; } // 提供商名称
public string BaseUrl { get; set; } // 提供商 API 基础路径
public string AuthorizationUrl { get; set; } // 授权 URL
public string TokenUrl { get; set; } // 获取 AccessToken URL
public string ClientId { get; set; } // 应用的 Client ID
public string ClientSecret { get; set; } // 应用的 Client Secret
public string RedirectUri { get; set; } // 应用回调的重定向 URL
public List<string> Scopes { get; set; } = new(); // 授权范围
public Dictionary<string, string> CommonHeaders { get; set; } = new(); // 公共 Headers
public Dictionary<string, ApiConfig> Apis { get; set; } = new(); // 配置的 API 集合
}
/// <summary>
/// 单个 API 的动态配置
/// </summary>
public class ApiConfig
{
public string Url { get; set; } // 动态路径 URL,例如 /user/{userId}
public string Method { get; set; } = "GET"; // HTTP 请求方法
public bool RequiresAuthentication { get; set; } = true; // 是否需要 AccessToken
public Dictionary<string, string> Headers { get; set; } = new(); // API 专属 Header 配置
public string BodyFormat { get; set; } = "application/json"; // Body 格式,默认 JSON
public List<ApiParameterConfig> Parameters { get; set; } = new(); // 参数配置
}
/// <summary>
/// API 参数配置
/// </summary>
public class ApiParameterConfig
{
public string Name { get; set; } // 参数名称
public string Location { get; set; } // 参数位置(path, query, body, header)
public string DefaultValue { get; set; } // 参数默认值(若需要)
}
}
TokenInfo 管理类
TokenInfo
类用于管理和存储 Access Token 和 Refresh Token 的生命周期信息。
using System;
namespace OAuth.Core.Models
{
public class TokenInfo
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public DateTime ExpiresAt { get; set; }
public bool IsValid => DateTime.UtcNow < ExpiresAt;
}
}
结果封装类 Result
通过 Result<T>
类封装 API 调用的结果,以便于处理成功和失败的状态。
namespace OAuth.Core.Models
{
public class Result<T>
{
public T Data { get; }
public bool IsSuccess { get; }
public string Error { get; }
private Result(T data, bool isSuccess, string error)
{
Data = data;
IsSuccess = isSuccess;
Error = error;
}
public static Result<T> Success(T data) => new(data, true, null);
public static Result<T> Failure(string error) => new(default, false, error);
}
}
核心逻辑实现:OAuth.Infrastructure
AuthProvider 类
这是实施 OAuth 逻辑的核心类,负责发送请求、处理响应,以及替换路径参数。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using OAuth.Core.Models;
namespace OAuth.Infrastructure
{
public class AuthProvider
{
private readonly HttpClient _httpClient;
private readonly ProviderConfiguration _config;
public AuthProvider(HttpClient httpClient, ProviderConfiguration config)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_config = config ?? throw new ArgumentNullException(nameof(config));
}
/// <summary>
/// 获取 AccessToken
/// </summary>
public async Task<Result<TokenInfo>> GetAccessTokenAsync(string code)
{
var url = $"{_config.BaseUrl}{_config.TokenUrl}";
var payload = new Dictionary<string, string>
{
{ "client_id", _config.ClientId },
{ "client_secret", _config.ClientSecret },
{ "code", code },
{ "grant_type", "authorization_code" },
{ "redirect_uri", _config.RedirectUri }
};
var response = await _httpClient.PostAsync(url, new FormUrlEncodedContent(payload));
if (!response.IsSuccessStatusCode)
return Result<TokenInfo>.Failure(await response.Content.ReadAsStringAsync());
var content = await response.Content.ReadAsStringAsync();
var tokenResponse = JsonSerializer.Deserialize<Dictionary<string, object>>(content);
return Result<TokenInfo>.Success(new TokenInfo
{
AccessToken = tokenResponse["access_token"].ToString(),
RefreshToken = tokenResponse.ContainsKey("refresh_token") ? tokenResponse["refresh_token"].ToString() : null,
ExpiresAt = DateTime.UtcNow.AddSeconds(Convert.ToDouble(tokenResponse["expires_in"]))
});
}
/// <summary>
/// 执行 API 调用
/// </summary>
public async Task<Result<T>> CallApiAsync<T>(string apiName, Dictionary<string, object> parameters = null)
{
if (!_config.Apis.TryGetValue(apiName, out var apiConfig))
return Result<T>.Failure($"未找到 API '{apiName}' 的配置");
// 动态替换路径参数
var url = ReplacePathParameters($"{_config.BaseUrl}{apiConfig.Url}", parameters);
// 构建 HTTP 请求
var request = new HttpRequestMessage(new HttpMethod(apiConfig.Method), url);
// 添加 Query 参数
url = AppendQueryParameters(url, apiConfig, parameters);
// 添加 Body (如果是 POST/PUT 请求)
if (apiConfig.Method == "POST" || apiConfig.Method == "PUT")
{
request.Content = CreateBodyContent(apiConfig, parameters);
}
// 设置公共和 API 专属 Header
foreach (var header in _config.CommonHeaders)
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
foreach (var header in apiConfig.Headers)
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
// 添加 Authentication Token
if (apiConfig.RequiresAuthentication)
{
var token = await GetValidAccessTokenAsync();
if (token == null) return Result<T>.Failure("未能获取有效令牌");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);
}
// 执行请求
var response = await _httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
return Result<T>.Failure(await response.Content.ReadAsStringAsync());
var responseContent = await response.Content.ReadAsStringAsync();
return Result<T>.Success(JsonSerializer.Deserialize<T>(responseContent));
}
/// <summary>
/// 替换路径中的动态参数
/// </summary>
private string ReplacePathParameters(string url, Dictionary<string, object> parameters)
{
if (parameters is null) return url;
foreach (var param in parameters)
{
var placeholder = $"{{{param.Key}}}";
if (url.Contains(placeholder))
{
url = url.Replace(placeholder, param.Value.ToString());
}
}
return url;
}
/// <summary>
/// 追加查询参数
/// </summary>
private string AppendQueryParameters(string url, ApiConfig apiConfig, Dictionary<string, object> parameters)
{
var queryParams = new List<string>();
foreach (var paramConfig in apiConfig.Parameters)
{
if (paramConfig.Location == "query" && parameters.ContainsKey(paramConfig.Name))
{
queryParams.Add($"{paramConfig.Name}={parameters[paramConfig.Name]}");
}
}
if (queryParams.Any())
{
var separator = url.Contains("?") ? "&" : "?";
url += separator + string.Join("&", queryParams);
}
return url;
}
/// <summary>
/// 创建请求体数据(Body)
/// </summary>
private HttpContent CreateBodyContent(ApiConfig apiConfig, Dictionary<string, object> parameters)
{
var bodyParams = parameters?
.Where(p => apiConfig.Parameters.Any(pc => pc.Location == "body" && pc.Name == p.Key))
.ToDictionary(kv => kv.Key, kv => kv.Value);
if (apiConfig.BodyFormat == "application/json")
{
return new StringContent(JsonSerializer.Serialize(bodyParams), Encoding.UTF8, "application/json");
}
else if (apiConfig.BodyFormat == "application/x-www-form-urlencoded")
{
return new FormUrlEncodedContent(bodyParams.ToDictionary(k => k.Key, k => k.Value.ToString()));
}
throw new NotSupportedException("不支持的 Body 格式");
}
/// <summary>
/// 模拟获取有效的 AccessToken
/// </summary>
private async Task<TokenInfo> GetValidAccessTokenAsync()
{
// 这里可以实现从缓存或数据库中获取 token 的逻辑
return await Task.FromResult(new TokenInfo
{
AccessToken = "mocked_access_token",
ExpiresAt = DateTime.UtcNow.AddHours(1)
});
}
}
}
AuthProviderFactory 类
AuthProviderFactory
负责创建 AuthProvider
的实例,简化多提供商的管理。
using System;
using System.Collections.Generic;
using System.Net.Http;
using OAuth.Core.Models;
namespace OAuth.Infrastructure
{
public class AuthProviderFactory
{
private readonly IDictionary<string, ProviderConfiguration> _configurations;
private readonly IHttpClientFactory _httpClientFactory;
public AuthProviderFactory(IDictionary<string, ProviderConfiguration> configurations, IHttpClientFactory httpClientFactory)
{
_configurations = configurations ?? throw new ArgumentNullException(nameof(configurations));
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
}
public AuthProvider Create(string providerName)
{
if (!_configurations.TryGetValue(providerName, out var config))
throw new KeyNotFoundException($"未找到提供商:{providerName}");
return new AuthProvider(_httpClientFactory.CreateClient(), config);
}
}
}
Web API 层:OAuth.WebApi
API 控制器
在 OAuth.WebApi
模块中,我们实现了 OAuthController
,提供 API 的行为。
using Microsoft.AspNetCore.Mvc;
using OAuth.Infrastructure;
namespace OAuth.WebApi.Controllers
{
[Route("api/oauth")]
[ApiController]
public class OAuthController : ControllerBase
{
private readonly AuthProviderFactory _factory;
public OAuthController(AuthProviderFactory factory)
{
_factory = factory;
}
[HttpGet("{provider}/{api}")]
public async Task<IActionResult> ExecuteApi(string provider, string api, [FromQuery] Dictionary<string, object> parameters)
{
var providerInstance = _factory.Create(provider);
var result = await providerInstance.CallApiAsync<object>(api, parameters);
if (!result.IsSuccess)
return BadRequest(result.Error);
return Ok(result.Data);
}
}
}
配置文件示例
appsettings.json
文件配置所有可使用的 OAuth 提供商的信息。
{
"OAuthProviders": {
"WeCom": {
"ProviderName": "WeCom",
"BaseUrl": "https://qyapi.weixin.qq.com",
"AuthorizationUrl": "/cgi-bin/authorize",
"TokenUrl": "/cgi-bin/gettoken",
"ClientId": "your-client-id",
"ClientSecret": "your-client-secret",
"RedirectUri": "https://your.app/wecom/callback",
"CommonHeaders": {
"User-Agent": "OAuthSolution Client/1.0"
},
"Apis": {
"GetUserInfo": {
"Url": "/cgi-bin/user/get/{userid}",
"Method": "GET",
"RequiresAuthentication": true,
"Parameters": [
{ "Name": "userid", "Location": "path" }
]
}
}
}
}
}
应用程序启动和配置
最后,在 Program.cs
中,设置 ASP.NET Core 中的依赖注入以支持我们的架构。
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OAuth.Core.Models;
using OAuth.Infrastructure;
var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;
var oauthProviders = configuration.GetSection("OAuthProviders").Get<Dictionary<string, ProviderConfiguration>>();
builder.Services.AddHttpClient();
builder.Services.AddSingleton(oauthProviders);
builder.Services.AddSingleton<AuthProviderFactory>();
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
结论
通过上述方式,我们构建了一个可扩展的 OAuth2 API集成方案。这种设计不仅支持多种 OAuth 提供商的接入,还允许动态配置 API 的调用方式,使得代码简洁明了、易于维护。未来如需引入新的提供商或 API,只需在配置文件中新增相应信息即可,无需对现有逻辑进行修改。
若您遇到不同 OAuth 提供商的实际需求、疑问或问题,请随时在评论区留言,我们将一起探讨解决方案! 🌟