重新整理 .net core 實踐篇——— 許可權原始碼閱讀四十五]

敖毛毛發表於2021-11-21

前言

簡單介紹一下許可權原始碼閱讀一下。

正文

一直有人對授權這個事情上爭論不休,有的人認為在輸入賬戶密碼給後臺這個時候進行了授權,因為認為傳送了一個身份令牌,令牌裡面可能有些使用者角色資訊,認為這就是授權,有的人認為這只是獲取令牌的過程。

現實生活中有一個是授權證書,那麼有人認為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();
}

進行一波篩選而來的哈。

下一節看下授權的原始碼吧。

相關文章