Fork me on GitHub

ABP-Zero模块

一、介绍

二、启动模版

三、功能

  1,租户管理

  2,版本管理

  3,用户管理

  4,角色管理

  5,组织单位管理

  6,权限管理

  7,语言管理

  8,Identity Server集成

 

一、介绍

1,Zero模块实现ASP.NET Boilerplate框架的所有基本概念。如:

租户管理(多租户)、角色管理、用户管理、session、授权(权限管理)、设置管理、语言管理、审计管理

2,Microsoft ASP.NET Identity模块有2个版本:

  • Abp.Zero.* 软件包基于Microsoft ASP.NET身份和EF6.x.
  • Abp.ZeroCore.* 软件包基于Microsoft ASP.NET Core身份和Entity Framework Core。 这些软件包也支持.net内核。

二、启动模版

注意:确保您已经为您的Visual Studio安装了Typescript 2.0+,因为Abp.Web.Resources nuget包附带d.ts,它需要Typescript 2.0+。

1,基于令牌的认证

启动模板使用基于cookie的浏览器身份验证。 但是,如果要从移动应用程序中使用Web API或应用程序服务(通过动态Web api公开),则可能需要基于令牌的身份验证机制。 启动模板包括承载令牌认证基础设施。 .WebApi项目中的AccountController包含用于获取令牌的Authenticate操作。 然后我们可以使用令牌进行下一个请求。

①认证

只需发送POST请求到http://localhost:6334/api/Account/Authenticate和 Context-Type="application/json"头,如下所示:

我们发送了一个JSON请求体,其中包含userNameOrEmailAddress和密码。 另外,应该为租户用户发送TenancyName。 如上所述,返回JSON的result属性包含令牌。 我们可以保存并用于下一个请求。

②使用API

在认证和获取令牌之后,我们可以使用它来调用任何授权的操作。 所有应用程序服务都可以远程使用。 例如,我们可以使用租户服务来获取租户列表: 

 

只需向http://localhost:6334/api/services/app/tenant/GetTenants发送POST请求到 Content-Type="application/json"Authorization="Bearer your-auth-token "。 请求正文只是空的{}。 当然,请求和响应机构对于不同的API将是不同的。

UI上几乎所有可用的操作也可用作Web API(由于UI使用相同的Web API),并且可以轻松地使用。

2,Migrator 控制台应用程序

启动模板包含一个工具Migrator.exe,可轻松迁移数据库。 您可以运行此应用程序来创建/迁移主机和租户数据库。

此应用程序从它自己的.config文件获取主机连接字符串。 在开头的web.config中将是一样的。 确保配置文件中的连接字符串是您想要的数据库。 获取主机连接后,首先创建主机数据库或应用迁移(如果它已经存在)。 然后,它获取租户数据库的连接字符串,并为这些数据库运行迁移。 如果没有一个专用数据库,或者它的数据库已经迁移到另一个租户(用于多个租户之间的共享数据库),它会跳过租户。

您可以在开发或产品环境中使用此工具来迁移部署时的数据库,而不是EntityFramework自己的Migrate.exe(这需要一些配置,并且可以在一次运行中为单个数据库工作)。

3,单元测试
启动模板包括测试基础架构设置和.Test项目下的一些测试。 您可以检查它们并轻松编写类似的测试。 实际上,它们是集成测试而不是单元测试,因为它们使用所有ASP.NET Boilerplate基础架构(包括验证,授权,工作单元...)测试代码。

三、租户管理

1,启用多租户
ASP.NET Boilerplate和Zero模块可以运行多租户或单租户模式。 默认情况下禁用多租户。 我们可以在我们的模块的PreInitialize方法中启用它,如下所示:

[DependsOn(typeof(AbpZeroCoreModule))]
public class MyCoreModule : AbpModule
{
    public override void PreInitialize()
    {
        Configuration.MultiTenancy.IsEnabled = true;    
    }

    ...
}

当我们创建一个基于ASP.NET Boilerplate和Zero模块的项目模板时,我们有一个Tenant实体和TenantManager领域服务。

2,租户实体

租户实体代表申请的租户。

public class Tenant : AbpTenant<Tenant, User>
{

}

它源于通用的AbpTenant类。 租户实体存储在数据库中的AbpTenants表中。 您可以将自定义属性添加到Tenant类。

AbpTenant类定义了一些基本属性,大多数重要的是:

  • TenancyName:租户名称,唯一。不能正常改变 它可以用来为“mytenant.mydomain.com”这样的租户分配子域名。 Tenant.TenancyNameRegex常量定义命名规则。
  • Name: 任意可读的名称
  • IsActive:true:这个租户可以使用该应用程序;false:该租户的用户不能登录到系统

AbpTenant类继承自FullAuditedEntity。 这意味着它具有创建,修改和删除审计属性。 它也是软删除。 所以,当我们删除租户时,它不会从数据库中删除,只是被标记为已删除。

最后,AbpTenant的Id被定义为int。

3,租户管理

租户管理是为租户执行领域逻辑的服务

public class TenantManager : AbpTenantManager<Tenant, Role, User>
{
    public TenantManager(IRepository<Tenant> tenantRepository)
        : base(tenantRepository)
    {

    }
}

TenantManager也用于管理租户功能。 你可以在这里添加你自己的方法。 此外,您可以根据自己的需要覆盖任何AbpTenantManager基类的方法。

4,默认租户
ASP.NET Boilerplate和Zero模块假设有一个预定义的租户,TenancyName为“Default”,Id为1.在单租户应用程序中,与租户一样使用。 在多租户应用程序中,您可以将其删除或将其设为被动。

 

四、版本管理

大多数SaaS(多租户)应用程序都有具有不同功能的版本(包)。 因此,他们可以向租户(客户)提供不同的价格和功能选项。

1,版本实体(Edition Entity)

版本是一个简单的实体代表应用程序的一个版本(或包)。 它只有Name和DisplayName属性。

2,版本管理

public class EditionManager : AbpEditionManager
{
}

它来自AbpEditionManager类。 您可以注入并使用EditionManager来创建,删除和更新版本。 此外,EditionManager用于管理版本的功能。 它内部缓存版本功能以获得更好的性能。

 

五、用户管理

1,用户实体

用户实体表示应用程序的用户。 它应该从AbpUser类派生,如下所示:

public class User : AbpUser<Tenant, User>
{
    //在这里添加您自己的用户属性
}

此类在您安装模块零时创建。 用户存储在数据库中的AbpUsers表中。 您可以将自定义属性添加到User类(并为更改创建数据库迁移)。

AbpUser类定义了一些基本属性。 一些属性是:

  • UserName: 用户的登录名对于租户应该是唯一的。
  • EmailAddress: 用户的电子邮件地址。 对于租户来说应该是独一无二的。
  • Password: 用户的密码。
  • IsActive:true:用户可以登录到系统
  • Name(用户姓名) 和 Surname(姓氏)

还有一些属性,如角色(Roles)、权限(Permissions)、租户(Tenant)、设置(Settings)、IsEmailConfirmed(邮箱是否确认)、

AbpUser类继承自FullAuditedEntity。 这意味着它具有创建,修改和删除审计属性。 它也是软删除。 所以,当我们删除一个用户时,它不会从数据库中删除,只是被标记为已删除。

AbpUser类实现了IMayHaveTenant过滤器,以便在多租户应用程序中正常工作。

最后,用户的ID被定义为long。

2,用户管理

UserManager是为用户执行领域逻辑的服务:

public class UserManager : AbpUserManager<Tenant, Role, User>
{
    //...
}

您可以注入并使用UserManager来创建,删除,更新用户,授予权限,为用户更改角色等等。 你可以在这里添加你自己的方法。 此外,您可以根据自己的需要覆盖任何AbpUserManager基类的方法。

①多租户

UserManager旨在一次为单个租户工作。 它适用于当前租户的默认值。 让我们看看UserManager的一些用法:

public class MyTestAppService : ApplicationService
{
    private readonly UserManager _userManager;

    public MyTestAppService(UserManager userManager)
    {
        _userManager = userManager;
    }

    public void TestMethod_1()
    {
        //通过电子邮件寻找当前租户的用户
        var user = _userManager.FindByEmail("sampleuser@aspnetboilerplate.com");
    }

    public void TestMethod_2()
    {
        //切换到租户42
        CurrentUnitOfWork.SetFilterParameter(AbpDataFilters.MayHaveTenant, AbpDataFilters.Parameters.TenantId, 42);

        //通过电子邮件找到租户42的一个用户
        var user = _userManager.FindByEmail("sampleuser@aspnetboilerplate.com");
    }

    public void TestMethod_3()
    {
        //禁用MayHaveTenant过滤器,所以我们可以覆盖所有用户
        using (CurrentUnitOfWork.DisableFilter(AbpDataFilters.MayHaveTenant))
        {
            //现在,我们可以在所有租户中搜索用户名
            var users = _userManager.Users.Where(u => u.UserName == "sampleuser").ToList();

            //或者我们可以添加TenantId过滤器,如果我们要搜索一个特定的租户
            var user = _userManager.Users.FirstOrDefault(u => u.TenantId == 42 && u.UserName == "sampleuser");
        }
    }
}

②用户登录

Zero模块定义了LoginManager,它具有用于登录应用程序的LoginAsync方法。 它检查所有用于登录的逻辑,并返回登录结果。 LoginAsync方法还会自动将所有登录尝试保存到数据库(即使是尝试失败)。 您可以使用UserLoginAttempt实体进行查询。

③关于IdentityResults

UserManager的一些方法返回IdentityResult,而不是在某些情况下抛出异常。 这是ASP.NET Identity Framework的本质。 Zero模块也随之而来。 所以,我们应该检查这个返回的结果对象来知道操作是否成功。

Zero模块定义了CheckErrors扩展方法,如果需要,可自动检查错误并抛出异常(本地化的UserFriendlyException)。 使用示例

(await UserManager.CreateAsync(user)).CheckErrors();

要获得本地化的例外,我们应该提供一个ILocalizationManager实例:

(await UserManager.CreateAsync(user)).CheckErrors(LocalizationManager);

3,外部认证

Zero模块登录方式从数据库中的AbpUsers表中认证用户。 一些应用程序可能需要从一些外部来源(如活动目录,从另一个数据库的表甚至远程服务)来验证用户。

对于这种情况,UserManager定义了一个名为“外部认证来源”的扩展点。 我们可以创建一个派生自IExternalAuthenticationSource的类并注册到配置。 有一个DefaultExternalAuthenticationSource类来简化IExternalAuthenticationSource的实现。 我们来看一个例子:

public class MyExternalAuthSource : DefaultExternalAuthenticationSource<Tenant, User>
{
    public override string Name
    {
        get { return "MyCustomSource"; }
    }

    public override Task<bool> TryAuthenticateAsync(string userNameOrEmailAddress, string plainPassword, Tenant tenant)
    {
        //TODO:验证用户并返回true或false
    }
}

在TryAuthenticateAsync方法中,我们可以从某些来源检查用户名和密码,如果给定用户由此源进行身份验证,则返回true。 此外,我们可以覆盖CreateUser和UpdateUser方法来控制用户创建和更新此源。

当用户通过外部源进行身份验证时,Zero模块检查该用户是否存在于数据库(AbpUsers表)中。 如果没有,它调用CreateUser来创建用户,否则调用UpdateUser来允许身份验证源来更新现有的用户信息。

我们可以在应用程序中定义多个外部认证源。 AbpUser实体具有AuthenticationSource属性,该属性显示哪个源验证了该用户。

要注册我们的认证来源,我们可以在我们的模块的PreInitialize中使用这样的代码:

Configuration.Modules.Zero().UserManagement.ExternalAuthenticationSources.Add<MyExternalAuthSource>();

①LDAP/Active Directory

LdapAuthenticationSource是外部身份验证的一种实现,使用户可以使用其LDAP(活动目录)用户名和密码登录。

如果我们要使用LDAP认证,我们首先将Abp.Zero.Ldap nuget包添加到我们的项目(通常为Core(domain)项目)。 那么我们应该为我们的应用程序扩展LdapAuthenticationSource,如下所示:

public class MyLdapAuthenticationSource : LdapAuthenticationSource<Tenant, User>
{
    public MyLdapAuthenticationSource(ILdapSettings settings, IAbpZeroLdapModuleConfig ldapModuleConfig)
        : base(settings, ldapModuleConfig)
    {
    }
}

最后,我们应该将模块依赖关系设置为AbpZeroLdapModule,并使用上面创建的auth源启用LDAP:

[DependsOn(typeof(AbpZeroLdapModule))]
public class MyApplicationCoreModule : AbpModule
{
    public override void PreInitialize()
    {
        Configuration.Modules.ZeroLdap().Enable(typeof (MyLdapAuthenticationSource));    
    }

    ...
}

在这些步骤之后,将为您的应用程序启用LDAP模块。 但默认情况下,LDAP认证未启用。 我们可以使用设置启用它。

1)设置

LdapSettingNames类定义了设置名称的常量。 您可以在更改设置(或获取设置)时使用这些常量名称。 LDAP设置是每个租户(对于多租户应用程序)。 因此,不同的租户具有不同的设置(请参阅在github上设置定义)。

您可以在MyLdapAuthenticationSource构造函数中看到,LdapAuthenticationSource期望ILdapSettings作为构造函数参数。 此界面用于获取LDAP设置,如域,用户名和密码以连接到Active Directory。 默认实现(LdapSettings类)从设置管理器获取这些设置。

如果您使用设置管理器,那么没有问题。 您可以使用设置管理器API更改LDAP设置。 如果需要,您可以将初始/种子数据添加到数据库,默认情况下启用LDAP验证。

注意:如果您不定义域,用户名和密码,LDAP认证适用于当前域,如果应用程序在具有适当权限的域中运行。

2)自定义设置

如果要定义另一个设置源,可以实现自定义ILdapSettings类,如下所示:

public class MyLdapSettings : ILdapSettings
{
    public async Task<bool> GetIsEnabled(int? tenantId)
    {
        return true;
    }

    public async Task<ContextType> GetContextType(int? tenantId)
    {
        return ContextType.Domain;
    }

    public async Task<string> GetContainer(int? tenantId)
    {
        return null;
    }

    public async Task<string> GetDomain(int? tenantId)
    {
        return null;
    }

    public async Task<string> GetUserName(int? tenantId)
    {
        return null;
    }

    public async Task<string> GetPassword(int? tenantId)
    {
        return null;
    }
}

并在您的模块的PreInitialize中注册到IOC:

[DependsOn(typeof(AbpZeroLdapModule))]
public class MyApplicationCoreModule : AbpModule
{
    public override void PreInitialize()
    {
        IocManager.Register<ILdapSettings, MyLdapSettings>(); //更改默认设置源
        Configuration.Modules.ZeroLdap().Enable(typeof (MyLdapAuthenticationSource));
    }

    ...
}

然后,您可以从任何其他来源获取LDAP设置。

 

六、角色管理

1,角色实体(Role Entity)

角色实体代表应用程序的角色。 它应该从AbpRole类派生,如下所示:

public class Role : AbpRole<Tenant, User>
{
    //在这里添加你自己的角色属性
}

此类在您安装Zero模块时创建。 角色存储在数据库中的AbpRoles表中。 您可以将自定义属性添加到Role类(并为更改创建数据库迁移)。

AbpRole定义了一些属性:

  • Name: 租户角色的独特名称。
  • DisplayName: 显示名称的角色。
  • IsDefault:这个角色是否默认分配给新用户?
  • IsStatic: 这个角色是静态的(预构建,不能被删除)。

角色用于分组权限。 当用户有角色时,他/她将具有该角色的所有权限。 用户可以有多个角色。 该用户的权限将是所有分配角色的所有权限的合并。

2,动态vs静态角色

在模块零中,角色可以是动态的或静态的:

  • Static role: 静态角色有一个已知的名称(如“admin”),不能更改此名称(我们可以更改显示名称)。 它存在于系统启动,无法删除。 因此,我们可以根据静态角色名称编写代码。
  • Dynamic (non static) role: 我们可以在部署后创建动态角色。 然后我们可以授予该角色的权限,我们可以将角色分配给某些用户,我们可以将其删除。 在开发时期,我们无法知道动态角色的名称。

使用IsStatic属性为角色设置它。 此外,我们应该在我们的模块的PreInitialize上注册静态角色。 假设我们对租户有一个“Admin”静态角色:

Configuration.Modules.Zero().RoleManagement.StaticRoles.Add(new StaticRoleDefinition("Admin", MultiTenancySides.Tenant));

因此,Zero模块将意识到静态角色。

3,默认角色

一个或多个角色可以设置为默认。 默认角色默认分配给新添加/注册的用户。 这不是开发时间属性,可以在部署后设置或更改。 使用IsDefault属性进行设置。

4,角色管理

public class RoleManager : AbpRoleManager<Tenant, Role, User>
{
    //...
}

您可以注入并使用RoleManager来创建,删除,更新角色,授予角色权限等等。 你可以在这里添加你自己的方法。 此外,您可以根据自己的需要覆盖任何AbpRoleManager基类的方法。

像UserManager一样,RoleManager的一些方法也返回IdentityResult,而不是在某些情况下抛出异常。 有关详细信息,请参阅用户管理文档

 

七、组织单位管理

组织单位(OU)可用于对用户和实体进行分层分组。

1,OrganizationUnit实体

OU由OrganizationUnit实体表示。 这个实体的基本属性是:

  • TenantId: 租户的这个OU的ID。 主机OU可以为空.
  • ParentId: 父OU的ID。 如果这是根OU,则可以为null.
  • Code:租户独有的层次化字符串代码.
  • DisplayName: 显示OU的名称.

OrganizationUnit entitiy的主键(id)为long类型,它源自FullAuditedEntity,它提供审计信息并实现ISoftDelete界面(因此,OU不会从数据库中删除,它们只被标记为已删除)

2,组织树
由于OU可以拥有父级,所以租户的所有OU都是树形结构。 这棵树有一些规则:

  • 可以有多个根(它们具有null ParentId)。
  • 最大深度树被定义为一个常量作为OrganizationUnit.MaxDepth,它是16.
  • 存在用于第一级子计数的OU的(因为固定OU代码单位长度在下面解释)的限制.

2,OU Code

OU代码由OrganizationUnit Manager自动生成和维护。 这是一个字符串,如:

"00001.00042.00005"

该代码可用于轻松查询OU的所有子项(递归)的数据库。 这段代码有一些规则:

  • 这对租户来说是独一无二的.
  • 同一个OU的所有子代都有父OU的代码开头的代码.
  • 它是基于树中OU的级别的固定长度,如示例所示.
  • 当OU代码是唯一的,如果您移动OU,它可以是可更改的。 因此,我们应该通过Id引用OU,而不是Code.

3,OrganizationUnit 管理

可以注入OrganizationUnitManager类并用于管理OU。 常见用例有:

  • 创建,更新或删除OU
  • 在OU树中移动OU.
  • 获取关于OU树和项目的信息.

4,多租户

OrganizationUnitManager旨在一次为单个租户工作。 它适用于当前租户的默认值。

5,常用案例

在这里,我们将看到OU的常见用例。 您可以在这里找到样品的源代码。

6,为组织单位创建实体,
OU的最明显的使用是将实体分配给OU。 我们来看一个示例实体:

public class Product : Entity, IMustHaveTenant, IMustHaveOrganizationUnit
{
    public virtual int TenantId { get; set; }

    public virtual long OrganizationUnitId { get; set; }
    
    public virtual string Name { get; set; }

    public virtual float Price { get; set; }
}

我们简单地创建了OrganizationUnitId属性来将此实体分配给OU。 IMustHaveOrganizationUnit定义了OrganizationUnitId属性。 我们不必执行它,但建议提供标准化。 还有一个具有可空的OrganizationUnitId属性的IMayHaveOrganizationId。

现在,我们可以将产品与OU相关联并查询特定OU的产品。

注意; 产品实体有一个TenantId(它是IMustHaveTenant的属性),用于区分多租户应用中不同租户的产品(见多租户文档)。 如果您的应用程序不是多租户,则不需要此接口和属性。

7,获取组织单位中的实体

获取OU的产品很简单。 我们来看看这个示例域服务:

public class ProductManager : IDomainService
{
    private readonly IRepository<Product> _productRepository;

    public ProductManager(IRepository<Product> productRepository)
    {
        _productRepository = productRepository;
    }

    public List<Product> GetProductsInOu(long organizationUnitId)
    {
        return _productRepository.GetAllList(p => p.OrganizationUnitId == organizationUnitId);
    }
                
}

我们可以简单地写一个反对Product.OrganizationUnitId的谓词,如上所示。

8,在组织单位中获得实体,包括其子单位单位
我们可能想要获得包含子组织单位的组织单位的产品。 在这种情况下,OU代码可以帮助我们:

public class ProductManager : IDomainService
{
    private readonly IRepository<Product> _productRepository;
    private readonly IRepository<OrganizationUnit, long> _organizationUnitRepository;

    public ProductManager(
        IRepository<Product> productRepository, 
        IRepository<OrganizationUnit, long> organizationUnitRepository)
    {
        _productRepository = productRepository;
        _organizationUnitRepository = organizationUnitRepository;
    }

    [UnitOfWork]
    public virtual List<Product> GetProductsInOuIncludingChildren(long organizationUnitId)
    {
        var code = _organizationUnitRepository.Get(organizationUnitId).Code;

        var query =
            from product in _productRepository.GetAll()
            join organizationUnit in _organizationUnitRepository.GetAll() on product.OrganizationUnitId equals organizationUnit.Id
            where organizationUnit.Code.StartsWith(code)
            select product;

        return query.ToList();
    }
}

首先,我们得到了给定OU的代码。 然后我们创建了一个带有连接和StartsWith(代码)条件的LINQ(StartsWith在SQL中创建一个LIKE查询)。 因此,我们可以分级地获得OU的产品。

9,过滤用户的实体
我们可能希望获得特定用户的OU中的所有产品。 示例代码:

public class ProductManager : IDomainService
{
    private readonly IRepository<Product> _productRepository;
    private readonly UserManager _userManager;

    public ProductManager(
        IRepository<Product> productRepository, 
        UserManager userManager)
    {
        _productRepository = productRepository;
        _organizationUnitRepository = organizationUnitRepository;
        _userManager = userManager;
    }

    public async Task<List<Product>> GetProductsForUserAsync(long userId)
    {
        var user = await _userManager.GetUserByIdAsync(userId);
        var organizationUnits = await _userManager.GetOrganizationUnitsAsync(user);
        var organizationUnitIds = organizationUnits.Select(ou => ou.Id);

        return await _productRepository.GetAllListAsync(p => organizationUnitIds.Contains(p.OrganizationUnitId));
    }
}

我们简单地发现了用户的OU的Ids。 然后在获得产品时使用包含条件。 当然,我们可以使用join创建一个LINQ查询来获取相同的列表。

我们可能想要在用户的OU中获得产品,包括其子OU:

public class ProductManager : IDomainService
{
    private readonly IRepository<Product> _productRepository;
    private readonly IRepository<OrganizationUnit, long> _organizationUnitRepository;
    private readonly UserManager _userManager;

    public ProductManager(
        IRepository<Product> productRepository, 
        IRepository<OrganizationUnit, long> organizationUnitRepository, 
        UserManager userManager)
    {
        _productRepository = productRepository;
        _organizationUnitRepository = organizationUnitRepository;
        _userManager = userManager;
    }

    [UnitOfWork]
    public virtual async Task<List<Product>> GetProductsForUserIncludingChildOusAsync(long userId)
    {
        var user = await _userManager.GetUserByIdAsync(userId);
        var organizationUnits = await _userManager.GetOrganizationUnitsAsync(user);
        var organizationUnitCodes = organizationUnits.Select(ou => ou.Code);

        var query =
            from product in _productRepository.GetAll()
            join organizationUnit in _organizationUnitRepository.GetAll() on product.OrganizationUnitId equals organizationUnit.Id
            where organizationUnitCodes.Any(code => organizationUnit.Code.StartsWith(code))
            select product;

        return query.ToList();
    }
}

我们将Any与StartsWith条件组合在LINQ连接语句中。

当然可能需要更复杂的要求,但是所有这些都可以用LINQ或SQL来完成。

10,设置

您可以注入并使用IOrganizationUnitSettings接口来获取组织单位设置值。 目前,只有一个可以根据您的应用需求进行更改的设置:

MaxUserMembershipCount:用户最大允许的会员数。
默认值为int.MaxValue,允许用户在同一时间成为无限制OU的成员。
设置名称是在AbpZeroSettingNames.OrganizationUnits.MaxUserMembershipCount中定义的常量。

 

八、权限管理

1,角色权限

如果我们授予权限角色,则所有用户都有权限授权(除非明确禁止特定用户使用)。

我们使用RoleManager更改角色的权限。 例如,SetGrantedPermissionsAsync可用于在一个方法调用中更改角色的所有权限:

public class RoleAppService : IRoleAppService
{
    private readonly RoleManager _roleManager;
    private readonly IPermissionManager _permissionManager;

    public RoleAppService(RoleManager roleManager, IPermissionManager permissionManager)
    {
        _roleManager = roleManager;
        _permissionManager = permissionManager;
    }

    public async Task UpdateRolePermissions(UpdateRolePermissionsInput input)
    {
        var role = await _roleManager.GetRoleByIdAsync(input.RoleId);
        var grantedPermissions = _permissionManager
            .GetAllPermissions()
            .Where(p => input.GrantedPermissionNames.Contains(p.Name))
            .ToList();

        await _roleManager.SetGrantedPermissionsAsync(role, grantedPermissions);
    }
}

在这个例子中,我们得到一个RoleId和已授予的权限名称列表(input.GrantedPermissionNames是List <string>)作为输入。 我们使用IPermissionManager按名称查找所有权限对象。 然后我们调用SetGrantedPermissionsAsync方法来更新角色的权限。

还有其他方法,如GrantPermissionAsync和ProhibitPermissionAsync一个一个地控制权限。

2,用户权限

虽然基于角色的权限管理对于大多数应用程序来说足够,但我们可能需要控制每个用户的权限。 当我们为用户定义权限设置时,它覆盖权限设置来自用户的角色。

举个例子; 假设我们有一个应用程序服务来禁止用户的权限:

public class UserAppService : IUserAppService
{
    private readonly UserManager _userManager;
    private readonly IPermissionManager _permissionManager;

    public UserAppService(UserManager userManager, IPermissionManager permissionManager)
    {
        _userManager = userManager;
        _permissionManager = permissionManager;
    }

    public async Task ProhibitPermission(ProhibitPermissionInput input)
    {
        var user = await _userManager.GetUserByIdAsync(input.UserId);
        var permission = _permissionManager.GetPermission(input.PermissionName);

        await _userManager.ProhibitPermissionAsync(user, permission);
    }
}

UserManager有许多方法来控制用户的权限。 在这个例子中,我们得到一个UserId和PermissionName,并使用UserManager的ProhibitPermissionAsync方法来禁止用户的权限。

当我们禁止用户的许可时,即使他/她的角色被授予许可,他/她也不能获得此许可。 我们可以说同样的原则给予。 当我们授予专门为用户授予的权限时,该用户被授予权限,即使用户的角色也不被授予权限。 我们可以为用户使用ResetAllPermissionsAsync来删除用户的所有用户特定权限设置。

 

九、语言管理

虽然在大多数情况下都有好处,但我们可能希望在数据库上动态定义语言和文本。 Zero模块允许我们动态管理每个租户的应用程序语言和文本。

1,介绍

①EnableDbLocalization

启用

Configuration.Modules.Zero().LanguageManagement.EnableDbLocalization();

这应该在顶级模块的PreInitialize方法(它是Web应用程序的Web模块)中导入Abp.Zero.Configuration命名空间(使用Abp.Zero.Configuration)来查看Zero()扩展方法)。

②种子数据库语言

由于ABP将从数据库中获得语言列表,所以我们应该将默认语言插入数据库。 如果您使用EntityFramework,您可以像下面那样使用种子代码

using System.Collections.Generic;
using System.Linq;
using Abp.Localization;
using AbpCompanyName.AbpProjectName.EntityFramework;

namespace AbpCompanyName.AbpProjectName.Migrations.SeedData
{
    public class DefaultLanguagesCreator
    {
        public static List<ApplicationLanguage> InitialLanguages { get; private set; }

        private readonly AbpProjectNameDbContext _context;

        static DefaultLanguagesCreator()
        {
            InitialLanguages = new List<ApplicationLanguage>
            {
                new ApplicationLanguage(null, "en", "English", "famfamfam-flag-gb"),
                new ApplicationLanguage(null, "tr", "Türkçe", "famfamfam-flag-tr"),
                new ApplicationLanguage(null, "zh-CN", "简体中文", "famfamfam-flag-cn"),
                new ApplicationLanguage(null, "pt-BR", "Português-BR", "famfamfam-flag-br"),
                new ApplicationLanguage(null, "es", "Español", "famfamfam-flag-es"),
                new ApplicationLanguage(null, "fr", "Français", "famfamfam-flag-fr"),
                new ApplicationLanguage(null, "it", "Italiano", "famfamfam-flag-it"),
                new ApplicationLanguage(null, "ja", "日本語", "famfamfam-flag-jp"),
                new ApplicationLanguage(null, "nl-NL", "Nederlands", "famfamfam-flag-nl"),
                new ApplicationLanguage(null, "lt", "Lietuvos", "famfamfam-flag-lt")
            };
        }

        public DefaultLanguagesCreator(AbpProjectNameDbContext context)
        {
            _context = context;
        }

        public void Create()
        {
            CreateLanguages();
        }

        private void CreateLanguages()
        {
            foreach (var language in InitialLanguages)
            {
                AddLanguageIfNotExists(language);
            }
        }

        private void AddLanguageIfNotExists(ApplicationLanguage language)
        {
            if (_context.Languages.Any(l => l.TenantId == language.TenantId && l.Name == language.Name))
            {
                return;
            }

            _context.Languages.Add(language);

            _context.SaveChanges();
        }
    }
}

③删除静态语言配置

如果您具有如下所示的静态语言配置,您可以从配置代码中删除这些行,因为它将从数据库中获取语言。

Configuration.Localization.Languages.Add(new LanguageInfo("en", "English", "famfamfam-flag-england", true));

④注意现有的XML本地化源

不要删除您的XML本地化文件和源配置代码。 因为这些文件被用作回退源,并且所有的本地化密钥都是从这个源获得的。

因此,当您需要一个新的本地化文本时,按照正常情况将其定义为XML文件。 您应至少在默认语言的XML文件中定义它。 因此,您不需要将本地化文本的默认值添加到数据库迁移代码。

2,管理语言

IApplicationLanguageManager接口被注入并用于管理语言。 它具有GetLanguagesAsync,AddAsync,RemoveAsync,UpdateAsync等方法来管理主机和租户的语言。

①语言列表逻辑

语言列表按租户和主机存储,计算方法如下:

  • 有一个为主机定义的语言列表。 该列表被视为所有租户的默认列表.
  • 每个租户有一个单独的语言列表。 此列表继承主机列表添加特定于特定语言的语言。 租户不能删除或更新主机定义(默认)语言(但可以覆盖本地化文本,我们将在后面看到).

②ApplicationLanguage实体
ApplicationLanguage实体表示租户或主机的语言。

[Serializable]
[Table("AbpLanguages")]
public class ApplicationLanguage : FullAuditedEntity, IMayHaveTenant
{
    //...
}

它的基本属性是:

  • TenantId (可空): 包含有关租户的Id,如果这种语言是特定于租户的。 如果这是主机语言,则为null。
  • Name: 语言名称 这必须是列表中的文化代码。
  • DisplayName: 显示语言的名称。 这可以是任意名称,一般是CultureInfo.DisplayName.
  • Icon: .语言的任意图标/标志。 这可以用于在UI上显示语言的标志

此外,ApplicationLanguage继承了您所看到的FullAuditedEntity。 这意味着它是一个软删除实体,并自动审核(有关更多信息,请参阅实体文档)。

ApplicationLanguage实体存储在数据库中的AbpLanguages表中。

3,管理本地化文本

IApplicationLanguageTextManager接口被注入并用于管理本地化文本。 它需要获取/设置租户或主机的本地化文本的方法。

①本地化文本
让我们看看当你想本地化一个文本时会发生什么?

  • 尝试获得当前文化(获得使用CurrentThread.CurrentUICulture).
    • 它检查给定文本是否定义(覆盖)当前租户(获取使用IAbpSession.TenantId)在数据库中的当前文化。 如果定义,则返回值.
    • 然后,它检查数据库中当前文化中的主机是否定义(覆盖)给定文本。 如果定义,则返回值.
    • 然后,它检查在当前文化中的底层XML文件中是否定义了给定的文本。 如果定义,则返回值.
  • 尝试寻找回归文化。 这样计算:如果现在的文化是“en-GB”,那么后备文化就是“en”.
    • 它检查数据库中的后备文化中当前租户是否定义(覆盖)给定文本。 如果定义,则返回值.
    • 然后,它检查在数据库中的后备文化中主机是否定义(覆盖)给定文本。 如果定义,则返回值.
    • 然后,它会检查在后备文化中的底层XML文件中是否定义了给定的文本。 如果定义,则返回值.
  • 尝试找到默认文化.
    • 它检查数据库中默认文化中当前租户是否定义(覆盖)给定文本。 如果定义,则返回值.
    • 然后它检查在数据库中默认文化中主机是否定义(覆盖)给定文本。 如果定义,则返回值.
    • 然后,它会检查在默认文化中的底层XML文件中是否定义了给定的文本。 如果定义,则返回值.
  • 获取相同的文本或抛出异常
    • 如果根本没有找到给定的文本(键),ABP会抛出异常或通过用[和]包装返回相同的文本(键).

因此,获取本地化的文本有点复杂。 但它起作用很快,因为它使用缓存。

②ApplicationLanguageText实体
ApplicationLanguageText用于存储数据库中的本地化值。

[Serializable]
[Table("AbpLanguageTexts")]
public class ApplicationLanguageText : AuditedEntity<long>, IMayHaveTenant
{
    //...
}

它的基本属性是:

  • TenantId (可空):如果本地化文本是特定于租户的,则包含相关租户的身份证号码。 如果这是主机本地化的文本,它为null .
  • LanguageName: 语言名称 这必须是列表中的文化代码。 这与ApplicationLanguage.Name匹配,但不强制外键使其独立于语言条目。 IApplicationLanguageTextManager正确处理它.
  • Source: 本地化源名称.
  • Key: 本地化文本的键/名称.
  • Value: 本地化值.

ApplicationLanguageText实体存储在数据库的AbpLanguageTexts表中。

 

十、Identity Server集成

Identity Server是一个开源OpenID Connect和OAuth 2.0框架。 它可以用于使您的应用程序在服务器上进行身份验证/单一登录。 它还可以为第三方客户端发出访问令牌。 本文档介绍如何将IdentityServer集成到项目中。

1,安装

由于EF核心包已经取决于第一个,您只能将Abp.ZeroCore.IdentityServer4.EntityFrameworkCore包安装到您的项目中。 安装到项目中包含您的DbContext(.EntityFrameworkCore项目为默认模板):

Install-Package Abp.ZeroCore.IdentityServer4.EntityFrameworkCore

然后你可以添加依赖关系到你的模块(一般来说,你的EntityFrameworkCore项目):

[DependsOn(typeof(AbpZeroCoreIdentityServerEntityFrameworkCoreModule))]
public class MyModule : AbpModule
{
    //...
}

2,配置

使用Abp.ZeroCore配置和使用IdentityServer4类似于独立使用IdentityServer4。 您应该阅读它自己的文档来了解和使用它。 在本文档中,我们仅显示了与Abp.ZeroCore集成所需的其他配置。

①Startup Class

在ASP.NET Core Startup类中,我们应该将IdentityServer添加到服务集合和ASP.NET Core中间件管道中。 突出了与标准IdentityServer4使用的差异:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        //...
        
        services.AddAbpIdentity<Tenant, User, Role>()
            ...
            .AddAbpIdentityServer();
        
        //...
        
        services.AddIdentityServer()
            .AddDeveloperSigningCredential()
            .AddInMemoryIdentityResources(IdentityServerConfig.GetIdentityResources())
            .AddInMemoryApiResources(IdentityServerConfig.GetApiResources())
            .AddInMemoryClients(IdentityServerConfig.GetClients())
            .AddAbpPersistedGrants<YourDbContext>()
            .AddAbpIdentityServer<User>();

        //...
    }

    public void Configure(IApplicationBuilder app)
    {
        //...
        app.UseIdentityServer();
        //...
    }
}

1)在AddAbpIdentity ... chain之后添加了.AddAbpIdentityServer()。 在ABP中,启动模板AddAbpIdentity位于IdentityRegistrar.Register(services)方法中。 所以,你可以像IdentityRegistrar.Register(services).AddAbpIdentityServer()链接。

2)在启动项目中,在IdentityRegistrar.Register(services)之后添加了services.AddIdentityServer()并添加了app.UseIdentityServer()只是app.UseAuthentication()。

3,IdentityServerConfig类
我们使用IdentityServerConfig类来获取身份资源,api资源和客户端。 您可以在自己的文档中找到有关此类的更多信息。 对于最简单的情况,它可以是一个静态类,如下所示

public static class IdentityServerConfig
{
    public static IEnumerable<ApiResource> GetApiResources()
    {
        return new List<ApiResource>
        {
            new ApiResource("default-api", "Default (all) API")
        };
    }

    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
            new IdentityResources.Email(),
            new IdentityResources.Phone()
        };
    }

    public static IEnumerable<Client> GetClients()
    {
        return new List<Client>
        {
            new Client
            {
                ClientId = "client",
                AllowedGrantTypes = GrantTypes.ClientCredentials.Union(GrantTypes.ResourceOwnerPassword),
                AllowedScopes = {"default-api"},
                ClientSecrets =
                {
                    new Secret("secret".Sha256())
                }
            }
        };
    }
}

4,DbContext Changes

 AddAbpPersistentGrants()方法用于保存持久数据存储的同意响应。 为了使用它,YourDbContext必须实现IAbpPersistedGrantDbContext接口,如下所示:

public class YourDbContext : AbpZeroDbContext<Tenant, Role, User, YourDbContext>, IAbpPersistedGrantDbContext
{
    public DbSet<PersistedGrantEntity> PersistedGrants { get; set; }

    public YourDbContext(DbContextOptions<YourDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.ConfigurePersistedGrantEntity();
    }
}

IAbpPersistedGrantDbContext定义了PersistedGrants DbSet。 我们还应该调用上面显示的modelBuilder.ConfigurePersistedGrantEntity()扩展方法,以便为PersistedGrantEntity配置EntityFramework。

请注意,YourDbContext中的此更改会导致新的数据库迁移。 因此,请记住使用“添加迁移”和“更新数据库”命令来更新数据库。

即使您不调用AddAbpPersistedGrants <YourDbContext>()扩展方法,IdentityServer4仍将继续工作,但在这种情况下,用户同意响应将被存储在内存数据存储中(重新启动应用程序时会被清除)。

5,JWT认证中间件
如果我们要针对相同的应用程序授权客户端,我们可以使用IdentityServer身份验证中间件。

首先,将IdentityServer4.AccessTokenValidation包从nuget安装到您的项目中:

Install-Package IdentityServer4.AccessTokenValidation

然后我们可以将中间件添加到Startup类中,如下所示

app.UseIdentityServerAuthentication(
    new IdentityServerAuthenticationOptions
    {
        Authority = "http://localhost:62114/",
        RequireHttpsMetadata = false,
        AutomaticAuthenticate = true,
        AutomaticChallenge = true
    });

我刚刚在启动项目中的app.UseIdentityServer()之后添加了这个。

6,测试

现在,我们的身份服务器已准备好从客户端获取请求。 我们可以创建一个控制台应用程序来发出请求并获得响应。

  • 在解决方案中创建一个新的控制台应用.
  • 将IdentityModel nuget软件包添加到控制台应用程序。 此包用于为OAuth端点创建客户端.

虽然IdentityModel nuget软件包足以创建客户端并使用您的API,但我想以更安全的方式显示使用API:我们将将传入的数据转换为应用程序服务返回的DTO。

  • 从控制台应用程序添加对应用程序层的引用。 这将允许我们使用客户端应用层返回的相同的DTO类.
  • 添加Abp.Web.Common nuget包。 这将允许我们使用ASP.NET Boilerplate类中定义的AjaxResponse类。 否则,我们将处理原始的JSON字符串来处理服务器响应.

那么我们可以改变Program.cs,如下所示:

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Abp.Application.Services.Dto;
using Abp.Json;
using IdentityModel.Client;
using Abp.MultiTenancy;
using Abp.Web.Models;
using IdentityServerIntegrationDemo.Users.Dto;
using Newtonsoft.Json;

namespace IdentityServerIntegrationDemo.ConsoleApiClient
{
    class Program
    {
        static void Main(string[] args)
        {
            RunDemoAsync().Wait();
            Console.ReadLine();
        }

        public static async Task RunDemoAsync()
        {
            var accessToken = await GetAccessTokenViaOwnerPasswordAsync();
            await GetUsersListAsync(accessToken);
        }

        private static async Task<string> GetAccessTokenViaOwnerPasswordAsync()
        {
            var disco = await DiscoveryClient.GetAsync("http://localhost:62114");

            var httpHandler = new HttpClientHandler();
            httpHandler.CookieContainer.Add(new Uri("http://localhost:62114/"), new Cookie(MultiTenancyConsts.TenantIdResolveKey, "1")); //Set TenantId
            var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret", httpHandler);
            var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync("admin", "123qwe");

            if (tokenResponse.IsError)
            {
                Console.WriteLine("Error: ");
                Console.WriteLine(tokenResponse.Error);
            }

            Console.WriteLine(tokenResponse.Json);

            return tokenResponse.AccessToken;
        }

        private static async Task GetUsersListAsync(string accessToken)
        {
            var client = new HttpClient();
            client.SetBearerToken(accessToken);

            var response = await client.GetAsync("http://localhost:62114/api/services/app/user/getUsers");
            if (!response.IsSuccessStatusCode)
            {
                Console.WriteLine(response.StatusCode);
                return;
            }

            var content = await response.Content.ReadAsStringAsync();
            var ajaxResponse = JsonConvert.DeserializeObject<AjaxResponse<PagedResultDto<UserListDto>>>(content);
            if (!ajaxResponse.Success)
            {
                throw new Exception(ajaxResponse.Error?.Message ?? "Remote service throws exception!");
            }

            Console.WriteLine();
            Console.WriteLine("Total user count: " + ajaxResponse.Result.TotalCount);
            Console.WriteLine();
            foreach (var user in ajaxResponse.Result.Items)
            {
                Console.WriteLine($"### UserId: {user.Id}, UserName: {user.UserName}");
                Console.WriteLine(user.ToJsonString(indented: true));
            }
        }
    }
}

运行此应用程序之前,请确保您的Web项目已启动并运行,因为此控制台应用程序将向Web应用程序发出请求。 另外,请确保请求端口(62114)与您的Web应用程序相同。

您可以在此处看到本教程的源代码:https://github.com/aspnetboilerplate/aspnetboilerplate-samples/tree/master/IdentityServerDemo。 

 

posted on 2017-10-22 21:28  *Hunter  阅读(7525)  评论(1编辑  收藏  举报

导航

AmazingCounters.com