美文网首页程序员.NET.NET Core
eShopOnContainers 知多少[3]:Identit

eShopOnContainers 知多少[3]:Identit

作者: 圣杰 | 来源:发表于2018-10-22 06:52 被阅读20次

    首先感谢晓晨Master和EdisonChou的审稿!也感谢正在阅读的您!

    引言

    通常,服务所公开的资源和 API 必须仅限受信任的特定用户和客户端访问。那进行 API 级别信任决策的第一步就是身份认证——确定用户身份是否可靠。

    在微服务场景中,身份认证通常统一处理。一般有两种实现形式:

    1. 基于API 网关中心化认证:要求客户端必须都通过网关访问微服务。(这就要求提供一种安全机制来认证请求是来自于网关。)

      基于API 网关中心化认证
    2. 基于安全令牌服务(STS)认证:所有的客户端先从STS获取令牌,然后请求时携带令牌完成认证。

      基于安全令牌服务(STS)认证

    而本节所讲的Identity microservice就是使用第二种身份认证方式。

    服务简介

    Identity microservice 主要用于统一的身份认证和授权,为其他服务提供支撑。

    提到认证,大家最熟悉不过的当属Cookie认证了,它也是目前使用最多的认证方式。但Cookie认证也有其局限性:不支持跨域、移动端不友好等。而从当前的架构来看,需要支持移动端、Web端、微服务间的交叉认证授权,所以传统的基于Cookie的本地认证方案就行不通了。我们就需要使用远程认证的方式来提供统一的认证授权机制。
    而远程认证方式当属:OAuth2.0和OpenID Connect了。借助OAuth2.0和OpenID Connect即可实现类似下图的认证体系:


    而如何实现呢,借助:

    1. ASP.NET Core Identity
    2. IdentityServer4

    基于Cookie的认证和基于Token的认证的差别如下所示:

    Cookie-Based Auth VS Token-Based Auth

    架构模式

    该微服务作为支撑服务,并没有选择复杂的架构模式,使用了MVC单层架构,使用EF Core ORM框架用于数据持久化,SQL Server数据库。使用Autofac IOC框架替换了默认依赖注入框架。

    项目结构如下所示:


    Identity.API 项目结构

    核心技术选型:

    1. MVC单层架构
    2. EF Core
    3. ASP.NET Core Identity
    4. IdentityServer4
    5. SQL Server 数据库
    6. Autofac

    PS:对ASP.NET Core Identity、IdentityServer4以及OAuth2.0不了解的,请先行阅读文末参考资料补课!!!

    下面就着重讲解ASP.NET Core Identity和IdentityServer4在本服务中的使用。

    ASP.NET Core Identity && IdentityServer4简介

    ASP.NET Core Identity用于构建ASP.NET Core Web应用程序的成员资格系统,包括成员资格,登录和用户数据(包括登录信息、角色和声明)。
    ASP.NET Core Identity封装了User、Role、Claim等身份信息,便于我们快速完成登录功能的实现,并且支持第三方登录(Google、Facebook、QQ、Weixin等,支持开箱即用[第三方身份提供商列表]),以及双重验证,同时内置支持Bearer 认证(令牌认证)。

    虽然ASP.NET Core Identity已经完成了绝大多数的功能,且支持第三方登录(第三方为其用户颁发令牌),但若要为本地用户颁发令牌,则需要自己实现令牌的颁发和验证逻辑。换句话说,我们需要自行实现OpenId Connect协议。

    OpenID Connect 1.0 是基于OAuth 2.0协议之上的简单身份层,它允许客户端根据授权服务器的认证结果最终确认终端用户的身份,以及获取基本的用户信息。

    而IdentityServer4就是为ASP.NET Core量身定制的实现了OpenId Connect和OAuth2.0协议的认证授权中间件。IdentityServer4在ASP.NET Core Identity的基础上,提供令牌的颁发验证等。

    认证流程简介

    在ASP.NET Core中使用的是基于申明(Claim)的认证,而什么是申明(Cliam)呢?

    Claim 是关于一个人或组织的某个主题的陈述,比如:一个人的名称,角色,个人喜好,种族,特权,社团,能力等等。它本质上就是一个键值对,是一种非常通用的保存用户信息的方式,可以很容易的将认证和授权分离开来,前者用来表示用户是/不是什么,后者用来表示用户能/不能做什么。在认证阶段我们通过用户信息获取到用户的Claims,而授权便是对这些的Claims的验证,如:是否拥有Admin的角色,姓名是否叫XXX等等。

    认证主要与以下几个核心对象打交道:

    1. Claim(身份信息)
    2. ClaimsIdentity(身份证)
    3. ClaimsPrincipal (身份证持有者)
    4. AuthorizationToken (授权令牌)
    5. IAuthenticationScheme(认证方案)
    6. IAuthenticationHandler(与认证方案对应的认证处理器)
    7. IAuthenticationService (向外提供统一的认证服务接口)

    那其认证流程是怎样的呢?

    用户打开登录界面,输入用户名密码先行登录,服务端先行校验用户名密码是否有效,有效则返回用户实例(User),这时进入认证准备阶段,根据用户实例携带的身份信息(Claim),创建身份证(ClaimsIdentity),然后将身份证交给身份证持有者(ClaimsPrincipal)持有。接下来进入真正的认证阶段,根据配置的认证方案(IAuthenticationScheme),使用相对应的认证处理器(IAuthenticationHandler)进行认证 。认证成功后发放授权令牌(AuthorizationToken)。该授权令牌包含后续授权阶段需要的全部信息。

    授权流程简介

    授权就是对于用户身份信息(Claims)的验证,,授权又分以下几种种:

    1. 基于Role的授权
    2. 基于Scheme的授权
    3. 基于Policy的授权

    授权主要与以下几个核心对象打交道:

    1. IAuthorizationRequirement(授权条件)
    2. IAuthorizationService(授权服务)
    3. AuthorizationPolicy(授权策略)
    4. IAuthorizationHandler (授权处理器)
    5. AuthorizationResult(授权结果)

    那授权流程是怎样的呢?

    当收到授权请求后,由授权服务(IAuthorizationService)根据资源上指定的授权策略(AuthorizationPolicy)中包含的授权条件(IAuthorizationRequirement),找到相对应的授权处理器(IAuthorizationHandler )来判断授权令牌中包含的身份信息是否满足授权条件,并返回授权结果。

    核心对象

    中间件集成

    简单了解了下认证和授权流程后,我们来了解Identity microservice是如何集成相关中间件的。

    1. 首先是映射自定义扩展的User和Role

     // 映射自定义的User,Role
    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()//配置使用EF持久化存储
        .AddDefaultTokenProviders();//配置默认的TokenProvider用于变更密码和修改email时生成Token
    

    2. 配置IdentityServer服务

    // Adds IdentityServer
    services.AddIdentityServer(x =>
    {
        x.IssuerUri = "null";
        x.Authentication.CookieLifetime = TimeSpan.FromHours(2);
    })
    .AddSigningCredential(Certificate.Get())
    .AddAspNetIdentity<ApplicationUser>()
    .AddConfigurationStore(options =>
    {
        options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString,
         sqlServerOptionsAction: sqlOptions =>
         {
             sqlOptions.MigrationsAssembly(migrationsAssembly);
             //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency 
             sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
         });
    })
    .AddOperationalStore(options =>
    {
        options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString,
         sqlServerOptionsAction: sqlOptions =>
         {
             sqlOptions.MigrationsAssembly(migrationsAssembly);
             //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency 
             sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
         });
    })
    .Services.AddTransient<IProfileService, ProfileService>();
    

    IdentityServer默认直接在内存中存储配置数据(客户端和资源)和操作数据(令牌,代码和和用户的授权信息consents)。这显然在生产环境是不合适的,如果服务所在主机宕机,那么内存中的数据就会丢失,所以有必要持久化到数据库。
    其中AddConfigurationStoreAddOperationalStore扩展方法就是用来来指定配置数据和操作数据基于EF进行持久化。

    3. 添加IdentityServer中间件

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
         // .....
        // Adds IdentityServer
        app.UseIdentityServer();
    }
    

    4. 预置种子数据

    从已知的体系结构来说,我们需要预置Client和Resource:

    1. Client
    public static IEnumerable<Client> GetClients(Dictionary<string,string> clientsUrl)
    {
        return new List<Client>
        {
            // SPA OpenId Client Client(Implicit)
            new Client
            // Xamarin Client(Hybrid)
            new Client
            // MVC Client(Hybrid)
            new Client
            // MVC TEST Client(Hybrid)
            new Client
            // Locations Swagger UI(Implicit)
            new Client
            // Marketing Swagger UI(Implicit)
            new Client
            // Basket Swagger UI(Implicit)
            new Client
            // Ordering Swagger UI(Implicit)
            new Client
            // Mobile Shopping Aggregattor Swagger UI(Implicit)
            new Client
            // Web Shopping Aggregattor Swagger UI(Implicit)
            new Client
        };
    }
    
    1. IdentityResources
    public static IEnumerable<IdentityResource> GetResources()
    {
        return new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile()
        };
    }
    
    1. ApiResources
    public static IEnumerable<ApiResource> GetApis()
    {
        return new List<ApiResource>
        {
            new ApiResource("orders", "Orders Service"),
            new ApiResource("basket", "Basket Service"),
            new ApiResource("marketing", "Marketing Service"),
            new ApiResource("locations", "Locations Service"),
            new ApiResource("mobileshoppingagg", "Mobile Shopping Aggregator"),
            new ApiResource("webshoppingagg", "Web Shopping Aggregator"),
            new ApiResource("orders.signalrhub", "Ordering Signalr Hub")
        };
    }
    

    5. 迁移数据库上下文

    下面就把提前在代码预置的种子数据迁移到数据库中,我们如何做呢?IdentityServer为配置数据和操作数据分别定义了DBContext用于持久化,配置数据对应ConfigurationDbContext,操作数据对应PersistedGrantDbContext。代码如下所示:

    public static void Main(string[] args)
    {
        BuildWebHost(args)
            .MigrateDbContext<PersistedGrantDbContext>((_, __) => { })//迁移操作数据库
            .MigrateDbContext<ApplicationDbContext>((context, services) =>
            {
                var env = services.GetService<IHostingEnvironment>();
                var logger = services.GetService<ILogger<ApplicationDbContextSeed>>();
                var settings = services.GetService<IOptions<AppSettings>>();
    
                new ApplicationDbContextSeed()
                    .SeedAsync(context, env, logger, settings)
                    .Wait();
            })//迁移用户数据库
            .MigrateDbContext<ConfigurationDbContext>((context,services)=> 
            {
                var configuration = services.GetService<IConfiguration>();
    
                new ConfigurationDbContextSeed()
                    .SeedAsync(context, configuration)
                    .Wait();
            })//迁移配置数据库
            .Run();
    }
    

    至此,本服务的核心代码已解析完毕。

    最终的生成的数据库如下图所示:


    IdentityDb

    最后

    本文从业务和技术上对本服务进行剖析,介绍了其技术选型,并紧接着简要介绍了ASP.NET Core Identity和IdentityServer4,最后分析源码,一步步揭开其神秘的面纱。至于客户端和其他微服务服务如何使用Identity microservice进行认证和授权,我将在后续文章再行讲解。

    如果对ASP.NET Core Idenity和IdentityServer4不太了解,建议大家博客园阅读雨夜朦胧晓晨MasterSavorboard
    的博客进行系统学习后,再重读本文,相信你对Identity microservice的实现机制豁然开朗。

    参考资料

    雨夜朦胧 -- ASP.NET Core 认证与授权:初识认证/授权
    Savorboard -- ASP.NET Core 之 Identity 入门(一)
    晓晨Master -- IdentityServer(14)- 通过EntityFramework Core持久化配置和操作数据
    IdentityServer4 知多少
    OAuth2.0 知多少

    相关文章

      网友评论

        本文标题:eShopOnContainers 知多少[3]:Identit

        本文链接:https://www.haomeiwen.com/subject/rsfoaftx.html