Pergunta

Estou migrando um aplicativo SaaS do ASP clássico para o .NET MVC5 e usarei o banco de dados EF6 primeiro.O formulário de login para usuários finais pode ser personalizado por cada locatário (em seu próprio subdomínio, mas apontando para o mesmo aplicativo web).Desejamos usar o esquema de banco de dados existente e os novos filtros de autenticação e autorização.

Por exemplo, um usuário de um locatário pode fazer login digitando seu nome, sobrenome e um código gerado pelo nosso sistema.Um usuário de outro locatário pode fazer login inserindo seu endereço de e-mail e uma senha.Além disso, cada locatário possui um login de administrador separado que usa nome de usuário e senha.Outro locatário pode usar a autenticação LDAP em um servidor AD remoto.

Existe uma maneira definitiva de fazer autenticação personalizada?

Quase todos os artigos parecem sugerir diferentes maneiras de fazer isso:simplesmente configurando FormsAuthentication.SetAuthCookie, usando um provedor OWIN personalizado, substitua AuthorizeAttribute, etc.

No ASP clássico, consultamos o banco de dados para descobrir o tipo de login daquele locatário, exibimos os campos apropriados na tela de login e depois no post back, verificamos se os campos correspondem ao que está no banco de dados e então definimos as variáveis ​​de sessão apropriadamente que foram verificado em cada solicitação de página.

Obrigado

Foi útil?

Solução

Acho que a estrutura de identidade é muito flexível em termos de opções de autenticação.Dê uma olhada neste código de autenticação:

var identity = await this.CreateIdentityAsync(applicationUser, DefaultAuthenticationTypes.ApplicationCookie);

authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);

Esta é uma parte bastante padrão da autenticação no Identity, você encontrará isso em todos os exemplos de Identity na web.Se você olhar de perto, é muito flexível - tudo que você precisa para autenticação é ApplicationUser objeto que a estrutura não se importa como você consegue.

Então, em teoria, você pode fazer coisas assim (pseudocódigo, não tentei compilar isso):

// get user object from the database with whatever conditions you like
// this can be AuthCode which was pre-set on the user object in the db-table
// or some other property
var user = dbContext.Users.Where(u => u.Username == "BillyJoe" && u.Tenant == "ExpensiveClient" && u.AuthCode == "654")

// check user for null 

// check if the password is correct - don't have to do that if you are doing
// super-custom auth.
var isCorrectPassword = await userManager.CheckPasswordAsync(user, "enteredPassword");

if (isCorrectPassword)
{
    // password is correct, time to login
    // this creates ClaimsIdentity object from the ApplicationUser object
    var identity = await this.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);

    // now we can set claims on the identity. Claims are stored in cookie and available without
    // querying database
    identity.AddClaim(new Claim("MyApp:TenantName", "ExpensiveClient"));
    identity.AddClaim(new Claim("MyApp:LoginType", "AuthCode"));
    identity.AddClaim(new Claim("MyApp:CanViewProducts", "true"));


    // this tells OWIN that it can set auth cookie when it is time to send 
    // a reply back to the client
    authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}

Usando esta autenticação, você definiu algumas reivindicações para o usuário - elas são armazenadas no cookie e disponíveis em qualquer lugar via ClaimsPrincipal.Current.Claims.As declarações são essencialmente uma coleção de pares de strings de valores-chave e você pode armazenar o que quiser.

Normalmente acesso as reivindicações do usuário por meio do método de extensão:

public static String GetTenantName(this ClaimsPrincipal principal)
{
    var tenantClaim = principal.Claims.FirstOrDefault(c => c.Type == "MyApp:TenantName");
    if (tenantClaim != null)
    {
        return tenantClaim.Value;
    }

    throw new ApplicationException("Tenant name is not set. Can not proceed");
}

public static String CanViewProducts(this ClaimsPrincipal principal)
{
    var productClaim = principal.Claims.FirstOrDefault(c => c.Type == "MyApp:CanViewProducts");
    if (productClaim == null)
    {
        return false;
    }

    return productClaim.Value == "true";
}

Portanto, na sua camada controlador/visualização/negócio você sempre pode ligar para ClaimsPrincipal.Current.GetTenantName() e neste caso você receberia "ExpensiveClient" de volta.

Ou se você precisar verificar se um recurso específico está habilitado para o usuário, você faz

if(ClaimsPrincipal.Current.CanViewProducts())
{
    // display products
}

Depende de você como armazenar as propriedades do usuário, mas contanto que você as defina como declarações no cookie, elas estarão disponíveis.

Alternativamente, você pode adicionar declarações ao banco de dados para cada usuário:

await userManager.AddClaimAsync(user.Id, new Claim("MyApp:TenantName", "ExpensiveClient"));

E isso persistirá a reivindicação no banco de dados.E por padrão, o Identity Framework adiciona essa declaração ao usuário quando ele faz login, sem que você precise adicioná-la manualmente.

Mas cuidado, você não pode definir muitas reivindicações em um cookie.Os cookies têm limite de 4K definido pelos navegadores.E a forma como a criptografia do cookie de identidade funciona aumenta o texto codificado em cerca de 1,1, para que você possa ter aproximadamente 3,6 K de texto representando declarações.Eu me deparei com isso problema aqui

Atualizar

Para controlar o acesso aos controladores por meio de declarações, você pode usar o seguinte filtro no controlador:

public class ClaimsAuthorizeAttribute : AuthorizeAttribute
{
    public string Name { get; private set; }


    public ClaimsAuthorizeAttribute(string name)
    {
        Name = name;
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        var user = HttpContext.Current.User as ClaimsPrincipal;
        if (user.HasClaim(Name, Name))
        {
            base.OnAuthorization(filterContext);
        }
        else
        {
            filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary()
            {
                {"controller", "errors"},
                {"action", "Unauthorised"}
            });
        }
    }
}

e então use este atributo em controladores ou ações separadas como esta:

    [ClaimsAuthorize("Creating Something")]
    public ActionResult CreateSomething()
    {
        return View();
    }

O usuário exigirá a reivindicação "Criar algo" para acessar esta ação, caso contrário, ele será redirecionado para a página "Não autenticado".

Recentemente, brinquei com autenticação de declarações e criei um protótipo de aplicativo semelhante ao seu requisito.Por favor, dê uma olhada na versão simples: https://github.com/trailmax/ClaimsAuthorisation/tree/SimpleClaims onde as reivindicações são armazenadas individualmente para cada usuário.Ou há uma solução mais complexa onde as declarações pertencem a uma função e quando os usuários fazem login, as declarações de função são atribuídas ao usuário: https://github.com/trailmax/ClaimsAuthorisation/tree/master

Outras dicas

Existem dois componentes que você precisa.A autenticação em si e a estratégia que cada usuário obtém para autenticação.

A primeira é fácil e se realiza com estas duas linhas...

var identity = await UserManager.CreateIdentityAsync(user, 
    DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() 
    { IsPersistent = isPersistent }, identity);

Quando um usuário está conectado, ele obtém uma identidade que contém as reivindicações do usuário sobre as funções e quem ele é.Eles são fornecidos ao usuário como um cookie.Após este ponto você apenas decora os controladores com [Authorize] para garantir que apenas usuários autenticados possam fazer login.Bastante padrão aqui.

A única parte complicada do problema é a segunda parte;A estratégia de como cada usuário é autenticado definida pelo administrador.

Algum pseudocódigo de como isso poderia funcionar em ações é este...

// GET: /Account/Login
[AllowAnonymous]
public ActionResult Login(int tenantId)
{
    var tenant = DB.GetTenant(tenantId);
    return View(tenant);
}

Na sua opinião, você geraria a estratégia de autenticação para o locatário.Pode ser e-mail e senha, um código e e-mail ou qualquer que seja sua necessidade.

Quando o usuário insere suas informações e clica para fazer login, você deve determinar qual estratégia ele estava usando e verificar se as informações correspondem.

//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model)
{
    var tenant = DB.GetTenant(model.tenantId);
    //If user info matches what is expected for the tenants strategy
    if(AuthenticateUserInfo(tenant, model.UserInputs))
    {
       //Sign the user in
       var identity = await UserManager.CreateIdentityAsync(user, 
           DefaultAuthenticationTypes.ApplicationCookie);
       AuthenticationManager.SignIn(new AuthenticationProperties() 
           { IsPersistent = isPersistent }, identity);
    }
}

Acenei muito na segunda parte por causa da natureza complicada de quão dinâmica ela é.No geral, você deve usar as mesmas estratégias usadas em seu aplicativo legado para gerar as entradas corretas e assim por diante.Nada mudou lá, apenas a forma como você faz login será diferente.

Usando Atualização 3 do Visual Studio 2013 você pode criar um novo aplicativo da Web que vem com MVC5, EF6 e Identidade já instalado.Veja como selecionar Identidade ao criar um novo aplicativo:

Com o modelo MVC selecionado, clique em Alterar autenticação e a janela destacada aparecerá.Contas de usuário individuais = Identidade.Clique em ok e continue.Select Identity in MVC Application

Feito isso, você criou um aplicativo com Identity.Agora você pode personalizar seu login e registro da seguinte maneira.

Você deseja ver seu AccountController.cs na pasta Controllers.Aqui você encontrará o script para Cadastro e Login.

Se você olhar para o

public async Task<ActionResult> Register(RegisterViewModel model)

função, você notará que ela contém:

IdentityResult result = await UserManager.CreateAsync(new ApplicationUser() { UserName = newUser.UserName }, newUser.Password);

É aqui que o usuário é criado.Se quiser usar o Identity, você deve salvar o nome de usuário e a senha do usuário.Você pode usar um e-mail como nome de usuário, se desejar.etc.

Depois de fazer isso, adiciono ao usuário uma função especificada (encontrei o usuário e o adiciono à função):

ApplicationUser userIDN = UserManager.FindByName(newUser.UserName);
result = await UserManager.AddToRoleAsync(userIDN.Id, "Admin");

No meu cenário, criei uma tabela estendida adicional onde mantenho seu endereço, número de telefone, etc.Nessa tabela, você pode conter qualquer informação adicional de login.Você pode adicionar essas novas entradas antes ou depois de criar a conta do usuário no Identity.Eu criaria as informações estendidas e depois criaria a conta de identidade apenas para ter certeza.

IMPORTANTE: Para qualquer cenário em que um usuário esteja fazendo login com algo que não seja um nome de usuário ou endereço de e-mail que não esteja salvo via Identidade, você terá que fazer uma solução personalizada.

Exemplo: O usuário digita seu nome, sobrenome e o código.Você poderia fazer duas coisas:Salve o nome e o sobrenome no campo nome de usuário da identidade e o código na senha e verifique o login dessa formaOU você verificaria essas propriedades em sua tabela personalizada e garantiria que elas correspondam. Se e quando corresponderem, você poderá chamar esta belezura:

await SignInAsync(new ApplicationUser() { UserName = model.UserName }, isPersistent: false);

Depois de chamar a função SignInAsync, você pode prosseguir e direcioná-los para sua página protegida.

OBSERVAÇÃO: Estou criando o ApplicationUser na chamada da função mas se você usar mais de uma vez seria ideal você declarar o ApplicationUser da seguinte forma:

ApplicationUser user = new ApplicationUser() { UserName = model.UserName };

NOTA 2: Se você não quiser usar métodos Async, todas essas funções terão versões não assíncronas deles.

Nota 3: No topo de qualquer página usando UserManagement, ele está sendo declarado.Certifique-se de que, se você estiver criando seu próprio controlador que não foi gerado pelo Visual Studio para usar o Identity, inclua o script de declaração UserManagement na parte superior da classe:

namespace NameOfProject.Controllers
{
    [Authorize]
    public class AccountController : Controller
    {
        public AccountController() : this(new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()))) { }
        public AccountController(UserManager<ApplicationUser> userManager) { UserManager = userManager; }
        public UserManager<ApplicationUser> UserManager { get; private set; }

Por favor, deixe-me saber se você tiver alguma dúvida e espero que isso ajude.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top