前言
簡單介紹一下許可權原始碼閱讀一下。
正文
一直有人對授權這個事情上爭論不休,有的人認為在輸入賬戶密碼給後臺這個時候進行了授權,因為認為傳送了一個身份令牌,令牌裡面可能有些使用者角色資訊,認為這就是授權,有的人認為這只是獲取令牌的過程。
現實生活中有一個是授權證書,那麼有人認為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
看下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();
}
進行一波篩選而來的哈。
結
下一節看下授權的原始碼吧。