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

ABP vNext + OpenIddict:自定义 OAuth2/OpenID Connect 认证策略

ABP vNext + OpenIddict:自定义 OAuth2/OpenID Connect 认证策略 🚀


📑 目录

  • ABP vNext + OpenIddict:自定义 OAuth2/OpenID Connect 认证策略 🚀
    • 🧠 背景与核心设计思路
    • 🛠 依赖注入与启动配置
    • 🔑 系统配置:注册 Token 授权管道
    • 🔧 自定义授权处理器:ApiKeyGrantHandler
    • 🏢 租户解析与多租户 SSO
      • Contributor 实现
      • 注入配置
      • 上下文切换
        • 🌀 多租户解析流程
    • 📋 接口定义:IApiKeyValidator
    • 📈 Scope & Client 动态管理
        • 🔄 Client 管理流程
    • 🧪 接口调用示例
      • 1. API Key 授权成功
      • 2. 刷新令牌示例
        • 🔄 刷新流程图
    • 🔐 安全加固建议
    • 📁 项目结构推荐


🧠 背景与核心设计思路

大型 SaaS 系统常见需求:

  1. 自定义身份来源(API Key、Device Flow)
  2. 多租户隔离与 SSO
  3. 精细化 Scope/资源管理

ABP 的 OpenIddict 模块提供 Handler 模型 插槽,轻松插入自定义授权逻辑。


🛠 依赖注入与启动配置

public class AuthServerModule : AbpModule
{public override void ConfigureServices(ServiceConfigurationContext context){var services = context.Services;// ➤ 注册 OpenIddict 核心 + Server + Validationservices.AddOpenIddict().AddCore(options => { /* 实体存储等 */ }).AddServer(options => { /* 稍后配置 */ }).AddValidation(options => { /* 稍后配置 */ });// ➤ 多租户解析services.Configure<AbpTenantResolveOptions>(opts =>{opts.Resolvers.Insert(0, new HeaderTenantResolveContributor());opts.Resolvers.Insert(1, new DomainTenantResolveContributor());});}
}

🔑 系统配置:注册 Token 授权管道

PreConfigure<OpenIddictBuilder>(builder =>
{builder.AddServer(options =>{// —— 端点 ——  options.SetTokenEndpointUris("/connect/token").SetAuthorizationEndpointUris("/connect/authorize").SetDeviceEndpointUris("/connect/device");// —— Grant & Scope ——  options.RegisterGrantType("api_key_grant").AllowPasswordFlow().AllowClientCredentialsFlow().AllowRefreshTokenFlow().AllowExtensionGrantType("api_key_grant").SetDefaultScopes("api", "profile");// —— 有效期 ——  options.SetAccessTokenLifetime(TimeSpan.FromHours(2)).SetRefreshTokenLifetime(TimeSpan.FromDays(7));// —— ASP.NET Core 集成 ——  options.UseAspNetCore().EnableTokenEndpointPassthrough();// —— 自定义 Handler ——  options.AddEventHandler<HandleTokenRequestContext>(cfg =>cfg.UseScopedHandler<ApiKeyGrantHandler>().SetOrder(OpenIddictServerHandlers.Authentication.ValidateTokenRequest.Descriptor.Order + 1).SetFilter(ctx => ctx.Request.GrantType == "api_key_grant"));});builder.AddValidation(options =>{options.UseLocalServer();options.UseAspNetCore();});
});

🔧 自定义授权处理器:ApiKeyGrantHandler

public class ApiKeyGrantHandler : IOpenIddictServerHandler<HandleTokenRequestContext>
{private readonly IApiKeyValidator _apiKeyValidator;private readonly ICurrentTenant   _currentTenant;private readonly ILogger<ApiKeyGrantHandler> _logger;public ApiKeyGrantHandler(IApiKeyValidator apiKeyValidator,ICurrentTenant   currentTenant,ILogger<ApiKeyGrantHandler> logger){_apiKeyValidator = apiKeyValidator;_currentTenant   = currentTenant;_logger          = logger;}public async ValueTask HandleAsync(HandleTokenRequestContext context){using var scope = _logger.BeginScope(new { GrantType = "api_key" });try{var apiKey = context.Request.GetParameter("api_key")?.ToString();if (string.IsNullOrWhiteSpace(apiKey)){context.Reject(Errors.InvalidRequest, "Missing API Key");return;}var userId = await _apiKeyValidator.ValidateAsync(apiKey);if (string.IsNullOrEmpty(userId)){context.Reject(Errors.InvalidGrant, "Invalid API Key");return;}var tenantId = _currentTenant.Id?.ToString() ?? "default";var claims = new[]{new Claim(Claims.Subject,  userId),new Claim("tenant_id",    tenantId),new Claim(Claims.Name,     "API Key User")};var identity  = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);var principal = new ClaimsPrincipal(identity);// 设置 Scopes & Resource  principal.SetScopes(context.Request.GetScopes());principal.SetResources("api");// 顶层返回 tenant_id  context.AddParameter("tenant_id", tenantId);context.Validate(principal);context.HandleRequest();}catch (Exception ex){_logger.LogError(ex, "API Key grant failed.");context.Reject(Errors.ServerError, "Internal error.");}}
}

🏢 租户解析与多租户 SSO

Contributor 实现

public class DomainTenantResolveContributor : HttpTenantResolveContributorBase
{public override Task<string> ResolveAsync(HttpContext context){var sub = context.Request.Host.Host.Split('.').FirstOrDefault();return Task.FromResult(string.IsNullOrWhiteSpace(sub) ? "default" : sub);}
}public class HeaderTenantResolveContributor : HttpTenantResolveContributorBase
{public override Task<string> ResolveAsync(HttpContext context){var header = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();return Task.FromResult(string.IsNullOrWhiteSpace(header) ? "default" : header);}
}

注入配置

services.Configure<AbpTenantResolveOptions>(opts =>
{opts.Resolvers.Insert(0, new HeaderTenantResolveContributor());opts.Resolvers.Insert(1, new DomainTenantResolveContributor());
});

上下文切换

using (_currentTenant.Change(tenantId))
{// 此作用域内,TenantId 生效  
}
🌀 多租户解析流程
Yes
No
Yes
No
Incoming HTTP Request
Has X-Tenant-Id Header?
Use HeaderTenantResolveContributor
Host Subdomain Exists?
Use DomainTenantResolveContributor
Fallback to default
Set CurrentTenant

📋 接口定义:IApiKeyValidator

public interface IApiKeyValidator
{/// <summary>/// 校验 API Key 并返回对应用户ID;失败返回 null/empty/// 实现可结合 IMemoryCache/IDistributedCache 缓存/// </summary>Task<string> ValidateAsync(string apiKey);
}

📈 Scope & Client 动态管理

[Authorize(Roles = "Admin")]
public class ScopeAppService : ApplicationService
{private readonly IOpenIddictScopeManager _scopeManager;public async Task<List<string>> GetScopesAsync(){var list = new List<string>();await foreach (var s in _scopeManager.ListAsync())list.Add(s.Name);return list;}
}[Authorize(Roles = "Admin")]
public class ClientAppService : ApplicationService
{private readonly IOpenIddictApplicationManager _appManager;public async Task CreateMobileClientAsync(){var desc = new OpenIddictApplicationDescriptor{ClientId    = "mobile_app",DisplayName = "Mobile App",Permissions ={Permissions.Endpoints.Token,Permissions.GrantTypes.Password}};await _appManager.CreateAsync(desc);}public async Task DeleteClientAsync(string clientId){var app = await _appManager.FindByClientIdAsync(clientId);if (app != null) await _appManager.DeleteAsync(app);}
}
🔄 Client 管理流程
Admin API Manager POST /api/clients (Create) CreateAsync(descriptor) App Created 201 Created DELETE /api/clients/{id} DeleteAsync(app) App Deleted 204 No Content Admin API Manager

🧪 接口调用示例

1. API Key 授权成功

POST /connect/token
Content-Type: application/x-www-form-urlencodedgrant_type=api_key_grant
api_key=valid-api-key
client_id=default
client_secret=secret
scope=api

成功响应:

{"access_token":"eyJhbGciOiJSUzI1NiIs...","token_type":"Bearer","expires_in":7200,"refresh_token":"eyJhbGciOiJIUzI1NiIs...","tenant_id":"tenant1"
}

错误示例:

{"error":"invalid_grant","error_description":"Invalid API Key"
}

2. 刷新令牌示例

POST /connect/token
grant_type=refresh_token
refresh_token={your_refresh_token}
client_id=default
client_secret=secret

刷新成功:

{"access_token":"…","token_type":"Bearer","expires_in":7200,"refresh_token":"…"
}

刷新失败:

{"error":"invalid_grant","error_description":"Refresh token is expired."
}
🔄 刷新流程图
Valid
Invalid
Client sends refresh_token
Server Validate Refresh Token
Issue new access_token & refresh_token
Return invalid_grant error

🔐 安全加固建议

🔒 类型🛡️ 实践建议
API Key哈希存储 + 过期 + 重放防护
限流使用 AspNetCoreRateLimit 保护 /connect/token
审计日志所有 Reject() 写入审计表,方便追踪
签名密钥DataProtection/RSA 证书 + 定期轮换
监控指标OpenTelemetry Meter/Counter 统计授权成功/失败

📁 项目结构推荐

AuthServer.Host
├── CustomGrants/
│   └── ApiKeyGrantHandler.cs
├── Tenanting/
│   ├── DomainTenantResolveContributor.cs
│   └── HeaderTenantResolveContributor.cs
├── Scopes/
│   └── ScopeAppService.cs
├── Clients/
│   └── ClientAppService.cs
├── Validation/
│   └── IApiKeyValidator.cs
├── OpenIddict/
│   └── PreConfiguration.cs
└── Program.cs

http://www.xdnf.cn/news/14002.html

相关文章:

  • 搭建前端项目 Vue+element UI引入 步骤 (超详细)
  • 2025年第二届仿真与电子技术国际学术会议(ICSET 2025)
  • 用 PlatformIO + ESP-IDF 框架开发 ESP32
  • OB Cloud × 海牙湾:打造高效灵活的金融科技 AI 数字化解决方案
  • 安科瑞中小工商业储能监测-能量管理系统Acrel-2000ES
  • SAP 生产订单报工检查报错异常(接口)
  • 智能客服不再冰冷,声网AI为品牌构建情绪接口
  • 仪表刻度动态显示控件--小三角指针
  • 语言模型进化论:从“健忘侦探”到“超级大脑”的破案之旅
  • MQTT协议
  • 使用Word2Vec实现中文文本分类
  • [深度学习]卷积神经网络
  • 【王阳明代数集合论基础】情感分析之句子的基本结构
  • MiniMind(1)Tokenizer与训练数据
  • 从Pura 80系列影像和鸿蒙AI融合看华为创新的“不可复制性”
  • [Rviz2报错,已解决!]导入urdf模型错误:Could not load mesh resource 。。。
  • 【Vue PDF】Vue PDF 组件初始不加载 pdfUrl 问题分析与修复
  • 【图像恢复算法】 ESRGAN Real-ESRGAN的配置和应用
  • SSE详解
  • 前端跨域解决方案(1):什么是跨域?
  • 【Bluedroid】蓝牙启动之 GAP_Init 流程源码解析
  • 国际数字影像产业园:数字技术赋能 引领产业升级变革
  • 自动化基础随心记三-zabbix
  • AI 重构代码实战:如何用飞算 JavaAI 快速升级遗留系统?
  • python编程基础
  • aflplusplus:开源的模糊测试工具!全参数详细教程!Kali Linux教程!(二)
  • 智能客服系统开发方案:RAG+多智能体技术实现
  • 机器学习 vs 深度学习:区别与应用场景全解析
  • OpenSIPS3.4 load balancer fetch_freeswitch_stats 测试
  • 计算机是怎么跑起来的第四章