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


本系列將分析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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM