Criar uma Web API com Autorização por Funções com ASP.NET Core

Criar uma Web API com Autorização por Funções com ASP.NET Core

Aprenda como criar uma Web API com autorização por funções usando ASP.NET Core 6. Usaremos o Swagger UI para visualizar e interagir com os endpoints e o MS SQL Server. Leia mais.

16min read

Quando se trata de criar uma Web API com autorização por funções usando ASP.NET Core, a abordagem code-first (código-primeiro) pode ser um método poderoso e eficiente. Usando-a, podemos definir nossos modelos de dados e relacionamentos em código e então gerar o esquema de banco de dados correspondente automaticamente. O que isso leva a? Ciclos de desenvolvimento mais rápidos e maior flexibilidade, com certeza. Como? Porque mudanças no modelo de dados podem ser feitas rápida e facilmente, sem ter que modificar o esquema do banco de dados diretamente. Você pode ler mais sobre as abordagens Design First e Code First em swagger.io.

Neste tutorial, então, cobriremos as etapas para criar uma Web API com autorização por funções usando ASP.NET Core 6. Usaremos o Swagger UI para visualizar e interagir com nossos endpoints e o MS SQL Server como nosso banco de dados. A aplicação incluirá um módulo de autenticação e um módulo de eventos. Usuários conectados serão capazes de visualizar os eventos associados à sua conta, enquanto usuários com a função de Administrador podem criar, atualizar e deletar eventos.

Vamos começar!

Configuração do Projeto

Primeiro, precisamos configurar nosso projeto. Para fazer isso, abra o Visual Studio, vá para criar um novo projeto e escolha ASP.NET Core Web API.

abra o Visual Studio, vá para criar um novo projeto e escolha ASP.NET Core Web API

Escolha o nome da aplicação e clique em Próximo.

Escolha o nome da aplicação no Visual Studio

Configurando o Banco de Dados da API

Depois que inicializarmos nossa aplicação, precisamos configurar o banco de dados. Vamos usar EntityFrameworkCore como ORM, portanto, isso nos ajudará a gerenciar nosso banco de dados. Por essa razão, devemos instalar alguns pacotes.

Configurando o Banco de Dados da API no Visual Studio

A próxima coisa a fazer depois que instalamos com sucesso os pacotes é criar um DbContext. Crie o arquivo DataContext.cs e herde a classe DBContext. Aqui vamos definir nossas tabelas.

public class DataContext: DbContext { public DataContext(DbContextOptions options): base(options) { }

//Define nossas tabelas }

Então devemos abrir o arquivo Program.cs e adicionar o dbContext. Devemos especificar dbProvider e a string de conexão que vêm do arquivo appsettings.json. O dbProvider pode ser SqlServer, MySql ou InMemory.

// Adicionar Db context

var dbProvider = builder.Configuration.GetConnectionString(“Provider”);

builder.Services.AddDbContext < DataContext > (options => {

if (dbProvider == "SqlServer")
{
  options.UseSqlServer(builder.Configuration.GetConnectionString("SqlServerConnectionString"));
}

});

Certifique-se de ter adicionado a ConnectionString e Provider no seu arquivo appsettings.json como segue:

… “ConnectionStrings”: { “Provider”: “SqlServer”, “SqlServerConnectionString”: “Data Source=(localdb)\\MSSQLLocalDB;Database=HRApplication2;Integrated Security=True;Connect Timeout=30; ” }, …

Depois de configurar o DbContext, é necessário gerar os modelos de banco de dados. Neste caso, exigimos duas entidades – User e Event – e uma terceira tabela – UserEvent – para estabelecer uma relação muitos-para-muitos entre elas. Para accomplish isso, é recomendado que criemos uma pasta Models e uma subpasta DbModels dentro dela onde possamos criar nossas entidades de banco de dados.

Vamos começar com o modelo User. Cada usuário deve ter um ID único, Email, FirstName, LastName, Password que será armazenado em um formato hash, Role que pode ser User e Administrator para a demonstração, e UserEvents que será relacionado à tabela UserEvent.

public class User { public string UserId { get; set; } public string Email { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Password { get; set; } public string Role { get; set; } public IList UserEvents { get; set; } }

O modelo Event também deve ter ID único, Title, Category, Date, e também relação para a tabela UserEvents.

public class Event { public string Id { get; set; } public string Title { get; set; } public string Category { get; set; } public DateTime Date { get; set; } public IList UserEvents { get; set; } }

Como um usuário pode participar de múltiplos eventos e um evento pode ser participado por múltiplos usuários, precisamos estabelecer uma relação muitos-para-muitos entre essas entidades. Para fazer isso, criaremos uma tabela adicional chamada UserEvents. Esta tabela incluirá colunas UserId e EventId que estabelecerão a relação entre as entidades User e Event.

public class UserEvent { public string UserId { get; set; } public User User { get; set; } public string EventId { get; set; } public Event Event { get; set; } }

Uma vez que criamos nossos modelos de banco de dados, o próximo passo é registrá-los em nosso DbContext. Para alcançar isso, podemos navegar para o arquivo DataContext.cs, adicionar todas as entidades como DbSets, e declarar nossos relacionamentos e chaves primárias. Isso pode ser realizado ao sobrescrever o método OnModelCreating e utilizando a Fluent API para configurar os relacionamentos e chaves. Uma vez concluído, o resultado deve aparecer como segue:

public class DataContext: DbContext { public DataContext(DbContextOptions options): base(options) { }

public DbSet Users { get; set; } public DbSet < Event > Events { get; set; } public DbSet < UserEvent > UserEvents { get; set; }

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

builder.Entity<User>()
  .HasKey(u => new {
    u.UserId
  });

builder.Entity<Event>()
  .HasKey(e => new {
    e.Id
  });

builder.Entity<UserEvent>()
  .HasKey(ue => new {
    ue.UserId, ue.EventId
  });

builder.Entity<UserEvent>()
  .HasOne(ue => ue.User)
  .WithMany(user => user.UserEvents)
  .HasForeignKey(u => u.UserId);

builder.Entity<UserEvent>()
  .HasOne(uc => uc.Event)
  .WithMany(ev => ev.UserEvents)
  .HasForeignKey(ev => ev.EventId);

} }

Depois que estamos prontos com o design do banco de dados, devemos gerar uma migração inicial que criará o banco de dados.

Abra o Console do Gerenciador de Pacotes e escreva o comando:

Add-Migration InitialCreate

Add-Migration InitialCreate no Console do Gerenciador de Projetos

Depois que for executado com sucesso, devemos atualizar o banco de dados com:

Update-Database

Então com o Microsoft SQL Management Studio, você deve ver o banco de dados recém-criado.

Configurando AutoMapper

AutoMapper nos ajudará a transformar um modelo em outro. Isso vai converter os modelos de entrada em dbModels. A razão pela qual estamos fazendo isso é que podemos não precisar de todas as propriedades de um dos modelos para serem incluídas no outro modelo. Você verá exatamente como vamos usá-lo mais tarde no tutorial. Antes disso, primeiro precisamos configurá-lo. Você pode encontrar uma explicação mais detalhada de AutoMapper na documentação oficial.

Para começar, devemos instalar o pacote AutoMaper NuGet. Após isso, podemos gerar um arquivo MappingProfiles.cs para definir todos os mapeamentos. É recomendado criar este arquivo em uma pasta Helpers para fins de organização.

Para declarar nossos mapeamentos, MappingProfiles deve herdar a classe Profile e podemos declarar nossos mapeamentos usando o método CreateMap<from, to>(). Se exigirmos a capacidade de mapear modelos na direção oposta, podemos incluir o método .ReverseMap().

Uma vez que completamos nossos mapeamentos, devemos navegar para o arquivo Program.cs e registrar AutoMapper com nossos MappingProfiles.

… var config = new MapperConfiguration(cfg => { cfg.AddProfile(new MappingProfiles()); });

var mapper = config.CreateMapper();

builder.Services.AddSingleton(mapper); …

Configurando Autenticação

Vamos usar tokens JWT para autenticação. Eles nos fornecem uma maneira de transmitir informações de forma segura entre as partes como objeto JSON. Você pode ler mais sobre tokens JWT aqui. Para usá-los, devemos primeiro instalar os pacotes NuGet necessários. Exigimos tanto Microsoft.IdentityModel.Tokens quanto Microsoft.AspNetCore.Authentication.JwtBearer.

Em seguida, devemos definir algumas configurações de token no arquivo appsettings.json. Essas configurações incluem o Issuer, Audience e SecretKey.

“Jwt”: { “Issuer”: “https://localhost:7244/”, “Audience”: “https://localhost:7244/”, “Key”: “S1u*p7e_r+S2e/c4r6e7t*0K/e7y” }

Uma vez que as configurações de token foram definidas, podemos configurar o serviço JWT no arquivo Program.cs. Isso envolve especificar o esquema que será usado junto com quaisquer parâmetros de validação necessários.

… builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(o => { o.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = builder.Configuration[“Jwt:Issuer”], ValidAudience = builder.Configuration[“Jwt:Audience”], IssuerSigningKey = new SymmetricSecurityKey (Encoding.UTF8.GetBytes(builder.Configuration[“Jwt:Key”])), ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = false, ValidateIssuerSigningKey = true }; }); …

Certifique-se de ter também adicionado app.UseAuthentication();

Configurando Swagger

Para testar nossos endpoints de aplicação usando Swagger UI, devemos incluir app.UseSwaggerUI() no arquivo Program.cs.

Depois disso, devemos gerar um filtro AuthResponse para ajudar a testar nossos endpoints autenticados usando tokens JWT. Para accomplish isso, podemos criar uma classe AuthResponsesOperationFilter que implementa a interface IOperationFilter. O método Apply deve incluir a lógica necessária para adicionar o filtro AuthResponse ao Swagger.

public class AuthResponsesOperationFilter: IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { var authAttributes = context.MethodInfo.DeclaringType.GetCustomAttributes(true) .Union(context.MethodInfo.GetCustomAttributes(true)) .OfType();

    if (authAttributes.Any())
    {
        var securityRequirement = new OpenApiSecurityRequirement()
        {
            {
                new OpenApiSecurityScheme
                {
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "Bearer"
                    }
                },
                new List<string>()
            }
        };

        operation.Security = new List<OpenApiSecurityRequirement> {
            securityRequirement
        };

        operation.Responses.Add("401", new OpenApiResponse {
            Description = "Não autorizado"
        });
    }
}

}

Depois disso, certifique-se de ter adicionado o filtro como uma opção no método Program.cs .AddSwaggerGen.

builder.Services.AddSwaggerGen(option => { option.SwaggerDoc(“v1”, new OpenApiInfo { Title = “Northwind CRUD”, Version = “v1” });

    option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Description = "Por favor, insira um token válido",
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        BearerFormat = "JWT",
        Scheme = "bearer"
    });

    option.OperationFilter < AuthResponsesOperationFilter > ();
}

);

Você pode ler uma explicação mais detalhada de “O que é Swagger?” na documentação oficial.

Endpoint de Registro

Depois que concluímos as configurações, podemos passar a criar o endpoint de registro. O primeiro passo é gerar um arquivo RegisterInputModel.cs, que deve estar localizado na pasta Models/InputModels.

O processo de registro requer os campos Email, FirstName, LastName, Password e ConfirmedPassword. Todos esses campos são obrigatórios, portanto, incluiremos o atributo [Required]. Também incluiremos o atributo [EmailAddress] para o campo Email. Podemos adicionar atributos adicionais, como comprimento mínimo e máximo, conforme desejado. No entanto, para os fins desta demonstração, vamos nos ater a esses atributos.

public class RegisterInputModel { [EmailAddress] public string Email { get; set; }

\[Required\]
public string FirstName { get; set; }

\[Required\]
public string LastName { get; set; }

\[Required\]
public string Password { get; set; }

\[Required\]
public string ConfirmedPassword { get; set; }

}

Em seguida, devemos adicionar um mapeamento ao arquivo MappingProfiles.cs que permitirá a conversão entre os modelos RegisterInputModel e User em ambas as direções.

CreateMap<RegisterInputModel, User>().ReverseMap();

Para manter a separação de preocupações, criaremos uma pasta Services. Cada módulo terá seu próprio serviço para interagir com o banco de dados. Podemos começar gerando um arquivo AuthService.cs e injetar o DataContext e Configuration.

Nosso primeiro método em AuthService.cs deve ser GenerateJwtToken, que recebe o email e a função como parâmetros e retorna um token JWT contendo informações do usuário.

public string GenerateJwtToken(string email, string role) { var issuer = this.configuration[“Jwt:Issuer”]; var audience = this.configuration[“Jwt:Audience”]; var key = Encoding.ASCII.GetBytes(this.configuration[“Jwt:Key”]); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new [] { new Claim(“Id”, Guid.NewGuid().ToString()), new Claim(JwtRegisteredClaimNames.Sub, email), new Claim(JwtRegisteredClaimNames.Email, email), new Claim(ClaimTypes.Role, role), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }), Expires = DateTime.UtcNow.AddMinutes(5), Issuer = issuer, Audience = audience, SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha512Signature) };

var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);

return tokenHandler.WriteToken(token);

}

Para fazer hash da senha, usaremos BCrypt.Net.BCrypt. Para começar, devemos instalar o pacote e adicioná-lo como uma instrução using no início do arquivo.

using BC = BCrypt.Net.BCrypt;

Depois disso, criaremos vários métodos auxiliares. Um verificará se um usuário com um determinado email existe, outro autenticará o usuário, e dois mais obterão um usuário por email e ID.

public bool IsAuthenticated(string email, string password) { var user = this.GetByEmail(email); return this.DoesUserExists(email) && BC.Verify(password, user.Password); }

public bool DoesUserExists(string email) { var user = this.dataContext.Users.FirstOrDefault(x => x.Email == email); return user != null; }

public User GetById(string id) { return this.dataContext.Users.FirstOrDefault(c => c.UserId == id); }

public User GetByEmail(string email) { return this.dataContext.Users.FirstOrDefault(c => c.Email == email); }

Antes de criarmos o método de registro, devemos primeiro criar um método para gerar um ID único. Este método pode ser definido como segue:

public class IdGenerator { public static string CreateLetterId(int length) { var random = new Random(); const string chars = “ABCDEFGHIJKLMNOPQRSTUVWXYZ”;

    return new string(Enumerable.Repeat(chars, length)
        .Select(s => s\[random.Next(s.Length)\]).ToArray());
}

}

Agora podemos prosseguir com a implementação do método Register. Dentro deste método, geraremos um ID único, verificaremos se ele já existe e, se for o caso, geraremos um novo. Então, faremos hash da senha do usuário e adicionaremos o novo usuário ao banco de dados.

public User RegisterUser(User model) { var id = IdGenerator.CreateLetterId(10); var existWithId = this.GetById(id);

while (existWithId != null)
{
    id = IdGenerator.CreateLetterId(10);
    existWithId = this.GetById(id);
}

model.UserId = id;
model.Password = BC.HashPassword(model.Password);
var userEntity = this.dataContext.Users.Add(model);
this.dataContext.SaveChanges();

return userEntity.Entity;

}

Se você ainda não criou AuthController, agora é a hora. Vá para a pasta Controllers e adicione AuthController que deve herdar a classe Controller. Também devemos adicionar o atributo [ApiController] e [Route(“[controller]”)] para que seja reconhecido pelo Swagger.

Depois disso, devemos injetar o mapper, authService e logger se usarmos um e criar RegisterMethod. Deve ser uma solicitação post acessível apenas por usuários não autenticados. Deve aceitar RegisterInputModel como argumento e verificar se o ModelState é válido. Se for, gerará um token jwt para esse usuário. O método inteiro deve parecer assim:

[AllowAnonymous] [HttpPost(“Register”)] public ActionResult Register(RegisterInputModel userModel) { try { if (ModelState.IsValid) { if (userModel.Password != userModel.ConfirmedPassword) { return BadRequest(“As senhas não correspondem!”); }

        if (this.authService.DoesUserExists(userModel.Email))
        {
            return BadRequest("Usuário já existe!");
        }

        var mappedModel = this.mapper.Map<RegisterInputModel, User>(userModel);
        mappedModel.Role = "User";
        
        var user = this.authService.RegisterUser(mappedModel);
        if (user != null)
        {
            var token = this.authService.GenerateJwtToken(user.Email, mappedModel.Role);
            return Ok(token);
        }
        return BadRequest("Email ou senha não estão corretos!");
    }

    return BadRequest(ModelState);
} catch (Exception error)
{
    logger.LogError(error.Message);
    return StatusCode(500);
}

}

Funcionalidade de Login

A funcionalidade de Login é similar, exceto que precisamos procurar um usuário no banco de dados. Primeiro devemos criar LoginInputModel.cs que desta vez terá apenas os campos Email e Password. Não esqueça de adicioná-lo também em MappingProfiles.cs, caso contrário, não funcionará.

public class LoginInputModel { [EmailAddress] [Required] public string Email { get; set; }

\[Required\]
public string Password { get; set; }

}

Então em AuthController.cs, crie um método Login que levará LoginInputModel como parâmetro e verificará se o usuário é autenticado. Se for, deve gerar um token. Caso contrário, deve retornar um erro.

[AllowAnonymous] [HttpPost(“Login”)] public ActionResult Login(LoginInputModel userModel) { try { if (ModelState.IsValid) { if (this.authService.IsAuthenticated(userModel.Email, userModel.Password)) { var user = this.authService.GetByEmail(userModel.Email); var token = this.authService.GenerateJwtToken(userModel.Email, user.Role);

            return Ok(token);
        }

        return BadRequest("Email ou senha não estão corretos!");
    }

    return BadRequest(ModelState);

} 
catch (Exception error)
{
    logger.LogError(error.Message);
    return StatusCode(500);
}

}

Adicionando CRUD para Eventos

Depois de concluírmos a autenticação, podemos desenvolver os endpoints para os eventos. Vamos criar operações CRUD completas. Identicamente como com os usuários, devemos criar um arquivo EventService.cs. Incluirá um método para obter um evento por ID, obter todos os eventos para um usuário específico, criar um novo evento, atualizar um evento existente e deletar um evento. O arquivo inteiro deve parecer assim:

public class EventService { private readonly DataContext dataContext;

public EventService(DataContext dataContext)
{
    this.dataContext = dataContext;
}

public Event\[\] GetAllForUser(string email)
{
    var user = this.dataContext.Users
        .FirstOrDefault(user => user.Email == email);

    return this.dataContext.Events
        .Include(ev => ev.UserEvents)
        .Where(e => e.UserEvents.FirstOrDefault(ue => ue.UserId == user.UserId) != null)
        .ToArray();
}

public Event GetById(string id)
{
    return this.dataContext.Events
        .Include(ev => ev.UserEvents)
        .FirstOrDefault(c => c.Id == id);
}

public Event Create(Event model)
{
    var id = IdGenerator.CreateLetterId(6);
    var existWithId = this.GetById(id);

    while (existWithId != null)
    {
        id = IdGenerator.CreateLetterId(6);
        existWithId = this.GetById(id);
    }

    model.Id = id;
    var eventEntity = this.dataContext.Events.Add(model);
    this.dataContext.SaveChanges();

    return eventEntity.Entity;
}

public Event Update(Event model)
{
    var eventEntity = this.dataContext.Events
        .Include(ev => ev.UserEvents)
        .FirstOrDefault(c => c.Id == model.Id);

    if (eventEntity != null)
    {
        eventEntity.Title = model.Title != null ? model.Title : eventEntity.Title;

        eventEntity.Date = model.Date != null ? model.Date : eventEntity.Date;

        eventEntity.Category = model.Category != null ? model.Category : eventEntity.Category;

        eventEntity.UserEvents = model.UserEvents.Count! > 0 ? model.UserEvents : eventEntity.UserEvents;

        this.dataContext.SaveChanges();
    }

    return eventEntity;
}

public Event Delete(string id)
{
    var eventEntity = this.GetById(id);

    if (eventEntity != null)
    {
        this.dataContext.Events.Remove(eventEntity);
        this.dataContext.SaveChanges();
    }

    return eventEntity;
}

}

Em seguida, você vai querer ir para o controller e configurar um método para cada solicitação.

Criaremos um EventBindingModel que será usado para armazenar todos os dados necessários do modelo Event.

Para o método GetAll, certifique-se de que ele usa uma solicitação GET e recupera o token do usuário, o decodifica e busca os eventos desse usuário.

[HttpGet] [Authorize] public ActionResult<EventBindingModel[]> GetAll() { try { var userEmail = this.authService.DecodeEmailFromToken(this.Request.Headers[“Authorization”]); var events = this.eventService.GetAllForUser(userEmail); return Ok(this.mapper.Map<Event[], EventBindingModel[]> (events));

} catch (Exception error)
{
    logger.LogError(error.Message);
    return StatusCode(500);
}

}

… public string DecodeEmailFromToken(string token) { var decodedToken = new JwtSecurityTokenHandler(); var indexOfTokenValue = 7; var t = decodedToken.ReadJwtToken(token.Substring(indexOfTokenValue));

return t.Payload.FirstOrDefault(x => x.Key == "email").Value.ToString();

} …

Get by id também deve ser uma solicitação GET com id como parâmetro.

[HttpGet(“{id}”)] [Authorize] public ActionResult GetById(string id) { try { var eventEntity = this.eventService.GetById(id);

    if (eventEntity != null)
    {
        return Ok(eventEntity);
    }

    return NotFound();
} 
catch (Exception error)
{
    logger.LogError(error.Message);
    return StatusCode(500);
}

}

Endpoint de Delete será uma solicitação DELETE e também levará id como argumento.

[HttpDelete(“{id}”)] [Authorize(Roles = “Administrator”)] public ActionResult Delete(string id) { try { var eventEntity = this.eventService.Delete(id);

    if (eventEntity != null)
    {
        return Ok(eventEntity);
    }

    return NotFound();
} 
catch (Exception error)
{
    logger.LogError(error.Message);
    return StatusCode(500);
}

}

Para simplificar o processo de adicionar ou atualizar registros de eventos, vamos criar um EventInputModel especificamente para operações Create e Update. Este modelo só exigirá que fornecemos as propriedades essenciais para userEvents, incluindo o título, categoria, data, userId e eventId. Usando este modelo, eliminamos a necessidade de especificar todas as propriedades do modelo Event para cada operação.

[HttpPost] public ActionResult Create(EventInputModel model) { try { if (ModelState.IsValid) { var mappedModel = this.mapper.Map < EventInputModel, Event > (model); var eventEntity = this.eventService.Create(mappedModel);

        return Ok(eventEntity);
    }

    return BadRequest(ModelState);
} 
catch (Exception error)
{
    logger.LogError(error.Message);
    return StatusCode(500);
}

}

Update será uma solicitação PUT e também levará EventInputModel como parâmetro.

[HttpPut] public ActionResult Update(EventInputModel model) { try { if (ModelState.IsValid) { var mappedModel = this.mapper.Map<EventInputModel, Event>(model); var eventEntity = this.eventService.Update(mappedModel);

        if (eventEntity != null)
        {
            return Ok(eventEntity);
        }

        return NotFound();
    }

    return BadRequest(ModelState);
} 
catch (Exception error)
{
    logger.LogError(error.Message);
    return StatusCode(500);
}

}

Adicionando Autorização Baseada em Funções

Para restringir certas ações para funções de usuário específicas, podemos usar autorização baseada em funções. Em nosso cenário, por exemplo, queremos limitar o acesso aos endpoints Create, Update e Delete para eventos aos usuários com uma função de Administrador.

Para configurar isso, precisamos adicionar app.UseAuthorization(); ao nosso arquivo Program.cs. Então, para cada endpoint que requer acesso restrito, adicionaremos o atributo [Authorize], que especificará as funções permitidas. Por exemplo, podemos garantir que apenas Administradores possam acessar o endpoint Delete.

… [Authorize(Roles = “Administrator”)] public ActionResult Delete(string id) …

Criando DbSeeder

Ao executar nossa aplicação, muitas vezes queremos ter alguns dados pré-populados em nosso banco de dados, seja para fins de testes ou outras razões. É aí que vem a semeadura. Para começar, precisamos definir os dados que queremos usar.

Para fazer isso, podemos criar uma pasta Resources e adicionar dois arquivos JSON: um para usuários e outro para eventos. Esses arquivos devem conter os dados que queremos preencher no banco de dados. Por exemplo, nossos arquivos podem parecer assim:

[ { “UserId”: “USERABCDE”, “FirstName”: “Kate”, “LastName”: “Lorenz”, “Password”: “kate.lorenz”, “Email”: “klorenz@hrcorp.com”, “Role”: “Administrator” }, { “UserId”: “ASERABCDE”, “FirstName”: “Anthony”, “LastName”: “Murray”, “Password”: “anthony.murray”, “Email”: “amurray@hrcorp.com”, “Role”: “User” } ]

Em seguida, devemos criar uma classe DbSeeder que inclui um método Seed. Este método lerá os dados que definimos anteriormente e os preencherá no banco de dados. Para fazer isso, precisamos passar o dbContext como parâmetro.

public class DBSeeder { public static void Seed(DataContext dbContext) { ArgumentNullException.ThrowIfNull(dbContext, nameof(dbContext)); dbContext.Database.EnsureCreated();

    var executionStrategy = dbContext.Database.CreateExecutionStrategy();

    executionStrategy.Execute(
        () => {
            using(var transaction = dbContext.Database.BeginTransaction())
            {
                try
                {
                    // Seed Users
                    if (!dbContext.Users.Any())
                    {
                        var usersData = File.ReadAllText("./Resources/users.json");
                        var parsedUsers = JsonConvert.DeserializeObject <User\[\]>(usersData);
                        foreach(var user in parsedUsers)
                        {
                            user.Password = BC.HashPassword(user.Password);
                        }

                        dbContext.Users.AddRange(parsedUsers);
                        dbContext.SaveChanges();
                    }

                    // Seed Events
                    if (!dbContext.Events.Any())
                    {
                        var eventsData = File.ReadAllText("./Resources/events.json");
                        var parsedEvents = JsonConvert.DeserializeObject <Event\[\]>(eventsData);

                        dbContext.Events.AddRange(parsedEvents);
                        dbContext.SaveChanges();
                    }

                    transaction.Commit();
                } 
                catch (Exception ex)
                {
                    transaction.Rollback();
                }
            }
        });
}

}

Depois disso, em nossa pasta Helpers, devemos criar uma extensão de inicializador de banco de dados, que vai executar o método Seed.

public static class DBInitializerExtension { public static IApplicationBuilder UseSeedDB(this IApplicationBuilder app) { ArgumentNullException.ThrowIfNull(app, nameof(app)); using var scope = app.ApplicationServices.CreateScope();

    var services = scope.ServiceProvider;
    var context = services.GetRequiredService < DataContext > ();

    DBSeeder.Seed(context);

    return app;
}

}

Por fim, precisamos abrir o arquivo Program.cs e adicionar o método app.UseSeedDB(). Isso garante que quando nossa aplicação inicia, ela verificará se há algum dado no banco de dados. Se não houver, o método Seed que criamos anteriormente preencherá automaticamente com os dados que definimos.

Adicionando CORS

Para habilitar o compartilhamento de recursos de origem cruzada (CORS) para nossos endpoints, precisamos adicionar um serviço cors no arquivo Program.cs. Neste caso, permitiremos o acesso de qualquer região, mas você pode especificar um domínio específico se preferir.

builder.Services.AddCors(policyBuilder => policyBuilder.AddDefaultPolicy(policy => policy.WithOrigins(”*”) .AllowAnyHeader() .AllowAnyHeader()) );

E depois disso, adicione o método app.UseCors();.

Isso nos permitirá acessar nossa API a partir de uma aplicação front-end.

Você pode ler mais sobre Cross-Origin Resource Sharing (CORS) aqui.

Para Resumir Tudo…

Criar uma API com autorização por funções com ASP.NET Core é um aspecto crucial quando se trata de construir aplicações web seguras e escaláveis. Ao usar autorização baseada em funções, você pode controlar o acesso aos recursos da sua API com base nas funções atribuídas aos seus usuários. Isso garante que apenas usuários autorizados possam acessar dados sensíveis ou executar ações críticas, tornando sua aplicação mais segura e confiável. Neste tutorial, vimos como criar uma API simples com autorização por funções do zero. Você pode visualizar todo o código da demonstração no GitHub.

Se você quiser ver como conectar a API criada aqui e construir uma aplicação front-end para ela com App BuilderTM, leia a Parte II deste post do blog – Criando uma Aplicação no App Builder Usando API com Autorização por Funções.

Desenvolvimento Web