重新整理 .net core 实践篇——— 权限源码阅读四十五]

前言

简单介绍一下权限源码阅读一下。

正文

一直有人对授权这个事情上争论不休,有的人认为在输入账户密码给后台这个时候进行了授权,因为认为发送了一个身份令牌,令牌里面可能有些用户角色信息,认为这就是授权,有的人认为这只是获取令牌的过程。

现实生活中有一个是授权证书,那么有人认为token 是授权证书,但这只是颁发证书。账户密码获取获取身份令牌也不是认证,认证是证明你的身份令牌有效的过程。

那么netcore 中是如何解释授权的:

授权是指确定用户可执行的操作的过程。故而实际上,获取身份令牌只是获取令牌,授权是指在访问过程中,确认是否可以访问的过程。身份令牌中有角色,有些是根据角色还确定是否可以访问的,这就是授权了。

不过随着业务的复杂,网关可以根据角色授权接口,也可以根据自己的策略了,授权的过程五花八门的。

在网关中一般有认证和授权两部分,先认证再授权,先确定合法身份在来确定一下授权。

先来看下认证吧,有些人一认证就想到了jwt,或者想到了具体的认证方式,其实认证就是你的系统认为它符合了合法身份,和具体的东西没有关系,是一个抽象的概念。

app.UseAuthentication();

通过上面这个看下认证过程。

然后看下具体的中间件。

public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
{
	if (next == null)
	{
		throw new ArgumentNullException(nameof(next));
	}
	if (schemes == null)
	{
		throw new ArgumentNullException(nameof(schemes));
	}

	_next = next;
	Schemes = schemes;
}

从这里看呢,IAuthenticationSchemeProvider 提供了认证解决方案,可以看下这个接口。

/// <summary>
/// Responsible for managing what authenticationSchemes are supported.
/// </summary>
public interface IAuthenticationSchemeProvider
{
	/// <summary>
	/// Returns all currently registered <see cref="AuthenticationScheme"/>s.
	/// </summary>
	/// <returns>All currently registered <see cref="AuthenticationScheme"/>s.</returns>
	Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync();

	/// <summary>
	/// Returns the <see cref="AuthenticationScheme"/> matching the name, or null.
	/// </summary>
	/// <param name="name">The name of the authenticationScheme.</param>
	/// <returns>The scheme or null if not found.</returns>
	Task<AuthenticationScheme?> GetSchemeAsync(string name);

	/// <summary>
	/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.AuthenticateAsync(HttpContext, string)"/>.
	/// This is typically specified via <see cref="AuthenticationOptions.DefaultAuthenticateScheme"/>.
	/// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>.
	/// </summary>
	/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.AuthenticateAsync(HttpContext, string)"/>.</returns>
	Task<AuthenticationScheme?> GetDefaultAuthenticateSchemeAsync();

	/// <summary>
	/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>.
	/// This is typically specified via <see cref="AuthenticationOptions.DefaultChallengeScheme"/>.
	/// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>.
	/// </summary>
	/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>.</returns>
	Task<AuthenticationScheme?> GetDefaultChallengeSchemeAsync();

	/// <summary>
	/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.ForbidAsync(HttpContext, string, AuthenticationProperties)"/>.
	/// This is typically specified via <see cref="AuthenticationOptions.DefaultForbidScheme"/>.
	/// Otherwise, this will fallback to <see cref="GetDefaultChallengeSchemeAsync"/> .
	/// </summary>
	/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.ForbidAsync(HttpContext, string, AuthenticationProperties)"/>.</returns>
	Task<AuthenticationScheme?> GetDefaultForbidSchemeAsync();

	/// <summary>
	/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.SignInAsync(HttpContext, string, System.Security.Claims.ClaimsPrincipal, AuthenticationProperties)"/>.
	/// This is typically specified via <see cref="AuthenticationOptions.DefaultSignInScheme"/>.
	/// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>.
	/// </summary>
	/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.SignInAsync(HttpContext, string, System.Security.Claims.ClaimsPrincipal, AuthenticationProperties)"/>.</returns>
	Task<AuthenticationScheme?> GetDefaultSignInSchemeAsync();

	/// <summary>
	/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.SignOutAsync(HttpContext, string, AuthenticationProperties)"/>.
	/// This is typically specified via <see cref="AuthenticationOptions.DefaultSignOutScheme"/>.
	/// Otherwise, this will fallback to <see cref="GetDefaultSignInSchemeAsync"/> .
	/// </summary>
	/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.SignOutAsync(HttpContext, string, AuthenticationProperties)"/>.</returns>
	Task<AuthenticationScheme?> GetDefaultSignOutSchemeAsync();

	/// <summary>
	/// Registers a scheme for use by <see cref="IAuthenticationService"/>. 
	/// </summary>
	/// <param name="scheme">The scheme.</param>
	void AddScheme(AuthenticationScheme scheme);

	/// <summary>
	/// Registers a scheme for use by <see cref="IAuthenticationService"/>. 
	/// </summary>
	/// <param name="scheme">The scheme.</param>
	/// <returns>true if the scheme was added successfully.</returns>
	bool TryAddScheme(AuthenticationScheme scheme)
	{
		try
		{
			AddScheme(scheme);
			return true;
		}
		catch {
			return false;
		}
	}

	/// <summary>
	/// Removes a scheme, preventing it from being used by <see cref="IAuthenticationService"/>.
	/// </summary>
	/// <param name="name">The name of the authenticationScheme being removed.</param>
	void RemoveScheme(string name);

	/// <summary>
	/// Returns the schemes in priority order for request handling.
	/// </summary>
	/// <returns>The schemes in priority order for request handling</returns>
	Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync();
}

虽然没有看到具体的provider,但是呢,可以通过接口注释看个大概哈。

比如说AuthenticationScheme 是认证方案的意思,从英文表面理解哈。然后里面有方法增删改查,意味着我们可以有多种认证方式。

这其实是刚需,因为比如以前颁发的身份令牌和现在接口颁发的身份令牌不一样了,那么为了无缝衔接,可以认可两种认证方式。

那么看下AuthenticationScheme 认证方案里面有些啥吧。

/// <summary>
/// AuthenticationSchemes assign a name to a specific <see cref="IAuthenticationHandler"/>
/// handlerType.
/// </summary>
public class AuthenticationScheme
{
	/// <summary>
	/// Initializes a new instance of <see cref="AuthenticationScheme"/>.
	/// </summary>
	/// <param name="name">The name for the authentication scheme.</param>
	/// <param name="displayName">The display name for the authentication scheme.</param>
	/// <param name="handlerType">The <see cref="IAuthenticationHandler"/> type that handles this scheme.</param>
	public AuthenticationScheme(string name, string? displayName, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type handlerType)
	{
		if (name == null)
		{
			throw new ArgumentNullException(nameof(name));
		}
		if (handlerType == null)
		{
			throw new ArgumentNullException(nameof(handlerType));
		}
		if (!typeof(IAuthenticationHandler).IsAssignableFrom(handlerType))
		{
			throw new ArgumentException("handlerType must implement IAuthenticationHandler.");
		}

		Name = name;
		HandlerType = handlerType;
		DisplayName = displayName;
	}

	/// <summary>
	/// The name of the authentication scheme.
	/// </summary>
	public string Name { get; }

	/// <summary>
	/// The display name for the scheme. Null is valid and used for non user facing schemes.
	/// </summary>
	public string? DisplayName { get; }

	/// <summary>
	/// The <see cref="IAuthenticationHandler"/> type that handles this scheme.
	/// </summary>
	[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
	public Type HandlerType { get; }
}

这里面有name 和displayname,一个是名称,一个是显示名称,相信很多人都见到过这样的类,里面有name 还有 displayname。

不要那么计较,显示名称是为了好大家好而已。比如说我们的sex 表示性别,那么displayname 可以写显示名称。

比如说你的一个计划类,里面可以有name 和 displayname。name 是JC159,displayname 是瞎扯计划,JC159 多难理解啊,瞎扯计划多好理解,瞎扯啊。

然后里面有一个是HandlerType,叫做处理类型,处理认证计划的类型。上面有注释,这个类型继承IAuthenticationHandler这个接口,那么也就是这个方案将由实现IAuthenticationHandler的类来处理,具体看实际的处理方案,比如jwt。

然后看下中间件的invoke。

/// <summary>
/// Invokes the middleware performing authentication.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
public async Task Invoke(HttpContext context)
{
	context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
	{
		OriginalPath = context.Request.Path,
		OriginalPathBase = context.Request.PathBase
	});

	// Give any IAuthenticationRequestHandler schemes a chance to handle the request
	var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
	foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
	{
		var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;
		if (handler != null && await handler.HandleRequestAsync())
		{
			return;
		}
	}

	var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
	if (defaultAuthenticate != null)
	{
		var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
		if (result?.Principal != null)
		{
			context.User = result.Principal;
		}
	}

	await _next(context);
}

一段一段看吧。

context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
{
	OriginalPath = context.Request.Path,
	OriginalPathBase = context.Request.PathBase
});

Features 是经过一个集合,比如我们经过中间件,我们可以向里面写入一些东西,然后供下一个中间件使用都行,有点像是游戏里面背包的功能。

var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
{
	var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;
	if (handler != null && await handler.HandleRequestAsync())
	{
		return;
	}
}

这里面就是获取就是获取相应的认真方案处理器,然后进行执行HandleRequestAsync。

值得主意的是,这里面并不是执行IAuthenticationHandler的方法,而是IAuthenticationRequestHandler的方法。

所以这并不意味这我们的写入的每个方案都必须通过,而是如果我们的写入的每个认证方案继承IAuthenticationRequestHandler,那么必须通过其中的HandleRequestAsync方法。

然后看下IAuthenticationRequestHandler 这个哈。

/// <summary>
/// Used to determine if a handler wants to participate in request processing.
/// </summary>
public interface IAuthenticationRequestHandler : IAuthenticationHandler
{
	/// <summary>
	/// Gets a value that determines if the request should stop being processed.
	/// <para>
	/// This feature is supported by the Authentication middleware
	/// which does not invoke any subsequent <see cref="IAuthenticationHandler"/> or middleware configured in the request pipeline
	/// if the handler returns <see langword="true" />.
	/// </para>
	/// </summary>
	/// <returns><see langword="true" /> if request processing should stop.</returns>
	Task<bool> HandleRequestAsync();
}

继续往下看:

var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
if (defaultAuthenticate != null)
{
	var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
	if (result?.Principal != null)
	{
		context.User = result.Principal;
	}
}

继续往下看哈,然后里面也有这个哈,如果有默认的认证方案,那么context.User 会通过默认认证方案的处理器进行获取。也就是说如果我们设置了默认方案,那么就会通过默认方案来进行认证。

await _next(context);

这个表示继续往下执行了。

那么来看下具体服务的认证吧,比如说jwt的。

services.AddAuthentication("Bearer")
	// 添加JwtBearer服务
 .AddJwtBearer(o =>
 {
	 o.TokenValidationParameters = tokenValidationParameters;
	 o.Events = new JwtBearerEvents
	 {
		 OnAuthenticationFailed = context =>
		 {
			 // 如果过期,则把<是否过期>添加到,返回头信息中
			 if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
			 {
				 context.Response.Headers.Add("Token-Expired", "true");
			 }
			 return Task.CompletedTask;
		 }
	 };
 });

首先来看一下:

services.AddAuthentication("Bearer")

这里面就是设置默认的认证方案:

public static AuthenticationBuilder AddAuthentication(this IServiceCollection services, string defaultScheme)
	=> services.AddAuthentication(o => o.DefaultScheme = defaultScheme);

public static AuthenticationBuilder AddAuthentication(this IServiceCollection services, Action<AuthenticationOptions> configureOptions) {
	if (services == null)
	{
		throw new ArgumentNullException(nameof(services));
	}

	if (configureOptions == null)
	{
		throw new ArgumentNullException(nameof(configureOptions));
	}

	var builder = services.AddAuthentication();
	services.Configure(configureOptions);
	return builder;
}

看一下:var builder = services.AddAuthentication();

这个哈,这个才是具体增加具体服务的。

public static AuthenticationBuilder AddAuthentication(this IServiceCollection services)
{
	if (services == null)
	{
		throw new ArgumentNullException(nameof(services));
	}

	services.AddAuthenticationCore();
	services.AddDataProtection();
	services.AddWebEncoders();
	services.TryAddSingleton<ISystemClock, SystemClock>();
	return new AuthenticationBuilder(services);
}

然后看下services.AddAuthenticationCore();,为什么看下这个呢?难道我提前看了这个东西吗?

不是,因为我们知道分层的时候有个Core的层,是具体实现的,那么这种带core 一般就是具体实现方式了。

public static IServiceCollection AddAuthenticationCore(this IServiceCollection services)
{
	if (services == null)
	{
		throw new ArgumentNullException(nameof(services));
	}

	services.TryAddScoped<IAuthenticationService, AuthenticationService>();
	services.TryAddSingleton<IClaimsTransformation, NoopClaimsTransformation>(); // Can be replaced with scoped ones that use DbContext
	services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>();
	services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>();
	return services;
}

前面我们看了这个IAuthenticationHandlerProvider 和IAuthenticationSchemeProvider ,那么这里可以看到他们的具体实现是AuthenticationHandlerProvider和AuthenticationSchemeProvider。

前面提及到会通过handletype来获取具体的处理器,那么来看下具体怎么实现的吧。

/// <summary>
/// Returns the handler instance that will be used.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="authenticationScheme">The name of the authentication scheme being handled.</param>
/// <returns>The handler instance.</returns>
public async Task<IAuthenticationHandler> GetHandlerAsync(HttpContext context, string authenticationScheme)
{
	if (_handlerMap.ContainsKey(authenticationScheme))
	{
		return _handlerMap[authenticationScheme];
	}

	var scheme = await Schemes.GetSchemeAsync(authenticationScheme);
	if (scheme == null)
	{
		return null;
	}
	var handler = (context.RequestServices.GetService(scheme.HandlerType) ??
		ActivatorUtilities.CreateInstance(context.RequestServices, scheme.HandlerType))
		as IAuthenticationHandler;
	if (handler != null)
	{
		await handler.InitializeAsync(scheme, context);
		_handlerMap[authenticationScheme] = handler;
	}
	return handler;
}

看这个GetHandlerAsync,是通过依赖注入的方式来获取的,根据方案里面的GetHandlerAsync。

那么从这里就能猜到jwt的具体实现了,那么直接来看吧。

services.AddAuthentication("Bearer")
                // 添加JwtBearer服务
             .AddJwtBearer(o =>
             {
                 o.TokenValidationParameters = tokenValidationParameters;
                 o.Events = new JwtBearerEvents
                 {
                     OnAuthenticationFailed = context =>
                     {
                         // 如果过期,则把<是否过期>添加到,返回头信息中
                         if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                         {
                             context.Response.Headers.Add("Token-Expired", "true");
                         }
                         return Task.CompletedTask;
                     }
                 };
             });

其实不建议这么写的,应该是:

直接标明这里使用的策略,之所以这个能够生效,是因为默认的是

红框框部分是Bearer,但是不友好,对框架不熟,容易形成误导。

继续往下看:

public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<JwtBearerOptions> configureOptions)
{
	builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());
	return builder.AddScheme<JwtBearerOptions, JwtBearerHandler>(authenticationScheme, displayName, configureOptions);
}

AddScheme 就是具体的注入了:

/// <summary>
/// Adds a <see cref="AuthenticationScheme"/> which can be used by <see cref="IAuthenticationService"/>.
/// </summary>
/// <typeparam name="TOptions">The <see cref="AuthenticationSchemeOptions"/> type to configure the handler."/>.</typeparam>
/// <typeparam name="THandler">The <see cref="AuthenticationHandler{TOptions}"/> used to handle this scheme.</typeparam>
/// <param name="authenticationScheme">The name of this scheme.</param>
/// <param name="displayName">The display name of this scheme.</param>
/// <param name="configureOptions">Used to configure the scheme options.</param>
/// <returns>The builder.</returns>
public virtual AuthenticationBuilder AddScheme<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
	where TOptions : AuthenticationSchemeOptions, new()
	where THandler : AuthenticationHandler<TOptions>
	=> AddSchemeHelper<TOptions, THandler>(authenticationScheme, displayName, configureOptions);

然后加入到认证方案中去,JwtBearerOptions 就是这个方案的配置,JwtBearerHandler就是具体的处理,看下AddSchemeHelper。

private AuthenticationBuilder AddSchemeHelper<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
	where TOptions : class, new()
	where THandler : class, IAuthenticationHandler
{
	Services.Configure<AuthenticationOptions>(o =>
	{
		o.AddScheme(authenticationScheme, scheme => {
			scheme.HandlerType = typeof(THandler);
			scheme.DisplayName = displayName;
		});
	});
	if (configureOptions != null)
	{
		Services.Configure(authenticationScheme, configureOptions);
	}
	Services.AddTransient<THandler>();
	return this;
}

分步骤看下:

Services.Configure<AuthenticationOptions>(o =>
{
	o.AddScheme(authenticationScheme, scheme => {
		scheme.HandlerType = typeof(THandler);
		scheme.DisplayName = displayName;
	});
});

这一步就是添加具体的认证方案。

if (configureOptions != null)
{
      Services.Configure(authenticationScheme, configureOptions);
}
Services.AddTransient<THandler>();

这一步就是注入配置文件,并且将处理器注入到ioc中,这里就是JwtBearerHandler了。

JwtBearerHandler 就不看了,就是一些具体的实现,根据配置文件,然后处理,就属于jwt的知识了。

补充

这里扩容一下配置的知识,主要解释一下JwtBearerHandler 是如何根据不同的authenticationScheme 获取不同的配置的。

Services.Configure(authenticationScheme, configureOptions);
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions)
	where TOptions : class
{
	if (services == null)
	{
		throw new ArgumentNullException(nameof(services));
	}

	if (configureOptions == null)
	{
		throw new ArgumentNullException(nameof(configureOptions));
	}

	services.AddOptions();
	services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureNamedOptions<TOptions>(name, configureOptions));
	return services;
}

看到吧,实际上获IConfigureOptions,会获取一组ConfigureNamedOptions,然后通过name筛选出来。

看下JwtBearerHandler:

public class JwtBearerHandler : AuthenticationHandler<JwtBearerOptions>
public JwtBearerHandler(IOptionsMonitor<JwtBearerOptions> options, ILoggerFactory logger, UrlEncoder encoder, IDataProtectionProvider dataProtection, ISystemClock clock)
	: base(options, logger, encoder, clock)
{ }

将options 传给了AuthenticationHandler。

那么看下AuthenticationHandler 中如何处理的吧。

初始化的时候:

/// <summary>
/// Initialize the handler, resolve the options and validate them.
/// </summary>
/// <param name="scheme"></param>
/// <param name="context"></param>
/// <returns></returns>
public async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
	if (scheme == null)
	{
		throw new ArgumentNullException(nameof(scheme));
	}
	if (context == null)
	{
		throw new ArgumentNullException(nameof(context));
	}

	Scheme = scheme;
	Context = context;

	Options = OptionsMonitor.Get(Scheme.Name) ?? new TOptions();
	Options.Validate(Scheme.Name);

	await InitializeEventsAsync();
	await InitializeHandlerAsync();
}

进行一波筛选而来的哈。

下一节看下授权的源码吧。

posted @ 2021-11-21 19:03  敖毛毛  阅读(669)  评论(0编辑  收藏  举报