• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

ASP.NETCore认证与授权[1]:初识认证(笔记)

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

原文链接:  https://www.cnblogs.com/RainingNight/p/introduce-basic-authentication-in-asp-net-core.html

 

在ASP.NET 4.X 中,我们最常用的是Forms认证,它既可以用于局域网环境,也可用于互联网环境,有着非常广泛的使用。但是它很难进行扩展,更无法与第三方认证集成,因此,在 ASP.NET Core 中对认证与授权进行了全新的设计,并使用基于声明的认证(claims-based authentication),以适应现代化应用的需求。在运行原理解剖[5]:Authentication中介绍了一下HttpContext与认证系统的集成,本系列文章则来详细介绍一下 ASP.NET Core 中认证与授权。

基于声明的认证有两个主要的特点:

  • 将认证与授权拆分成两个独立的服务。

  • 在需要授权的服务中,不用再去关心你是如何认证的,你用Windows认证也好,Forms认证也行,只要你出示你的 Claims就行了。

ASP.NET Core 中的用户身份

Claim

在 ASP.NET Core 中,使用Cliam类来表示用户身份中的一项信息,它由核心的TypeValue属性构成:

一个Claim可以是“用户的姓名”,“邮箱地址”,“电话”,等等,而多个Claim构成一个用户的身份,使用ClaimsIdentity类来表示:

public class ClaimsIdentity : IIdentity
{    
    public virtual IEnumerable<Claim> Claims {get;}

    public virtual string AuthenticationType => _authenticationType;
    public virtual bool IsAuthenticated => !string.IsNullOrEmpty(_authenticationType);
    public virtual string Name
    {
        get
        {
            Claim claim = FindFirst(_nameClaimType);
            if (claim != null) return claim.Value;
            return null;
        }
    }

}

如上,其Name属性用来查找Claims中,第一个Type为我们创建ClaimsIdentity时指定的NameClaimType的Claim的值,若未指定Type时则使用默认的ClaimTypes.Name。而IsAuthenticated只是判断_authenticationType是否为空,_authenticationType则对应上一章中介绍的Scheme。

// 创建一个用户身份,注意需要指定AuthenticationType,否则IsAuthenticated将为false。
var claimIdentity = new ClaimsIdentity("myAuthenticationType");
// 添加几个Claim
claimIdentity.AddClaim(new Claim(ClaimTypes.Name, "bob"));
claimIdentity.AddClaim(new Claim(ClaimTypes.Email, "[email protected]"));
claimIdentity.AddClaim(new Claim(ClaimTypes.MobilePhone, "18888888888"));

如上,我们可以根据需要添加任意个的Claim,最后我们还需要再将用户身份放到ClaimsPrincipal对象中。

ClaimsPrincipal

ASP.NET Core 中,HttpContext直接使用的就是ClaimsPrincipal类型

public abstract class HttpContext
{
    public abstract ClaimsPrincipal User { get; set; }
}

ClaimsPrincipal的创建非常简单,只需传入我们上面创建的用户身份即可:

var principal = new ClaimsPrincipal(claimIdentity);

由于HTTP是无状态的,我们通常使用Cookie,请求头或请求参数等方式来附加用户的信息,在网络上进行传输,这就涉及到序列化和安全方面的问题。因此,还需要将principal对象包装成AuthenticationTicket对象。

 

AuthenticationTicket

我们创建完ClaimsPrincipal对象后,需要将它生成一个用户票据并颁发给用户,然后用户拿着这个票据,便可以访问受保持的资源,而在 ASP.NET Core 中,用户票据用AuthenticationTicket来表示,如在Cookie认证中,其认证后的Cookie值便是对该对象序列化后的结果,它的定义如下:

用户票据除了包含上面创建的principal对象外,还需要指定一个AuthenticationScheme (通常在授权中用来验证Scheme),并且还包含一个AuthenticationProperties对象,它主要是一些用户票据安全方面的一些配置,如过期时间,是否持久等。

var properties = new AuthenticationProperties();
var ticket = new AuthenticationTicket(principal, properties, "myScheme");
// 加密 序列化
var token = Protect(ticket);

最后,我们可以将票据(token)写入到Cookie中,或是也可以以JSON的形式返回让客户端自行保存,由于我们对票据进行了加密,可以保证在网络中安全的传输而不会被篡改。

最终身份令牌的结构大概是这样的:

 

Microsoft.AspNetCore.Authentication

上面,我们介绍了身份票据的创建过程,下面就来介绍一下 ASP.NET Core 中的身份认证。

ASP.NET Core 中的认证系统具体实现在 Security 项目中,它包含 CookieJwtBearerOAuthOpenIdConnect 等:

Usage

而对于认证系统的配置,分为两步,也是我们所熟悉的注册服务和配置中间件:

首先,在DI中注册服务认证所需的服务:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(o =>
    {
        o.ClientId = "server.hybrid";
        o.ClientSecret = "secret";
        o.Authority = "https://demo.identityserver.io/";
        o.ResponseType = OpenIdConnectResponseType.CodeIdToken;
    });
}

最后,注册认证中间件:

public void Configure(IApplicationBuilder app)
{
    app.UseAuthentication();
}

如上,我们的系统便支持了CookieJwtBearer两种认证方式

Microsoft.AspNetCore.Authentication,是所有认证实现的公共抽象类,它定义了实现认证Handler的规范,并包含一些共用的方法,如令牌加密,序列化等,AddAuthentication 便是其提供的统一的注册认证服务的扩展方法:

public static AuthenticationBuilder AddAuthentication(this IServiceCollection services)
{
    services.AddAuthenticationCore();
    services.AddDataProtection();
    services.AddWebEncoders();
    services.TryAddSingleton<ISystemClock, SystemClock>();
    return new AuthenticationBuilder(services);
}

public static AuthenticationBuilder AddAuthentication(this IServiceCollection services, Action<AuthenticationOptions> configureOptions) 
{
    var builder = services.AddAuthentication();
    services.Configure(configureOptions);
    return builder;
}

如上,它首先会调用上一章中介绍的AddAuthenticationCore方法,然后注册了DataProtectionWebEncoders两个服务。而对 AuthenticationOptions 我们之前在IAuthenticationSchemeProvider也介绍过,它用来配置Scheme。

AddScheme

在上面的 AddAuthentication 中返回的是一个AuthenticationBuilder类型,所有认证Handler的注册都是以它的扩展形式来实现的,它同时也提供了AddScheme扩展方法,使我们可以更加方便的来配置Scheme:在这里的AddScheme 扩展方法只是封装了对AuthenticationOptionsAddScheme的调用,如上面示例中的AddCookie便是调用该扩展方法来实现的。

AddRemoteScheme

看到 Remote 我们应该就可以猜到它是一种远程验证方式

UseAuthentication

在上面,注册认证中间件时,我们只需调用一个UseAuthentication扩展方法,因为它会执行我们注册的所有认证Handler:

public static IApplicationBuilder UseAuthentication(this IApplicationBuilder app)
{
    return app.UseMiddleware<AuthenticationMiddleware>();
}
public class AuthenticationMiddleware
{
    private readonly RequestDelegate _next;
    public IAuthenticationSchemeProvider Schemes { get; set; }

    public async Task Invoke(HttpContext context)
    {
        context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
        {
            OriginalPath = context.Request.Path,
            OriginalPathBase = context.Request.PathBase
        });

        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);
    }
}

很简单,但是很强大,不管我们是使用Cookie认证,还是Bearer认证,等等,都只需要这一个中间件,因为它会解析所有的Handler来执行。

不过,在这里,这会先判断是否具体实现了IAuthenticationRequestHandler的Hander,优先来执行,这个是什么鬼?

查了一下,发现IAuthenticationRequestHandler是在HttpAbstractions中定义的,只是在运行原理解剖[5]:Authentication中没有介绍到它:

public interface IAuthenticationRequestHandler : IAuthenticationHandler
{
    Task<bool> HandleRequestAsync();
}

它多了一个HandleRequestAsync方法,那么它存在的意义是什么呢?其实在Cookie认证中并没有用到它,它通常在远程认证(如:OAuth, OIDC等)中使用,下文再来介绍。

继续分析上面代码,通过调用Schemes.GetDefaultAuthenticateSchemeAsync来获取到认证的Scheme,也就是上文提到的问题,我们必须指定默认的Scheme。

最后,调用AuthenticateAsync方法进行认证,认证成功后,为HttpContext.User赋值,至于如何解析身份令牌生成ClaimsPrincipal对象,则交给相应的Handler来处理。

 

认证Handler

上文中多次提到认证Handler,它由统一的AuthenticationMiddleware来调用,负责具体的认证实现,并分为本地认证与远程认证两种方式。

在本地验证中,身份令牌的发放与认证通常是由同一个服务器来完成,这也是我们比较熟悉的场景,对于Cookie, JwtBearer等认证来说,都属于是本地验证。而当我们使用OAuth, OIDC等验证方式时,身份令牌的发放则是由独立的服务或是第三方(QQ, Weibo 等)认证来提供,此时在我们的应用程序中获取身份令牌时需要请求远程服务器,因此称之为远程验证。

 

AuthenticationHandler

AuthenticationHandler是所有认证Handler的抽象基类,对于本地认证直接实现该类即可,定义如下:

 

public abstract class AuthenticationHandler<TOptions> : IAuthenticationHandler where TOptions : AuthenticationSchemeOptions, new()
{
    ...

    public async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
    {
        ...

        await InitializeEventsAsync();
        await InitializeHandlerAsync();
    }

    protected virtual async Task InitializeEventsAsync() { }
    protected virtual Task<object> CreateEventsAsync() => Task.FromResult(new object());
    protected virtual Task InitializeHandlerAsync() => Task.CompletedTask;

    public async Task<AuthenticateResult> AuthenticateAsync()
    {
        var result = await HandleAuthenticateOnceAsync();

        ...
    }

    protected Task<AuthenticateResult> HandleAuthenticateOnceAsync()
    {
        if (_authenticateTask == null)
        {
            _authenticateTask = HandleAuthenticateAsync();
        }
        return _authenticateTask;
    }

    protected abstract Task<AuthenticateResult> HandleAuthenticateAsync();


    protected virtual Task HandleForbiddenAsync(AuthenticationProperties properties)
    {
        Response.StatusCode = 403;
        return Task.CompletedTask;
    }

    protected virtual Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        Response.StatusCode = 401;
        return Task.CompletedTask;
    }

    ...
}

如上,它定义一个抽象方法HandleAuthenticateAsync,并使用HandleAuthenticateOnceAsync方法来保证其在每次认证只执行一次。而HandleAuthenticateAsync是认证的核心,交给具体的认证Handler负责实现。而对于 ChallengeAsync, ForbidAsync 等方法也提供了默认的实现。

而对于HandleAuthenticateAsync的实现,大致的逻辑就是从请求中获取上面发放的身份令牌,然后解析成AuthenticationTicket,并经过一系列的验证,最终返回ClaimsPrincipal对象。

 

RemoteAuthenticationHandler

RemoteAuthenticationHandler 便是所有远程认证的抽象基类了,它继承自AuthenticationHandler,并实现了IAuthenticationRequestHandler接口:

在上面介绍的AuthenticationMiddleware中,提到它会先执行实现了IAuthenticationRequestHandler 接口的Handler(远程认证),之后(若未完成认证)再执行本地认证Handler。

RemoteAuthenticationHandler中核心的认证逻辑便是 HandleRequestAsync 方法,它主要包含2个步骤:

  1. 首先执行一个抽象方法HandleRemoteAuthenticateAsync,由具体的Handler来实现,该方法返回的HandleRequestResult对象包含验证的结果(跳过,失败,成功等),在成功时会包含一个ticket对象。

  2. 若上一步验证成功,则根据返回的ticket,获取到ClaimsPrincipal对象,并调用其它认证Handler的Context.SignInAsync方法。

也就是说,远程Hander会在用户未登录时,指引用户跳转到认证服务器,登录成功后,解析认证服务器传回的凭证,最终依赖于本地Handler来保存身份令牌。当用户再次访问则无需经过远程Handler,直接交给本地Handler来处理。

由此也可以知道,远程认证中本身并不具备SignIn的能力,所以必须通过指定其它SignInScheme交给本地认证来完成 SignIn

对于其父类的HandleAuthenticateAsync抽象方法则定义了一个默认实现:“直接转交给本地验证来处理”。当我们需要定义自己的远程认证方式时,通常只需实现 HandleRemoteAuthenticateAsync 即可,而不用再去处理 HandleAuthenticateAsync 。

总结

基于声明的认证并不是微软所特有的,它在国外被广泛的使用,如微软的ADFS,Google,Facebook,Twitter等等。在基于声明的认证中,对认证和授权进行了明确的区分,认证用来颁发一个用户的身份标识,其包含这个用户的基本信息,而对于这个身份的颁发则由我们信任的第三方机构来(STS)颁发(当然,你也可以自己来颁发)。而授权,则是通过获取身份标识中的信息,来判断该用户能做什么,不能做什么。

本文对 ASP.NET Core 中认证系统的整个流程做了一个简要的介绍,可能会比较苦涩难懂,不过没关系,大致有个印象就好,下一章则详细介绍一下最常用的本地认证方式:Cookie认证,后续也会详细介绍 OIDC 的用法与实现,到时再回头来看本文或许会豁然开朗。

 


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
ASP.NET多种调用javascript脚本的方法发布时间:2022-07-10
下一篇:
ASP.NETWebAPI跨域访问(CORS)发布时间:2022-07-10
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap