【ASP.NET Core】執行原理(3):認證

Never、C發表於2017-12-14

本系列將分析ASP.NET Core執行原理

本節將分析Authentication

原始碼參考.NET Core 2.0.0

目錄

  1. 認證
    1. AddAuthentication
      1. IAuthenticationService
      2. IAuthenticationHandlerProvider
      3. IAuthenticationSchemeProvider
    2. UseAuthentication
  2. Authentication.Cookies
  3. 模擬一個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);

本文連結:http://www.cnblogs.com/neverc/p/8037477.html

相關文章