本系列將分析ASP.NET Core執行原理
- 【ASP.NET Core】執行原理[1]:建立WebHost
- 【ASP.NET Core】執行原理[2]:啟動WebHost
- 【ASP.NET Core】執行原理[3]:認證
本節將分析Authentication
原始碼參考.NET Core 2.0.0
目錄
- 認證
- AddAuthentication
- IAuthenticationService
- IAuthenticationHandlerProvider
- IAuthenticationSchemeProvider
- UseAuthentication
- AddAuthentication
- Authentication.Cookies
- 模擬一個Cookie認證
認證
認證已經是當前Web必不可缺的元件。看看ASP.NET Core如何定義和實現認證。
在Startup類中,使用認證元件非常簡單。
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
}
AddAuthentication
先來分析AddAuthentication:
public static IServiceCollection AddAuthenticationCore(this IServiceCollection services)
{
services.TryAddScoped<IAuthenticationService, AuthenticationService>();
services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>();
services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>();
return services;
}
public static AuthenticationBuilder AddAuthentication(this IServiceCollection services)
{
services.AddAuthenticationCore();
return new AuthenticationBuilder(services);
}
IAuthenticationService
在AddAuthentication方法中註冊了IAuthenticationService、IAuthenticationHandlerProvider、IAuthenticationSchemeProvider3個服務。
首先分析下IAuthenticationService
:
public interface IAuthenticationService
{
Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme);
Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties);
Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties);
Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties);
Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties);
}
AuthenticateAsync:驗證使用者身份,並返回AuthenticateResult物件。
ChallengeAsync:通知使用者需要登入。在預設實現類AuthenticationHandler
中,返回401。
ForbidAsync:通知使用者許可權不足。在預設實現類AuthenticationHandler
中,返回403。
SignInAsync:登入使用者。(該方法需要與AuthenticateAsync配合驗證邏輯)
SignOutAsync:退出登入。
而IAuthenticationService的預設實現類為:
public class AuthenticationService : IAuthenticationService
{
public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme)
{
if (scheme == null)
{
var defaultScheme = await Schemes.GetDefaultAuthenticateSchemeAsync();
scheme = defaultScheme?.Name;
}
var handler = await Handlers.GetHandlerAsync(context, scheme);
var result = await handler.AuthenticateAsync();
if (result != null && result.Succeeded)
return AuthenticateResult.Success(new AuthenticationTicket(result.Principal, result.Properties, result.Ticket.AuthenticationScheme));
return result;
}
}
在AuthenticateAsync程式碼中,先查詢Scheme,然後根據SchemeName查詢Handle,再呼叫handle的同名方法。
解釋一下GetDefaultAuthenticateSchemeAsync
會先查DefaultAuthenticateScheme
,如果為null,再查DefaultScheme
。
實際上,AuthenticationService的其他方法都是這樣的模式,最終呼叫的都是handle的同名方法。
IAuthenticationHandlerProvider
因此,我們看看獲取Handle的IAuthenticationHandlerProvider
:
public interface IAuthenticationHandlerProvider
{
Task<IAuthenticationHandler> GetHandlerAsync(HttpContext context, string authenticationScheme);
}
該介面只有一個方法,根據schemeName查詢Handle:
public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider
{
public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes)
{
Schemes = schemes;
}
public IAuthenticationSchemeProvider Schemes { get; }
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方法中,我們看到是先從IAuthenticationSchemeProvider中根據schemeName獲取scheme,然後透過scheme的HandleType來建立IAuthenticationHandler。
建立Handle的時候,是先從ServiceProvider中獲取,如果不存在則透過ActivatorUtilities建立。
獲取到Handle後,將呼叫一次handle的InitializeAsync方法。
當下次獲取Handle的時候,將直接從快取中獲取。
需要補充說明的是一共有3個Handle:
IAuthenticationHandler、IAuthenticationSignInHandler、IAuthenticationSignOutHandler。
public interface IAuthenticationSignInHandler : IAuthenticationSignOutHandler, IAuthenticationHandler{}
public interface IAuthenticationSignOutHandler : IAuthenticationHandler{}
public interface IAuthenticationHandler{}
之所以介面拆分,應該是考慮到大部分的系統的登入和退出是單獨一個身份系統處理。
IAuthenticationSchemeProvider
透過IAuthenticationHandlerProvider程式碼,我們發現最終還是需要IAuthenticationSchemeProvider來提供Handle型別:
這裡展示IAuthenticationSchemeProvider介面核心的2個方法。
public interface IAuthenticationSchemeProvider
{
void AddScheme(AuthenticationScheme scheme);
Task<AuthenticationScheme> GetSchemeAsync(string name);
}
預設實現類AuthenticationSchemeProvider
:
public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider
{
private IDictionary<string, AuthenticationScheme> _map = new Dictionary<string, AuthenticationScheme>(StringComparer.Ordinal);
public virtual void AddScheme(AuthenticationScheme scheme)
{
if (_map.ContainsKey(scheme.Name))
{
throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
}
lock (_lock)
{
if (_map.ContainsKey(scheme.Name))
{
throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
}
_map[scheme.Name] = scheme;
}
}
public virtual Task<AuthenticationScheme> GetSchemeAsync(string name)
=> Task.FromResult(_map.ContainsKey(name) ? _map[name] : null);
}
因此,整個認證邏輯最終都回到了Scheme位置。也就說明要認證,則必須先註冊Scheme。
UseAuthentication
AddAuthentication實現了註冊Handle,UseAuthentication則是使用Handle去認證。
public static IApplicationBuilder UseAuthentication(this IApplicationBuilder app)
{
return app.UseMiddleware<AuthenticationMiddleware>();
}
使用了AuthenticationMiddleware
:
public class AuthenticationMiddleware
{
private readonly RequestDelegate _next;
public IAuthenticationSchemeProvider Schemes { get; set; }
public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
{
_next = next;
Schemes = schemes;
}
public async Task Invoke(HttpContext context)
{
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);
}
}
在Invoke程式碼中,我們看到先查詢出所有的AuthenticationRequestHandler
。如果存在,則立即呼叫其HandleRequestAsync方法,成功則直接返回。
(RequestHandler一般是處理第三方認證響應的OAuth / OIDC等遠端認證方案。)
如果不存在RequestHandler或執行失敗,將呼叫預設的AuthenticateHandle
的AuthenticateAsync方法。同時會對context.User賦值。
Authentication.Cookies
Cookies認證是最常用的一種方式,這裡我們分析一下Cookie原始碼:
AddCookie
public static class CookieExtensions
{
public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder)
=> builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme)
=> builder.AddCookie(authenticationScheme, configureOptions: null);
public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, Action<CookieAuthenticationOptions> configureOptions)
=> builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, configureOptions);
public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, Action<CookieAuthenticationOptions> configureOptions)
=> builder.AddCookie(authenticationScheme, displayName: null, configureOptions: configureOptions);
public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<CookieAuthenticationOptions> configureOptions)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
return builder.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler>(authenticationScheme, displayName, configureOptions);
}
}
AddCookie(this AuthenticationBuilder builder, Action<CookieAuthenticationOptions> configureOptions)
可能是我們最常用的
該方法將註冊CookieAuthenticationHandler用於處理認證相關。
public class CookieAuthenticationHandler : AuthenticationHandler<CookieAuthenticationOptions>,IAuthenticationSignInHandler
{
public async virtual Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
{
var signInContext = new CookieSigningInContext(
Context,
Scheme,
Options,
user,
properties,
cookieOptions);
var ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.Scheme.Name);
var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding());
Options.CookieManager.AppendResponseCookie(
Context,
Options.Cookie.Name,
cookieValue,
signInContext.CookieOptions);
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var cookie = Options.CookieManager.GetRequestCookie(Context, Options.Cookie.Name);
var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding());
return AuthenticateResult.Success(ticket);
}
}
這裡我們用Cookie示例:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options => options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options => options.Cookie.Path = "/");
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.Map("/login", app2 => app2.Run(async context =>
{
var claimIdentity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
claimIdentity.AddClaim(new Claim(ClaimTypes.Name, Guid.NewGuid().ToString("N")));
await context.SignInAsync(new ClaimsPrincipal(claimIdentity));
}));
app.UseAuthentication();
app.Run(context => context.Response.WriteAsync(context.User?.Identity?.IsAuthenticated ?? false ? context.User.Identity.Name : "No Login!"));
}
當訪問login的時候,將返回Cookie。再訪問除了login以外的頁面時則返回一個guid。
模擬身份認證
public class DemoHandle : IAuthenticationSignInHandler
{
private HttpContext _context;
private AuthenticationScheme _authenticationScheme;
private string _cookieName = "user";
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
_context = context;
_authenticationScheme = scheme;
return Task.CompletedTask;
}
public Task<AuthenticateResult> AuthenticateAsync()
{
var cookie = _context.Request.Cookies[_cookieName];
if (string.IsNullOrEmpty(cookie))
{
return Task.FromResult(AuthenticateResult.NoResult());
}
var identity = new ClaimsIdentity(_authenticationScheme.Name);
identity.AddClaim(new Claim(ClaimTypes.Name, cookie));
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), _authenticationScheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
{
_context.Response.Cookies.Append(_cookieName, user.Identity.Name);
return Task.CompletedTask;
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultScheme = "cookie";
options.AddScheme<DemoHandle>("cookie", null);
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.Map("/login", app2 => app2.Run(async context =>
{
var claimIdentity = new ClaimsIdentity();
claimIdentity.AddClaim(new Claim(ClaimTypes.Name, Guid.NewGuid().ToString("N")));
await context.SignInAsync(new ClaimsPrincipal(claimIdentity));
context.Response.Redirect("/");
}));
app.UseAuthentication();
app.Run(context => context.Response.WriteAsync(context.User?.Identity?.IsAuthenticated ?? false ? context.User.Identity.Name : "No Login!"));
}
預設訪問根目錄的時候,顯示“No Login”
當使用者訪問login路徑的時候,會跳轉到根目錄,並顯示登入成功。
這裡稍微補充一下Identity.IsAuthenticated => !string.IsNullOrEmpty(_authenticationType);