用C#实现基于(OpenId Connect)的单点登录与RBAC权限验证(SSO,OIDC,RBAC)
一、项目需求
由于微服务技术的发展,需要对老的项目进行升级改造。其中一大难点就是老项目中使用了RBAC的权限系统,
面向微服务,首先要完成界面和认证后台的分离。
于是,对面向微服务的 RBAC 系统提出了如下需求:
A、认证系统需要遵守当前流行的 Auth2.0 协议,从而支持到单点登录SSO;
B、客户端使用纯 js 开发,做到前后台分离;
C、实现 RBAC 权限控制。
针对需求,查了当前C#开源,对 RBAC 与 OpenId Connect 的结合比较有限。对是自己动手丰衣足食,提出了如下解决方案:
二、解决方案
1、总体解决方案及参考项目
针对需求的每一项提出解决方案
A、后台采用 OpenId Connect 包作 Auth2.0 协议的支持。github上源码: openiddict-core;
B、客户端使用 D2Admin + oidc-client.js 和后台配合完成 SSO 和 Auth2.0 的交互认证;
C、由于 Auth2.0 并不支持 RBAC 权限认证,自己开发了一个 Rbac.Auth.Introspection 工程,用来完成资源程序中的RBAC认证,
并且完全不破坏 Auth2.0 协议。本质上是对 Auth2.0 的扩展。github上的原始参考项目:AspNet.Security.OAuth.Extensions
开发好的库可以直接用 Nuget 获得:https://www.nuget.org/packages/Rbac.Auth.Introspection/
2、Auth2.0 与 RBAC 整合的基本原理
默认的认证中只要 Access Token 合法即通过,改为 Access Token 和 Url 同时合法才能通过即可。
Url 作为基础的权限的参数,权限分配到角色,人员和再分配不同角色。
后台认证时,一个 Access Token 和 Url 进来,通过 Access Token 可查到人员,从而查到该人员是否有对应权限,如果有对应的 Url
则返回认证成功,否则失败。
可以看出:是在标准的 Auth2.0 的 Acess Token 认证交互中扩展了 Url 参数参与认证,从而实现了 RBAC 的认证。
// Note: always specify the token_type_hint to help // the authorization server make a faster token lookup. var parameters = new Dictionary<string, string> { [RbacAuthIntrospectionConstants.Parameters.Token] = token, [RbacAuthIntrospectionConstants.Parameters.Path] = path, [RbacAuthIntrospectionConstants.Parameters.TokenTypeHint] = RbacAuthIntrospectionConstants.TokenTypeHints.AccessToken }; ... request.Content = new FormUrlEncodedContent(parameters); var notification = new SendIntrospectionRequestContext(Context, Scheme, Options, request, path, token); await Events.SendIntrospectionRequest(notification);
3、对于资源端权限定义的扩展
每个微服务都有自己提供服务的 Url 接口,这对于 RBAC 系统的配置提出了巨大的工作量,还需小心谨慎。有没有自动方化的方法,
减少配置的工作量和出错的可能性。为此在 Rbac.Auth.Introspection 中扩展了权限定义和权限列表生成接口。
3.1 资源端的初始化
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(options => { options.DefaultScheme = RbacAuthIntrospectionDefaults.AuthenticationScheme; }) .AddRbacAuthIntrospection(options => { options.Authority = new Uri("http://localhost:12345/"); options.Audiences.Add("resource-server-2"); options.ClientId = "resource-server-2"; options.ClientSecret = "C744604A-CD05-4092-9CF8-ECB7DC3499A2"; options.RequireHttpsMetadata = false; }); services.AddMvc(options => { options.EnableEndpointRouting = false; }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseCors(builder => { builder.WithOrigins(new string[] { "http://localhost:12345", "http://localhost:8080" }); builder.WithMethods("GET"); builder.WithHeaders("Authorization"); }); app.UseStaticFiles(); app.UseAuthentication(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
通过指定 RbacAuthIntrospectionDefaults.AuthenticationScheme 来绑定 Rbac 的权限认证;
通过 AddRbacAuthIntrospection 设置服务器相关的参数;
通过 app.UseAuthentication 激活权限认证。
3.2 扩展了权限定义。
目的:方便自动化生成权限列表,并且和 Asp.Net Core 的 Identity 权限系统完美整合。
[AuthorizeMenu("资源示例", AuthenticationSchemes = RbacAuthIntrospectionDefaults.AuthenticationScheme)] public class ResourceController : Controller { [HttpGet] [PermissionFilter(PermissionType.Menu, "私有功能", Page = "/home/test")] public IActionResult Private() { var identity = User.Identity as ClaimsIdentity; if (identity == null) { return BadRequest(); } return Content($"You have authorized access to resources belonging to {identity.Name} on ResourceServer01."); } [HttpGet] [AllowAnonymous] public IActionResult Public() { return Content("This is a public endpoint that is at ResourceServer01; it does not require authorization."); } }
其中的 AuthorizeMenu 和 PermissionFilter 便是自定义的扩展,可以直接生成对应的权限。减少工作量和出错机会。
每次修正代码后也方便自动修正后台服务器中对应的权限,保持系统与代码的一至性。
3.3 如何自动化生成权限列表
对于资源端,通过 ApplicationController 的 GetPermissionList 接口来获得权限列表。(appId 是用于标识本资源的唯一Id)
public class ApplicationController : Controller { [HttpGet] public IActionResult GetPermissionList(string appId) { var resList = new List<PermissionDTO>(); var controllerList = GetTypes().Where(t => t.IsSubclassOf(typeof(Controller))).ToList(); if (controllerList.Count > 0) { resList = ControllerTools.CreatePermissionDTOList(appId, controllerList); } var res = new JsonResult(new { code = 0, msg = "", data = JsonConvert.SerializeObject(resList) }); return res; } private IEnumerable<Type> GetTypes() { foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) foreach (var t in GetTypes(asm)) yield return t; } private IEnumerable<Type> GetTypes(System.Reflection.Assembly asm) { Type[] ts; try { ts = asm.GetTypes(); } catch { yield break; } foreach (var t in ts) yield return t; } }
三、参考
OIDC(OpenId Connect)身份认证(核心部分)
OAuth2.0 & OpenID Connect解析
OpenIddict 登录及详细流程解析
HANDLING ACCESS TOKENS FOR PRIVATE APIS IN ASP.NET CORE
RBAC | 使用authing实现基于角色的访问控制