1. 尝试新的开发组合:Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS
  2. Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之配置IdentityServer
  3. Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之数据迁移
  4. Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之添加实体

上一篇文章把项目建好了,现在要进行一些必要配置。由于IdentityServer4需要使用到3.2.5的包,而项目默认使用的是3.2.4的包,因而,我们需要先升级一下解决方案的包。在解决方案上单击右键,在右键菜单中选择管理解决方案的NuGet包。在NuGet的标签页内,切换到更新标签页,会看到6个更新,选择全部包,然后单击更新按钮。

包更新完以后,我们先做一次数据库迁移。在EntityFrameworkCore项目中添加Pomelo.EntityFrameworkCore.MySql和Pomelo.EntityFrameworkCore.MySql.Design两个包,以便支持MySql。为什么不用Oracle提供的MySql.Data.EntityFrameworkCore呢?在微软的文档《MySQL EF Core Database Provider》中,该包只支持MySql,且还是预览版状态,而在Pomelo EF Core Database Provider for MySQL中,该包不单支持MySQL,还支持MariaDB,所以,怎么选择已经很明显,可以说,笔者是毫不犹豫的选择了Pomelo包。至于两者的性能,笔者目前无法给出答案,只有等大神们的测试了。

至于测试数据库,笔者选择的是XAMMP带的MySql,版本是10.1.13-MariaDB。数据库所选的字符集为utf8_general_ci。虽然Pomelo建议选择utf8mb4作为字符集(详细信息请参阅Pomelo《Getting Started》),但建议不要选择,因为“max key length is 767 bytes”这个错误会把你折磨死的。出现这个错误的原因是utf8以3字节方式存储一个字符的,而一列的索引长度最大值只能是767字节,如果字段的长度是256,那么,索引的长度就是768了,正好超过1字节,迁移就出问题了。如果使用utf8mb4,那么,一个字符的长度就是4字节,字段的最大长度只能是191了,而这要修改的字段就有很多了,所以在这练习了还是采用utf8算了。在实际项目中,建议使用utf8mb4,但要考虑带索引的字段的长度是191是否足够。要想彻底解决这个问题,需要将字段的索引扩大到3072字段,但实现这个需要执行以下4个步骤:

  • SET GLOBAL innodb_file_format=Barracuda;
  • SET GLOBAL innodb_file_per_table=ON;
  • ROW_FORMAT=DYNAMIC; – or COMPRESSED (goes on end of CREATE)
  • innodb_large_prefix=1

以上4个步骤,1、2和4都可以在数据库中直接修改。第3步则有点难度了,因为实现这个需要在创建表格时在CREATE语句中添加附加信息,而这个在迁移代码中暂时没找到解决方案,除非是使用Database First的方式来实现。

MySql的驱动准备好以后,需要修改数据库链接,打开Migrations项目和Web.Host项目中的appsettings.json文件,将ConnectionStrings的Default值修改为“Server=localhost;database=simplecmswithabp;uid=root;pwd=abcd-1234;charset=UTF8;”。在生产环境中,不要使用root作为数据库的连接用户,这个大家应该都懂的。

修改数据库连接后,还需要修改EntityFrameworkCore项目中的SimpleCmsWithAbpDbContextConfigurer.cs文件,将方法中的UseSqlServer方法修改为UseMySql方法,完成后的代码如下:

    public static class SimpleCmsWithAbpDbContextConfigurer
    {
        public static void Configure(DbContextOptionsBuilder<SimpleCmsWithAbpDbContext> builder, string connectionString)
        {
            //builder.UseSqlServer(connectionString);
            builder.UseMySql(connectionString);           
        }

        public static void Configure(DbContextOptionsBuilder<SimpleCmsWithAbpDbContext> builder, DbConnection connection)
        {
            //builder.UseSqlServer(connection);
            builder.UseMySql(connection);
        }
    }

下一步要修改字段长度为256,且需要创建索引的字段的长度。打开20170424115119_Initial_Migrations.cs文件,然后使用搜索功能寻找256,经过一轮搜索和索引字段对比后,最终需要修改的字段包括:

  • AbpLanguageTexts表:Key
  • AbpUsers表:EmailAddress和NormalizedEmailAddress
  • AbpUserLogins表:ProviderKey
  • AbpSettings表:Name

长度修改为255就行了。如果使用utf8mb4,需要修改为191。字段修改完成后,将Migrator项目设置为启动项目,然后运行,会看到如下图所示的结果:

迁移数据库

在窗口中输入y,等待以后后出现以下信息,说明迁移已经完成了,按回车退出程序。

2017-12-16 09:54:29 | HOST database migration started...
2017-12-16 09:54:34 | HOST database migration completed.
2017-12-16 09:54:34 | --------------------------------------------------------
2017-12-16 09:54:34 | All databases have been migrated.
Press ENTER to exit...

使用数据库工具打开数据库,会看到下图所示的表格:

数据库

包更新完以后,我们就可以根据《Identity Server Integration》一文来配置IdentityServer了。

首先要做的是在EntityFrameworkCore项目中添加 Abp.ZeroCore.IdentityServer4和 Abp.ZeroCore.IdentityServer4.EntityFrameworkCore的引用。然后打开SimpleCmsWithAbpEntityFrameworkModule.cs文件,在类定义上添加依赖项,具体代码如下:

    [DependsOn(
        typeof(SimpleCmsWithAbpCoreModule), 
        typeof(AbpZeroCoreEntityFrameworkCoreModule),
        typeof(AbpZeroCoreIdentityServerEntityFrameworkCoreModule))]
    public class SimpleCmsWithAbpEntityFrameworkModule : AbpModule
    ……

下面要配置Startup Class,打开SimpleCmsWithAbp.Web.Host项目下Startup文件的Startup.cs文件,在IdentityRegistrar.Register(services)这句下添加以下代码:

            services.AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddInMemoryIdentityResources(IdentityServerConfig.GetIdentityResources())
                .AddInMemoryApiResources(IdentityServerConfig.GetApiResources())
                .AddInMemoryClients(IdentityServerConfig.GetClients())
                .AddAbpPersistedGrants<IAbpPersistedGrantDbContext>()
                .AddAbpIdentityServer<User>();

在Configure方法内,注释掉app.UseAuthentication这句,因为UseIdentityServer方法会自动调用UseAuthentication方法。在app.UseJwtTokenMiddleware下

app.UseIdentityServer();

为什么不像文章中那样在调用UseJwtTokenMiddleware方法时将IdentityBearer作为参数呢?主要原因是这个参数是在授权客户端与应用程序是同一应用程序才使用,而我们预想的情况是在不同应用程序,因而不用采用这个参数。

如果打算使用内存来提供资源,可以参考文章在SimpleCmsWithAbp.Web.Host项目中添加一个名为IdentityServerConfig的静态类,用来指定资源,代码请参考《Identity Server Integration》一文。如果打算使用数据库来管理资源,可以忽略这一步。

下面要做的是SimpleCmsWithAbpDbContext添加IAbpPersistedGrantDbContext接口。切换到EntityFrameworkCore项目,打开SimpleCmsWithAbpDbContext.cs文件,在类定义的后面添加IAbpPersistedGrantDbContext接口,以实现授权操作:

public class SimpleCmsWithAbpDbContext : AbpZeroDbContext<Tenant, Role, User, SimpleCmsWithAbpDbContext>, IAbpPersistedGrantDbContext

添加IAbpPersistedGrantDbContext接口后,需要在类内实现接口功能,代码如下:

    public DbSet<PersistedGrantEntity> PersistedGrants { get; set; }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.ConfigurePersistedGrantEntity();
    }

好了,IdentityServer的配置基本完成了,但在《Identity Server Integration》一文中,资源的定义使用的是内存定义模式(AddInMemoryIdentityResources),对于项目来说,这不便于资源的管理,因而,一般会通过数据库来实现管理,而要实现这个,就要参考Using EntityFramework Core for configuration and operational data一文来实现。

本来打算从IConfigurationDbContext接口派生,将ConfigurationDbContext合并到SimpleCmsWithAbpDbContext,但水平有限,尝试了不少方法都不行,只能按照文章的步骤的去实现了。先在EntityFrameworkCore项目添加IdentityServer4.EntityFramework包,再将以上调用AddIdentityServer方法的代码替换为以下代码:

            var connectionString = _appConfiguration.GetConnectionString("Default");
            services.AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddConfigurationStore(options =>
                {
                    options.ConfigureDbContext = builder =>
                        builder.UseMySql(connectionString, sql => sql.MigrationsAssembly("SimpleCmsWithAbp.EntityFrameworkCore"));
                })
                //.AddInMemoryIdentityResources(IdentityServerConfig.GetIdentityResources())
                //.AddInMemoryApiResources(IdentityServerConfig.GetApiResources())
                //.AddInMemoryClients(IdentityServerConfig.GetClients())
                .AddAbpPersistedGrants<IAbpPersistedGrantDbContext>()
                .AddAbpIdentityServer<User>(); 

代码添加了AddConfigurationStore方法来配置ConfigurationDbContext,并注释掉了使用内存资源的代码。在ConfigurationDbContext方法内,指定了ConfigurationDbContext将使用MySql数据库,而它的迁移模块在EntityFrameworkCore项目内。

接下来要添加迁移,我们不需要采用文章所介绍的方法。先将Web.Host项目设置为启动项目,不然会出现“No DbContext named ‘ConfigurationDbContext’ was found”的错误,,原先是迁移需要根据上下文来寻找Context。在IDE中,切换到程序包管理器控制器,将默认项目设置为SimpleCmsWithAbp.Web.Host,然后输入以下命名:

Add-Migration -Name InitialIdentityServerConfigurationDbMigration -Context ConfigurationDbContext  -OutputDir Migrations/IdentityServer/ConfigurationDb -Project SimpleCmsWithAbp.EntityFrameworkCore

命令将使用ConfigurationDbContext创建一个名为InitialIdentityServerConfigurationDbMigration 的迁移。迁移文件将输出到SimpleCmsWithAbp.EntityFrameworkCore项目的Migrations/IdentityServer/ConfigurationDb文件夹。

命令执行后,会在EntityFrameworkCore项目的Migrations文件夹内看到下图所示文件夹和文件:

迁移文件

余下的工作是初始化数据库,这里也不采用文章所使用的方法,将参考EntityFrameworkCore项目中的迁移方式来实现。先要做的是在EntityFrameworkCore\Seed文件夹下创建IdentityServer文件夹,并添加以下4个类文件:

  • DefaultApiResourceCreator.cs
    public class DefaultApiResourceCreator
    {
        private readonly ConfigurationDbContext _context;

        public DefaultApiResourceCreator(ConfigurationDbContext context)
        {
            _context = context;
        }

        public void Create()
        {
            CreateDefaultApiResource();
        }

        private void CreateDefaultApiResource()
        {
            if (_context.ApiResources.Any()) return;
            var defaultApiResource = new ApiResource() { Name = "default-api", DisplayName = "Default (all) API" };
            _context.ApiResources.Add(defaultApiResource);
            _context.SaveChanges();
        }
    }
  • DefaultClientCreator.cs
    public class DefaultClentCreator
    {
        private readonly ConfigurationDbContext _context;

        public DefaultClentCreator(ConfigurationDbContext context)
        {
            _context = context;
        }

        public void Create()
        {
            CreateDefaultClient();
        }

        private void CreateDefaultClient()
        {
            if (_context.Clients.Any()) return;
            foreach (var client in GetClients())
            {
                _context.Clients.Add(client.ToEntity());
            }
            _context.SaveChanges();
        }

        private static IEnumerable<IdentityServer4.Models.Client> GetClients() => new List<IdentityServer4.Models.Client>
            {
                new IdentityServer4.Models.Client
                {
                    ClientId = "client",
                    AllowedGrantTypes =
                    {
                        GrantType.ResourceOwnerPassword,
                        GrantType.ClientCredentials
                    },
                    AllowedScopes = {"default-api"},
                    ClientSecrets =
                    {
                        new IdentityServer4.Models.Secret("secret".Sha256())
                    }
                }
            };
    }
  • DefaultIdentityResourceCreator.cs
    public class DefaultIdentityResourceCreator
    {
        private readonly ConfigurationDbContext _context;

        public DefaultIdentityResourceCreator(ConfigurationDbContext context)
        {
            _context = context;
        }

        public void Create()
        {
            CreateDefaultIdentityResource();
        }

        private void CreateDefaultIdentityResource()
        {
            if (_context.IdentityResources.Any()) return;
            foreach (var resource in GetIdentityResource())
            {
                _context.IdentityResources.Add(resource.ToEntity());
            }
            _context.SaveChanges();
        }

        private static IEnumerable<IdentityServer4.Models.IdentityResource> GetIdentityResource()
        {
            return new List<IdentityServer4.Models.IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Email(),
                new IdentityResources.Phone()
            };

        }
    }
  • InitialIConfigurationDbBuilder.cs
    public class InitialIConfigurationDbBuilder
    {
        private readonly ConfigurationDbContext _context;

        public InitialIConfigurationDbBuilder(ConfigurationDbContext context)
        {
            _context = context;
        }

        public void Create()
        {
            new DefaultApiResourceCreator(_context).Create();
            new DefaultIdentityResourceCreator(_context).Create();
            new DefaultClentCreator(_context).Create();

            _context.SaveChanges();
        }
    }

这4个类添加完成后,在SeedHelper类内添加以下两个方法用来初始化资源:

        public static void SeedIdentityServerDb(IIocResolver iocResolver)
        {
            WithDbContext<ConfigurationDbContext>(iocResolver, SeedIdentityServerDb);
        }

        public static void SeedIdentityServerDb(ConfigurationDbContext context)
        {
            new InitialIConfigurationDbBuilder(context).Create();
        }

在EntityFrameworkCore项目的EntityFrameworkCore文件夹内,参考AbpZeroDbMigrator创建一个AbpZeroIdentityServerDbMigrator类用来迁移配置库,具体代码如下:

    public class AbpZeroIdentityServerDbMigrator : AbpZeroDbMigrator<ConfigurationDbContext>
    {
        public AbpZeroIdentityServerDbMigrator(
            IUnitOfWorkManager unitOfWorkManager,
            IDbPerTenantConnectionStringResolver connectionStringResolver,
            IDbContextResolver dbContextResolver)
            : base(
                unitOfWorkManager,
                connectionStringResolver,
                dbContextResolver)
        {
        }
    }

AbpZeroIdentityServerDbMigrator与AbpZeroDbMigrator主要的不同在派生于AbpZeroDbMigrator时,实体类是ConfigurationDbContext而不是SimpleCmsWithAbpDbContext。

试了一下看能不能在Migrator项目内迁移数据库,但老是出现“No component for supporting the service IdentityServer4.EntityFramework.DbContexts.ConfigurationDbContext was found”的错误,放弃了,还是在Start类内初始化吧。在Configure方法内,app.UseAbp代码下,添加以下迁移代码:

            using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
            {
                var migrator = serviceScope.ServiceProvider.GetRequiredService<AbpZeroIdentityServerDbMigrator>();
                migrator.CreateOrMigrateForHost(SeedHelper.SeedIdentityServerDb);

            }

IdentiyServer现在基本上是配置好了,但要使应用程序跑起来,还得修改个地方,切换到Web.Core项目,打开Controllers文件夹下的TokenAuthController.cs文件,找到215行以下这句:

var nameIdClaim = claims.First(c => c.Type == ClaimTypes.NameIdentifier);

将它修改为:

var nameIdClaim = claims.First(c => c.Type == AbpClaimTypes.UserId);

使用ClaimTypes.NameIdentifier作为比较,nameIdClaim 永远返回空的列表,导致不能生成token,这应该是ABP的bug。修改后就可以使用postman来测试了。

打开postman,在地址栏输入地址:http://localhost:21021/api/TokenAuth/Authenticate。提交方式选择POST。在Headers中输入以下两个参数:
- Content-Type:application/json
- Abp.TenantId:1

在body中输入以下数据:

{
  "userNameOrEmailAddress": "admin@defaulttenant.com",
  "password": "123qwe",
  "rememberClient": false
}

单击Send按钮后,会看到如下图所示输出:

登录

将图中的accessToken复制出来。新建一个页面标签,提交方式设置为GET,地址输入:http://localhost:21021/api/services/app/User/GetAll?SkipCount=0&MaxResultCount=100。在Headers中,在Key中输入Authorization,在value中,先输入Bearer加一个空格,然后在空格后把刚才复制的accessToken粘贴在后面。再加上Abp.TenantId后,单击Send,将看到如下图所示的结果:

从图中可以看到,通过accessToken可以顺利获取数据了,这说明IdentityServer运行正常。

获取用户列表

今天就说到这里了。

Logo

更多推荐