Лучшая практика для хранения требований к авторизации основного ресурса ASP.NET при аутентификации пользователей в Active Directory?

Я создаю приложение ASP.NET Core MVC для корпоративных приложений. Я хочу, чтобы мои пользователи аутентифицировались с помощью Active Directory, и я хочу, чтобы авторизации пользователей (заявки) хранились в ApplicationDbContext.

Я предполагаю, что мне нужно использовать Microsoft.AspNetCore.Identity и Microsoft.AspNetCore.Identity.EntityFrameworkCore для достижения моих целей. Какова наилучшая практика хранения требований к авторизации основного ресурса ASP.NET при аутентификации в Active Directory?

Следующий код предоставит мне доступ к текущему контексту безопасности пользователей Windows (текущему входе в систему пользователя) изнутри конвейера. Как-то мне нужно сопоставить пользователя с соответствующими утверждениями Microsoft.AspNetCore.Identity?

 app.Use(async (context, next) =>
 {
      var identity = (ClaimsIdentity) context.User.Identity;
      await next.Invoke();
 });

Заранее благодарим за помощь!

Ответ 1

Я не думаю, что в этом есть лучшая практика, но у вас есть много вариантов. Возможно, вы можете немного рассказать о том, чего вы пытаетесь достичь? Вот несколько указателей, которые могут помочь вам разобраться в этом. Просто вопрос... что вы используете для аутентификации против AD под капотом? Если вы используете IdentityServer или что-то подобное, я бы рекомендовал придерживаться варианта 4.

Вариант 1 - Сохранить претензии как свойство ApplicationUser

Прежде всего, утверждения - это просто пары ключ-значение. Это означает, что вы можете создать такую ​​структуру, как:

public class UserClaim 
{
    public string UserName {get; set;}
    public string Key {get; set;}
    public string Value {get; set;}
}

Это позволит вам хранить претензии в классе ApplicationUser.

Вариант 2 - Сохранение претензий с использованием UserManager

Еще лучше, вы можете использовать встроенные компоненты идентификации, введя UserManager<ApplicationUser> userManager в класс, где вы хотите добавить претензии пользователя, а затем вызовите AddClaim() с соответствующими значениями. Из-за системы DI в Core вы можете делать это в любом классе, который будет активирован средой выполнения.

Вариант 3 - Сохранение претензий в их собственной таблице

Другим подходом было бы увеличение класса UserClaim с помощью свойства UserName, а затем использование уникального идентификатора принципа (User.Identity.Name). Затем вы можете сохранить это в DbSet<UserClaim> в ApplicationDbContext и получить формулы по имени пользователя.

Вариант 4 - Не хранить претензии

Если вам просто нужно получить доступ к претензиям и не хранить их (я не уверен в ваших намерениях из вашего вопроса), вам может быть лучше всего получить доступ к свойству User, если вы находитесь в контроллере, при условии, что вы используете службу проверки подлинности, которая правильно увлажняет форму заявки.

User украшен претензиями, принадлежащими пользователю, который подписался в ваше приложение и доступен на каждом контроллере.

В противном случае вы можете получить ClaimsPrinciple и получить доступ к заявкам пользователя таким образом. В этом случае (вне контроллера) вы должны добавить конструктор класса IHttpContextAccessor accessor к конструктору вашего класса и перейти к свойству HttpContextAccessor.HttpContext.User, который снова будет ClaimsPrinciple.

Ответ 2

Это был мой подход:

Внедрение класса MyUser

public class MyUser: IdentityUser
{
}

Используйте ASP.Net Core DbContext в отдельном "защищенном" DBC-тексте (я предпочитаю ограниченные контексты, отвечающие за их собственную функциональность)

public class SecurityDbContext : IdentityDbContext<MyUser>
{
    public SecurityDbContext(DbContextOptions<SecurityDbContext> options)
        : base(options)
    { }
}

Создайте трансформатор претензий. Это используется для добавления претензий из хранилища в ClaimsIdentity (объект User в вашем HttpContext).

public class ClaimsTransformer : IClaimsTransformer
{
    private readonly SecurityDbContext _context;
    private readonly UserManager<MyUser> _userManager;

    public ClaimsTransformer(SecurityDbContext context, UserManager<MyUser> userManager)
    {
        _context = context;
        _userManager = userManager;
    }

    public Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
    {
        var identity = context.Principal.Identity as ClaimsIdentity;
        if (identity == null) return Task.FromResult(context.Principal);

        try
        {
            if (context.Context.Response.HasStarted)
                return Task.FromResult(context.Principal);

            var claims = AddClaims(identity);
            identity.AddClaims(claims);
        }
        catch (InvalidOperationException)
        {
        }
        catch (SqlException ex)
        {
            if (!ex.Message.Contains("Login failed for user") && !ex.Message.StartsWith("A network-related or instance-specific error"))
                throw;
        }

        return Task.FromResult(context.Principal);
    }

    private IEnumerable<Claim> AddClaims(IIdentity identity)
    {
        var claims = new List<Claim>();
        var existing = _userManager.Users.Include(u => u.Claims).SingleOrDefault(u => u.UserName == identity.Name);
        if (existing == null) return claims;

        claims.Add(new Claim(SupportedClaimTypes.ModuleName, Constants.ADGroupName));
        claims.AddRange(existing.Claims.Select(c => c.ToClaim()));

        return claims;
    }
}

Вам нужно добавить несколько вещей в свой класс startup.cs, я добавил только следующие:

public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentity<FreightRateUser, IdentityRole>(config =>
            {
                config.User.AllowedUserNameCharacters =
                    "ab[email protected]\\";
            config.User.RequireUniqueEmail = false;
            config.Cookies.ApplicationCookie.LoginPath = "/Auth/Login";
        })
        .AddEntityFrameworkStores<SecurityDbContext>();
}

Не забывайте, что метод Configure

public async void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.UseIdentity();
}

Наконец, вам понадобится некоторое представление, чтобы добавить соответствующие пользовательские/требования к таблицам Identity, у меня есть контроллер с несколькими действиями (только показывая Create here):

    [AllowAnonymous]
    public IActionResult CreateAccess()
    {
        var vm = new AccessRequestViewModel
        {
            Username = User.Identity.Name
        };
        return View(vm);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    [AllowAnonymous]
    public async Task<IActionResult> CreateAccess(AccessRequestViewModel viewModel)
    {
        if (User == null || !User.Identity.IsAuthenticated) return View("Index");

            var newUser = new MyUser
            {
                UserName = viewModel.Username
            };
            var x = await _userManager.CreateAsync(newUser);
            if (!x.Succeeded)
                return View(ModelState);

            var myClaims = new List<Claim>();
            if (viewModel.CanManageSecurity)
                myClaims.Add(new Claim(SupportedClaimTypes.Security, "SITE,LOCAL"));
            if (viewModel.CanChangeExchangeRates)
                myClaims.Add(new Claim(SupportedClaimTypes.ExchangeRates, "AUD,USD"));
            if (viewModel.CanChangeRates)
                myClaims.Add(new Claim(SupportedClaimTypes.Updates, "LOCAL"));
            if (viewModel.CanManageMasterData)
                myClaims.Add(new Claim(SupportedClaimTypes.Admin, "SITE,LOCAL"));

            await _userManager.AddClaimsAsync(newUser, myClaims);
        }
        return View("Index");
    }

Когда пользователь будет сохранен и снова отобразит страницу с проверкой подлинности (индекс), трансформатор претензий будет загружать претензии в ClaimsIdentity, и все должно быть хорошо.

Обратите внимание, что это для текущего пользователя HttpContext, который захочет создать для других пользователей в Create, поскольку вы, очевидно, не хотите, чтобы текущий пользователь предоставлял себе доступ.