【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

posted @ 2017-12-14 14:20  Never、C  阅读(6124)  评论(3编辑  收藏  举报