1、本文主要介绍.Net 5.0通过认证授权、路由终结点、OpenIdConnect组件结合IdentityServer4实现单点登录的源码解析,内容较多,只解读demo的调用部分.
首页看调用代码:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "http://localhost:5001";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.SaveTokens = true;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute()
.RequireAuthorization();
});
}
}
首先看ConfigureServices方法,通过该方法注入将控制器注入到了容器中,并配置认证组件的默认认证方案为Cookie,Challenge方案设置为oidc,其实就是当用户未认证时,回调用oidc方法的Handler,下面会解释,接着看Configure方法,启用路由、认证、授权、终结点组件,并在终结点里面配置了给默认控制器路由全都设置了Authorize特性.相当于所有的控制器方法,必要要登录过后才能访问.下面会进行源码解析.
2、授权中间件源码解析
再通过上述代码配置好客户端之后,说明客户端已经具备接入oidc服务端了(本文不多做讲解),那么现在访问客户端api,必然会被拦截,应为在配置客户端时,引入了授权组件,并且给所有的控制器方法加上了Authorize特性.相当于所有的控制器方法,必要要登录过后才能访问.ok,带着这个前提条件,来看看授权中间件(如果不了解授权中间件请参阅.Net Core 3.0授权组件源码解析)做了什么.
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var endpoint = context.GetEndpoint();
if (endpoint != null)
{
}
// IMPORTANT: Changes to authorization logic should be mirrored in MVC's AuthorizeFilter
var authorizeData = endpoint?.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();
首先先在当前访问的终结点获取到其Metadata中关于授权的信息细节如下:
public interface IAuthorizeData
{
/// <summary>
/// 终结点(可以理解为控制器方法)配置的认证策略
/// </summary>
string? Policy { get; set; }
/// <summary>
/// 终结点(可以理解为控制器方法)配置的角色(大多数系统是基于Role的授权策略)
/// </summary>
string? Roles { get; set; }
/// <summary>
/// 终结点(可以理解为控制器方法)配置的认证方式
/// </summary>
string? AuthenticationSchemes { get; set; }
}
细节就是获取控制器方法上的实现IAuthorizeData(默认Authorize特性)的特性内容,包括方法采用的认证方案、采用的自定义授权策略、采用的角色授权策略信息.获取当上述信息后,接下看源码如下:
var policy = await AuthorizationPolicy.CombineAsync(_policyProvider, authorizeData);
if (policy == null)
{
await _next(context);
return;
}
判断一下控制器方法上的Authorize特性中的授权策略相关的内容是否为空,为空的话,直接执行接下去的中间件.这里查阅下授权策略是如何Combine的,代码如下:
public static async Task<AuthorizationPolicy?> CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData)
{
if (policyProvider == null)
{
throw new ArgumentNullException(nameof(policyProvider));
}
if (authorizeData == null)
{
throw new ArgumentNullException(nameof(authorizeData));
}
// Avoid allocating enumerator if the data is known to be empty
var skipEnumeratingData = false;
if (authorizeData is IList<IAuthorizeData> dataList)
{
skipEnumeratingData = dataList.Count == 0;
}
AuthorizationPolicyBuilder? policyBuilder = null;
if (!skipEnumeratingData)
{
foreach (var authorizeDatum in authorizeData)
{
if (policyBuilder == null)
{
policyBuilder = new AuthorizationPolicyBuilder();
}
var useDefaultPolicy = true;
if (!string.IsNullOrWhiteSpace(authorizeDatum.Policy))
{
var policy = await policyProvider.GetPolicyAsync(authorizeDatum.Policy);
if (policy == null)
{
throw new InvalidOperationException("");
}
policyBuilder.Combine(policy);
useDefaultPolicy = false;
}
var rolesSplit = authorizeDatum.Roles?.Split(',');
if (rolesSplit?.Length > 0)
{
var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim());
policyBuilder.RequireRole(trimmedRolesSplit);
useDefaultPolicy = false;
}
var authTypesSplit = authorizeDatum.AuthenticationSchemes?.Split(',');
if (authTypesSplit?.Length > 0)
{
foreach (var authType in authTypesSplit)
{
if (!string.IsNullOrWhiteSpace(authType))
{
policyBuilder.AuthenticationSchemes.Add(authType.Trim());
}
}
}
if (useDefaultPolicy)
{
policyBuilder.Combine(await policyProvider.GetDefaultPolicyAsync());
}
}
}
// If we have no policy by now, use the fallback policy if we have one
if (policyBuilder == null)
{
var fallbackPolicy = await policyProvider.GetFallbackPolicyAsync();
if (fallbackPolicy != null)
{
return fallbackPolicy;
}
}
return policyBuilder?.Build();
}
}