How to use Asp.Net Core Identity in Multi-Tenant environment

前端 未结 1 1719
Happy的楠姐
Happy的楠姐 2020-12-29 09:25

I have a working Asp.Net Core application with default Identity handling. Now I want to use it for multi domains. I extended ApplicationUser with DomainId. How can I handle

1条回答
  •  甜味超标
    2020-12-29 10:01

    Finally I figured it out. So first, I have to set user email to not unique. Sidenote: I'm using email for UserName also, I don't like to ask UserName from users:

    services.Configure(options =>
    {
        options.User.RequireUniqueEmail = false;
    });
    

    When a new user register himself, I'm merging current Domain Id to UserName, this helps users to register with same Email / UserName into the system through totally different domains.

    Then I had to create my custom UserManager, where I'm overriding FindByEmail:

    using Microsoft.AspNetCore.Identity;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Options;
    using MultiShop.Core.Repositories.User;
    using MultiShop.Core.Tenant;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    
    namespace Test
    {
        public class MyShopUserManager : UserManager, IDisposable where TUser : class
    {
        private readonly ITenantService tenantService;
        private readonly IUserRepository userRepository;
    
        public MyUserManager(IUserStore store, IOptions optionsAccessor,
            IPasswordHasher passwordHasher, IEnumerable> userValidators,
            IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer,
            IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger,
            ITenantService tenantService, IUserRepository userRepository)
            : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
        {
            this.tenantService = tenantService;
            this.userRepository = userRepository;
        }
    
        public override async Task FindByEmailAsync(string email)
        {
            ThrowIfDisposed();
            if (email == null)
            {
                throw new ArgumentNullException(nameof(email));
            }
    
            var users = (await userRepository.GetAllAsync()).Where(u => u.Email == email);
    
            if (users == null)
            {
                return null;
            }
    
            if (users.Count() == 1)
            {
                return await Store.FindByIdAsync(users.First().Id.ToString(), CancellationToken);
            }
    
            var currentDomain = tenantService.GetCurrentDomainAsync().Result;
            var user = users.SingleOrDefault(u => u.DomainId == currentDomain.Id);
    
            if (user == null)
            {
                return null;
            }
    
            return await Store.FindByIdAsync(user.Id.ToString(), CancellationToken);
        }
    }
    }
    

    Be careful, because of multi-domains and generated UserNames, you should use userManager.FindByEmailAsync, instead of FindByNameAsync.

    I had to create custom SignInManager for handling multi-domain users:

    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Options;
    using MultiShop.Core.Tenant;
    using MultiShop.Data.Entities;
    using System.Threading.Tasks;
    
    namespace Test
    {
    public class MySignInManager : SignInManager
    
    {
        private readonly ITenantService tenantService;
    
        public MySignInManager(UserManager userManager, IHttpContextAccessor contextAccessor,
            IUserClaimsPrincipalFactory claimsFactory, IOptions optionsAccessor,
            ILogger> logger, IAuthenticationSchemeProvider schemes,
            ITenantService tenantService)
            : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes)
        {
            this.tenantService = tenantService;
        }
    
        public override async Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure)
        {
            var currentDomain = await tenantService.GetCurrentDomainAsync();
            return await base.PasswordSignInAsync($"{userName}{currentDomain.Id}", password, isPersistent, lockoutOnFailure);
        }
    }
    }
    

    Finally I have to register my custom managers into Asp.Net Core Identity DI:

    services
       .AddIdentity()
       .AddEntityFrameworkStores()
       .AddDefaultTokenProviders()
       //my custom managers for domain segmented users
       .AddUserManager>()
       .AddSignInManager();
    

    That's it!

    0 讨论(0)
提交回复
热议问题