一个通用的居于 OAuth2的API集成方案
在现代 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 提供商的实际需求、疑问或问题,请随时在评论区留言,我们将一起探讨解决方案! 🌟
相关文章:

一个通用的居于 OAuth2的API集成方案
在现代 web 应用程序中,OAuth 协议是授权和认证的主流选择。为了与多个授权提供商进行无缝对接,我们需要一个易于扩展和维护的 OAuth 解决方案。本文将介绍如何构建一个灵活的、支持多提供商的 OAuth 系统,包括动态 API 调用、路径参数替换、…...

STM32配合可编程加密芯片SMEC88ST的防抄板加密方案设计
SMEC88ST SDK开发包下载 目前市场上很多嵌入式产品方案都是可以破解复制的,主要是因为方案主芯片不具备防破解的功能,这就导致开发者投入大量精力、财力开发的新产品一上市就被别人复制,到市场上的只能以价格竞争,最后工厂复制的产…...

QML学习(五) 做出第一个简单的应用程序
通过前面四篇对QML已经有了基本的了解,今天先尝试做出第一个单页面的桌面应用程序。 1.首先打开Qt,创建项目,选择“QtQuick Application - Empty” 空工程。 2.设置项目名称和项目代码存储路径 3.这里要注意选择你的编译器类型,以及输出的程…...
深入解析Android Framework中的android.location包:架构设计、设计模式与系统定制
深入解析Android Framework中的android.location包:架构设计、设计模式与系统定制 目录 引言android.location包概述核心类解析 LocationManagerLocationProviderLocationCriteriaGpsStatusGpsStatus.ListenerLocationListener位置服务的工作原理位置信息的获取与处理GPS状态…...

【C++11】类型分类、引用折叠、完美转发
目录 一、类型分类 二、引用折叠 三、完美转发 一、类型分类 C11以后,进一步对类型进行了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值 (expiring value,简称xvalue)。 纯右值是指那些字面值常量或求值结果相当于…...

mongodb(6.0.15)安装注意事项,重装系统后数据恢复
window10系统 上周重装了系统,环境变量之类的都没有了。现在要恢复。 我电脑里之前的安装包没有删除(虽然之前也没在C盘安装,但是找不到了,所以需要重新下载安装),长下图这样。这个不是最新版本࿰…...
union的实际使用
记录一下,免得忘记: 1、定义一个共用体变量 这里定义一个64位变量 i2creg_rev,然后通过共用体定义两个位变量bits和bits_reverse,通过bit可以访问指定位的值大小,不需要自己再左移右移转换。 bits_reverse是bits的对…...

EKF 自动匹配维度 MATLAB代码
该 M A T L A B MATLAB MATLAB代码实现了扩展卡尔曼滤波( E...
Oracle复合索引规则指南
在Oracle中可以创建组合索引,即同时包含两个或两个以上列的索引。在组合索引的使用方面,Oracle有以下特点: 1、 当使用基于规则的优化器(RBO)时,只有当组合索引的前导列出现在SQL语句的where子句中时&#…...
JS - Array Api
判断一个对象是否为数组 /* 语法: Array.isArray(object); 参数:object 必需,要测试的对象。返回值 如果 object 是数组,则为 true;否则为 false。 如果 object 参数不是对象,则返回 false。 */ 一、改…...
【JS】for-in 和 for-of遍历对象的区别
【介绍】 for-in 和 for-of 都是 JavaScript 中用于遍历数据结构的循环语句,但它们的工作原理和适用场景有所不同。特别是它们在遍历对象时的行为是不同的。 【区别】 for-in 遍历对象 for-in 是用于遍历对象的 可枚举属性的键名(属性名)…...
【每日学点鸿蒙知识】ets匿名类、获取控件坐标、Web显示iframe标签、软键盘导致上移、改变Text的背景色
1、HarmonyOS ets不支持匿名类吗? 不支持,需要显式标注对象字面量的类型,可以参考以下文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/typescript-to-arkts-migration-guide-V5#%E9%9C%80%E8%A6%81%E6%…...

深度学习blog- 数学基础(全是数学)
矩阵:矩阵是一个二维数组,通常由行和列组成,每个元素可以通过行索引和列索引进行访问。 张量:张量是一个多维数组的抽象概念,可以具有任意数量的维度。除了标量(0D张量)、向量(…...
最后100米配送
1. 项目概述 1.1 项目目标 集成无人机与电动车:设计并实现将无人机固定在电动车上,利用电动车的电源进行飞行,实现高楼内部从电动车位置到用户办公/居住地点的最后100米精准配送。低成本实现:通过利用电动车现有的电源和结构&am…...

Linux的进程替换以及基础IO
进程替换 上一篇草率的讲完了进程地址空间的组成结构和之间的关系,那么我们接下来了解一下程序的替换。 首先,在进程部分我们提过了,其实文件可以在运行时变成进程,而我们使用的Linux软件其实也是一个进程,所以进一步…...
《计算机网络A》单选题-复习题库
1. 计算机网络最突出的优点是(D) A、存储容量大B、将计算机技术与通信技术相结合C、集中计算D、资源共享 2. RIP 路由协议的最大跳数是(C) A、13B、14C、15D、16 3. 下面哪一个网络层次不属于 TCP/IP 体系模型(D&a…...

闲谭Scala(2)--安装与环境配置
1. 概述 Java开发环境安装,需要两步,第一安装JDK,第二配置环境变量。 Scala的话,也是两步,第一安装Scale环境,第二配置环境变量。 需要注意的是,配置环境变量,主要是想让windows操…...

Python基于卷积神经网络的车牌识别系统开发与实现
1. 简介 车牌识别是人工智能在交通领域的重要应用,广泛用于高速违章检测、停车场管理和智能交通系统等场景。本系统通过基于卷积神经网络(CNN)的深度学习算法,结合 Python 和 MySQL 实现车牌的快速识别与管理。 系统特点&#x…...
Spring Boot集成Netty创建一个TCP服务器,接收16进制数据(自定义解码器和编码器)
Netty Netty是一个高性能、异步事件驱动的网络应用程序框架,它提供了对并发和异步编程的抽象,使得开发网络应用程序变得更加简单和高效。 在Netty中,EventLoopGroup是处理I/O操作的多线程事件循环器。在上面的示例中,我们创建了两个EventLoopGroup实例:bossGroup和worker…...
Python 中的 with open:文件操作的最佳实践
在 Python 中,文件操作是最常用的一项任务,无论是读取文件内容,还是将数据写入文件。传统的文件操作方式使用 open() 和 close() 函数来处理文件,但在实际开发中,我们推荐使用 with open() 语句来进行文件操作。本文将…...
Linux简单的操作
ls ls 查看当前目录 ll 查看详细内容 ls -a 查看所有的内容 ls --help 查看方法文档 pwd pwd 查看当前路径 cd cd 转路径 cd .. 转上一级路径 cd 名 转换路径 …...

华为OD机试-食堂供餐-二分法
import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...

SpringBoot+uniapp 的 Champion 俱乐部微信小程序设计与实现,论文初版实现
摘要 本论文旨在设计并实现基于 SpringBoot 和 uniapp 的 Champion 俱乐部微信小程序,以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务,提供稳定高效的数据处理与业务逻辑支持;利用 uniapp 实现跨平台前…...
是否存在路径(FIFOBB算法)
题目描述 一个具有 n 个顶点e条边的无向图,该图顶点的编号依次为0到n-1且不存在顶点与自身相连的边。请使用FIFOBB算法编写程序,确定是否存在从顶点 source到顶点 destination的路径。 输入 第一行两个整数,分别表示n 和 e 的值(1…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
服务器--宝塔命令
一、宝塔面板安装命令 ⚠️ 必须使用 root 用户 或 sudo 权限执行! sudo su - 1. CentOS 系统: yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh2. Ubuntu / Debian 系统…...
管理学院权限管理系统开发总结
文章目录 🎓 管理学院权限管理系统开发总结 - 现代化Web应用实践之路📝 项目概述🏗️ 技术架构设计后端技术栈前端技术栈 💡 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 🗄️ 数据库设…...

并发编程 - go版
1.并发编程基础概念 进程和线程 A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中…...

MySQL:分区的基本使用
目录 一、什么是分区二、有什么作用三、分类四、创建分区五、删除分区 一、什么是分区 MySQL 分区(Partitioning)是一种将单张表的数据逻辑上拆分成多个物理部分的技术。这些物理部分(分区)可以独立存储、管理和优化,…...

相关类相关的可视化图像总结
目录 一、散点图 二、气泡图 三、相关图 四、热力图 五、二维密度图 六、多模态二维密度图 七、雷达图 八、桑基图 九、总结 一、散点图 特点 通过点的位置展示两个连续变量之间的关系,可直观判断线性相关、非线性相关或无相关关系,点的分布密…...