当前位置: 首页 > news >正文

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

STM32配合可编程加密芯片SMEC88ST的防抄板加密方案设计

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

QML学习(五) 做出第一个简单的应用程序

通过前面四篇对QML已经有了基本的了解&#xff0c;今天先尝试做出第一个单页面的桌面应用程序。 1.首先打开Qt,创建项目&#xff0c;选择“QtQuick Application - Empty” 空工程。 2.设置项目名称和项目代码存储路径 3.这里要注意选择你的编译器类型&#xff0c;以及输出的程…...

深入解析Android Framework中的android.location包:架构设计、设计模式与系统定制

深入解析Android Framework中的android.location包:架构设计、设计模式与系统定制 目录 引言android.location包概述核心类解析 LocationManagerLocationProviderLocationCriteriaGpsStatusGpsStatus.ListenerLocationListener位置服务的工作原理位置信息的获取与处理GPS状态…...

【C++11】类型分类、引用折叠、完美转发

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

mongodb(6.0.15)安装注意事项,重装系统后数据恢复

window10系统 上周重装了系统&#xff0c;环境变量之类的都没有了。现在要恢复。 我电脑里之前的安装包没有删除&#xff08;虽然之前也没在C盘安装&#xff0c;但是找不到了&#xff0c;所以需要重新下载安装&#xff09;&#xff0c;长下图这样。这个不是最新版本&#xff0…...

union的实际使用

记录一下&#xff0c;免得忘记&#xff1a; 1、定义一个共用体变量 这里定义一个64位变量 i2creg_rev&#xff0c;然后通过共用体定义两个位变量bits和bits_reverse&#xff0c;通过bit可以访问指定位的值大小&#xff0c;不需要自己再左移右移转换。 bits_reverse是bits的对…...

EKF 自动匹配维度 MATLAB代码

该 M A T L A B MATLAB MATLAB代码实现了扩展卡尔曼滤波( E...

Oracle复合索引规则指南

在Oracle中可以创建组合索引&#xff0c;即同时包含两个或两个以上列的索引。在组合索引的使用方面&#xff0c;Oracle有以下特点&#xff1a; 1、 当使用基于规则的优化器&#xff08;RBO&#xff09;时&#xff0c;只有当组合索引的前导列出现在SQL语句的where子句中时&#…...

JS - Array Api

判断一个对象是否为数组 /* 语法&#xff1a; Array.isArray(object); 参数&#xff1a;object 必需&#xff0c;要测试的对象。返回值 如果 object 是数组&#xff0c;则为 true&#xff1b;否则为 false。 如果 object 参数不是对象&#xff0c;则返回 false。 */ 一、改…...

【JS】for-in 和 for-of遍历对象的区别

【介绍】 for-in 和 for-of 都是 JavaScript 中用于遍历数据结构的循环语句&#xff0c;但它们的工作原理和适用场景有所不同。特别是它们在遍历对象时的行为是不同的。 【区别】 for-in 遍历对象 for-in 是用于遍历对象的 可枚举属性的键名&#xff08;属性名&#xff09;…...

【每日学点鸿蒙知识】ets匿名类、获取控件坐标、Web显示iframe标签、软键盘导致上移、改变Text的背景色

1、HarmonyOS ets不支持匿名类吗&#xff1f; 不支持&#xff0c;需要显式标注对象字面量的类型&#xff0c;可以参考以下文档&#xff1a;https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/typescript-to-arkts-migration-guide-V5#%E9%9C%80%E8%A6%81%E6%…...

深度学习blog- 数学基础(全是数学)

矩阵‌&#xff1a;矩阵是一个二维数组&#xff0c;通常由行和列组成&#xff0c;每个元素可以通过行索引和列索引进行访问。 张量‌&#xff1a;张量是一个多维数组的抽象概念&#xff0c;可以具有任意数量的维度。除了标量&#xff08;0D张量&#xff09;、向量&#xff08;…...

最后100米配送

1. 项目概述 1.1 项目目标 集成无人机与电动车&#xff1a;设计并实现将无人机固定在电动车上&#xff0c;利用电动车的电源进行飞行&#xff0c;实现高楼内部从电动车位置到用户办公/居住地点的最后100米精准配送。低成本实现&#xff1a;通过利用电动车现有的电源和结构&am…...

Linux的进程替换以及基础IO

进程替换 上一篇草率的讲完了进程地址空间的组成结构和之间的关系&#xff0c;那么我们接下来了解一下程序的替换。 首先&#xff0c;在进程部分我们提过了&#xff0c;其实文件可以在运行时变成进程&#xff0c;而我们使用的Linux软件其实也是一个进程&#xff0c;所以进一步…...

《计算机网络A》单选题-复习题库

1. 计算机网络最突出的优点是&#xff08;D&#xff09; A、存储容量大B、将计算机技术与通信技术相结合C、集中计算D、资源共享 2. RIP 路由协议的最大跳数是&#xff08;C&#xff09; A、13B、14C、15D、16 3. 下面哪一个网络层次不属于 TCP/IP 体系模型&#xff08;D&a…...

闲谭Scala(2)--安装与环境配置

1. 概述 Java开发环境安装&#xff0c;需要两步&#xff0c;第一安装JDK&#xff0c;第二配置环境变量。 Scala的话&#xff0c;也是两步&#xff0c;第一安装Scale环境&#xff0c;第二配置环境变量。 需要注意的是&#xff0c;配置环境变量&#xff0c;主要是想让windows操…...

Python基于卷积神经网络的车牌识别系统开发与实现

1. 简介 车牌识别是人工智能在交通领域的重要应用&#xff0c;广泛用于高速违章检测、停车场管理和智能交通系统等场景。本系统通过基于卷积神经网络&#xff08;CNN&#xff09;的深度学习算法&#xff0c;结合 Python 和 MySQL 实现车牌的快速识别与管理。 系统特点&#x…...

Spring Boot集成Netty创建一个TCP服务器,接收16进制数据(自定义解码器和编码器)

Netty Netty是一个高性能、异步事件驱动的网络应用程序框架,它提供了对并发和异步编程的抽象,使得开发网络应用程序变得更加简单和高效。 在Netty中,EventLoopGroup是处理I/O操作的多线程事件循环器。在上面的示例中,我们创建了两个EventLoopGroup实例:bossGroup和worker…...

Python 中的 with open:文件操作的最佳实践

在 Python 中&#xff0c;文件操作是最常用的一项任务&#xff0c;无论是读取文件内容&#xff0c;还是将数据写入文件。传统的文件操作方式使用 open() 和 close() 函数来处理文件&#xff0c;但在实际开发中&#xff0c;我们推荐使用 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 俱乐部微信小程序&#xff0c;以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务&#xff0c;提供稳定高效的数据处理与业务逻辑支持&#xff1b;利用 uniapp 实现跨平台前…...

是否存在路径(FIFOBB算法)

题目描述 一个具有 n 个顶点e条边的无向图&#xff0c;该图顶点的编号依次为0到n-1且不存在顶点与自身相连的边。请使用FIFOBB算法编写程序&#xff0c;确定是否存在从顶点 source到顶点 destination的路径。 输入 第一行两个整数&#xff0c;分别表示n 和 e 的值&#xff08;1…...

Device Mapper 机制

Device Mapper 机制详解 Device Mapper&#xff08;简称 DM&#xff09;是 Linux 内核中的一套通用块设备映射框架&#xff0c;为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程&#xff0c;并配以详细的…...

服务器--宝塔命令

一、宝塔面板安装命令 ⚠️ 必须使用 root 用户 或 sudo 权限执行&#xff01; sudo su - 1. CentOS 系统&#xff1a; yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh2. Ubuntu / Debian 系统…...

管理学院权限管理系统开发总结

文章目录 &#x1f393; 管理学院权限管理系统开发总结 - 现代化Web应用实践之路&#x1f4dd; 项目概述&#x1f3d7;️ 技术架构设计后端技术栈前端技术栈 &#x1f4a1; 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 &#x1f5c4;️ 数据库设…...

并发编程 - go版

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

MySQL:分区的基本使用

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

相关类相关的可视化图像总结

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