Publicada:

.NET 6.0 - Tutorial de autorización basada en roles con API de ejemplo

Tutorial creado con .NET 6.0

Otras versiones disponibles:

En este tutorial, veremos un ejemplo simple de cómo implementar el control de acceso/autorización basado en funciones en una API de .NET 6.0 con C#. El ejemplo se basa en otro tutorial que publiqué recientemente que se centra en JWT authentication in .NET 6.0, este tutorial se ha ampliado para incluir autorización basada en funciones/control de acceso además de la autenticación JWT.

Puntos finales de la API

La API de ejemplo tiene solo tres puntos finales/rutas para demostrar la autenticación y la autorización basada en roles:

  • /users/authenticate: ruta pública que acepta solicitudes HTTP POST con nombre de usuario y contraseña en el cuerpo. Si el nombre de usuario y la contraseña son correctos, se devuelve un token de autenticación JWT.
  • /users: ruta segura restringida solo a usuarios "Admin", acepta solicitudes HTTP GET y devuelve una lista de todos los usuarios si el encabezado de autorización HTTP contiene un token JWT válido y el usuario está en el rol de "Admin". Si no hay un token de autenticación o el token no es válido o el usuario no tiene el rol de "Admin", se devuelve una respuesta 401 Unauthorized.
  • /users/{id}: ruta segura restringida a usuarios autenticados en cualquier rol, acepta solicitudes HTTP GET y devuelve el registro de usuario para el parámetro id especificado si la autorización es exitosa. Tenga en cuenta que los usuarios "Admin" pueden acceder a todos los registros de usuario, mientras que otros roles (por ejemplo, "User") solo pueden acceder a su propio registro de usuario.

Base de datos EF Core InMemory para pruebas

Para mantener el código API lo más simple posible, está configurado para usar el proveedor de base de datos EF Core InMemory que permite a Entity Framework Core crear y conectarse a una base de datos en memoria en lugar de tener que instalar un servidor de base de datos real. Esto se puede cambiar fácilmente a un proveedor de base de datos real cuando esté listo para trabajar con una base de datos como SQL Server, Oracle, MySql, etc. Para ver un ejemplo de API que usa SQLite en desarrollo y SQL Server en producción, consulte .NET 6.0 - User Registration and Login Tutorial with Example API.

Código en GitHub

El proyecto tutorial está disponible en GitHub en https://github.com/cornflourblue/dotnet-6-role-based-authorization-api.

Contenido del tutorial


Herramientas necesarias para ejecutar localmente el ejemplo de autorización basada en funciones de .NET 6.0

Para desarrollar y ejecutar aplicaciones .NET 6.0 localmente, descargue e instale lo siguiente:

  • .NET SDK: incluye el tiempo de ejecución de .NET y las herramientas de línea de comandos
  • Visual Studio Code: editor de código que se ejecuta en Windows, Mac y Linux
  • Extensión de C# para Visual Studio Code: agrega compatibilidad con VS Code para desarrollar aplicaciones .NET


Ejecute localmente la API de autorización basada en funciones de .NET 6.0

  1. Descargue o clone el código del proyecto del tutorial desde https://github.com/cornflourblue/dotnet-6-role-based-authorization-api
  2. Inicie la API ejecutando dotnet run desde la línea de comando en la carpeta raíz del proyecto (donde se encuentra el archivo WebApi.csproj), debería ver el mensaje Now listening on: http://localhost:4000. Siga las instrucciones a continuación para probar con Postman o conectarse con una de las aplicaciones de una sola página de ejemplo disponibles (Angular, React o Vue).


Depuración en VS Code

Puede iniciar la aplicación en modo de depuración en VS Code abriendo la carpeta raíz del proyecto en VS Code y presionando F5 o seleccionando Depurar -> Inicie la depuración desde el menú superior. La ejecución en modo de depuración le permite adjuntar puntos de interrupción para pausar la ejecución y recorrer el código de la aplicación. Para obtener más información sobre la depuración de .NET en VS Code, consulte VS Code + .NET - Debug a .NET Web App in Visual Studio Code.

Antes de ejecutar en producción

Antes de ejecutar en producción, también asegúrese de actualizar la propiedad Secret en el archivo appsettings.json, se usa para firmar y verificar los tokens JWT para la autenticación, cámbielo a una cadena aleatoria para asegurarse de que nadie más pueda generar un JWT con el mismo secreto y obtener acceso no autorizado a su API. Una forma rápida y fácil es unir un par de GUID para crear una cadena aleatoria larga (por ejemplo, desde https://www.guidgenerator.com/).


Pruebe la API .NET 6.0 con Postman

Postman es una gran herramienta para probar API, puede descargarla en https://www.postman.com/downloads.

A continuación, se incluyen instrucciones sobre cómo usar Postman para autenticar al usuario de prueba "admin" para obtener un token JWT de la API y luego realizar una solicitud autenticada con el token para recuperar una lista de usuarios. de la API.

Cómo autenticar a un usuario con Postman

Para autenticar a un usuario con la API y obtener un token JWT, siga estos pasos:

  1. Abra una nueva pestaña de solicitud haciendo clic en el botón más (+) al final de las pestañas.
  2. Cambie el método HTTP a POST con el selector desplegable a la izquierda del campo de entrada de URL.
  3. En el campo URL ingrese la dirección de la ruta de autenticación de su API local - http://localhost:4000/users/authenticate.
  4. Seleccione la pestaña Body debajo del campo URL, cambie el botón de opción de tipo de cuerpo a raw y cambie el selector desplegable de formato a JSON.
  5. Ingrese un objeto JSON que contenga el nombre de usuario y la contraseña del administrador en el área de texto Body: { "username": "admin", "password": "admin" }
  6. Haga clic en el botón Send, debería recibir una respuesta "200 OK" que contiene los detalles del usuario y un token JWT en el cuerpo de la respuesta.

Captura de pantalla de Postman después de enviar la solicitud y autenticar al usuario:


Cómo realizar una solicitud autenticada para recuperar todos los usuarios

Para realizar una solicitud autenticada utilizando el token JWT del paso anterior, siga estos pasos:

  1. Abra una nueva pestaña de solicitud haciendo clic en el botón más (+) al final de las pestañas.
  2. Cambie el método HTTP a GET con el selector desplegable a la izquierda del campo de entrada de URL.
  3. En el campo URL ingrese la dirección de la ruta de los usuarios de su API local - http://localhost:4000/users.
  4. Seleccione la pestaña Authorization debajo del campo URL, cambie el tipo a Bearer Token en el selector desplegable de tipo y pegue el token JWT del paso anterior en el campo Token.
  5. Haga clic en el botón Send, debería recibir una respuesta "200 OK" que contiene una matriz JSON con todos los registros de usuario en el sistema (solo los dos usuarios de prueba en el ejemplo).

NOTA: Esta ruta está restringida a los usuarios con el rol de Admin, otros usuarios recibirán una respuesta 401 Unauthorized.

Captura de pantalla de Postman después de realizar una solicitud autenticada para obtener todos los usuarios:


Ejecute una aplicación de cliente Angular con la API de autenticación basada en funciones de .NET 6.0

Para obtener detalles completos sobre la aplicación Angular de ejemplo, consulte la publicación Angular 10 - Role Based Authorization Tutorial with Example. Pero para ponerse en marcha rápidamente, solo siga los pasos a continuación.

  1. Instalar Node.js y npm desde https://nodejs.org.
  2. Descargue o clone el código del tutorial de Angular 10 desde https://github.com/cornflourblue/angular-10-role-based-authorization-example
  3. Instale todos los paquetes npm necesarios ejecutando npm install desde la línea de comando en la carpeta raíz del proyecto (donde se encuentra el paquete.json).
  4. Elimine o comente la línea debajo del comentario // provider used to create fake backend ubicado en el archivo /src/app/app.module.ts.
  5. Inicie la aplicación ejecutando npm start desde la línea de comando en la carpeta raíz del proyecto, esto iniciará un navegador que muestra la aplicación de ejemplo de Angular y debe estar conectado con el API de autorización basada en roles de .NET 6.0 que ya tiene en ejecución.


Ejecute una aplicación de cliente React con la API de autenticación basada en funciones de .NET 6.0

Para obtener detalles completos sobre la aplicación React de ejemplo, consulte la publicación React - Role Based Authorization Tutorial with Example. Pero para ponerse en marcha rápidamente, solo siga los pasos a continuación.

  1. Instalar Node.js y npm desde https://nodejs.org.
  2. Descargue o clone el código del tutorial de React desde https://github.com/cornflourblue/react-role-based-authorization-example
  3. Instale todos los paquetes npm necesarios ejecutando npm install desde la línea de comando en la carpeta raíz del proyecto (donde se encuentra el paquete.json).
  4. Elimine o comente las 2 líneas debajo del comentario // setup fake backend ubicado en el archivo /src/index.jsx.
  5. Inicie la aplicación ejecutando npm start desde la línea de comando en la carpeta raíz del proyecto, esto iniciará un navegador que muestra la aplicación de ejemplo de React y debe estar conectado con el API de autorización basada en roles de .NET 6.0 que ya tiene en ejecución.


Ejecute una aplicación de cliente Vue.js con la API de autenticación basada en funciones de .NET

Para obtener detalles completos sobre la aplicación Vue.js de ejemplo, consulte la publicación Vue.js - Role Based Authorization Tutorial with Example. Pero para ponerse en marcha rápidamente, solo siga los pasos a continuación.

  1. Instalar Node.js y npm desde https://nodejs.org.
  2. Descargue o clone el código del tutorial de Vue.js desde https://github.com/cornflourblue/vue-role-based-authorization-example
  3. Instale todos los paquetes npm necesarios ejecutando npm install desde la línea de comando en la carpeta raíz del proyecto (donde se encuentra el paquete.json).
  4. Elimine o comente las 2 líneas debajo del comentario // setup fake backend ubicado en el archivo /src/index.js.
  5. Inicie la aplicación ejecutando npm start desde la línea de comando en la carpeta raíz del proyecto, esto iniciará un navegador que muestra la aplicación de ejemplo Vue.js y debe estar conectado con el API de autorización basada en roles de .NET 6.0 que ya tiene en ejecución.
 

.NET 6.0 Estructura de proyecto de control de acceso basado en funciones

El proyecto del tutorial está organizado en las siguientes carpetas:

Authorization
Contiene las clases responsables de implementar la autenticación JWT personalizada y la autorización basada en roles en la API.

Controllers
Defina los puntos finales / rutas para la API web, los controladores son las puertas de enlace a la API web para las aplicaciones cliente a través de solicitudes http.

Models
Representa modelos de solicitud y respuesta para métodos de controlador, los modelos de solicitud definen los parámetros para las solicitudes entrantes y los modelos de respuesta definen los datos que se devuelven.

Services
Contiene lógica de negocios, validación y código de acceso a la base de datos.

Entities
Representa los datos de la aplicación que se almacenan en la base de datos.
Entity Framework Core (EF Core) asigna datos relacionales de la base de datos a instancias de objetos de entidad de C# que se utilizarán dentro de la aplicación para la gestión de datos y las operaciones CRUD.

Helpers
Cualquier cosa que no encaje en las carpetas anteriores.

Haga clic en cualquiera de los enlaces a continuación para ir a una descripción de cada archivo junto con su código:

 

Atributo anónimo permitido personalizado de .NET

Ruta: /Authorization/AllowAnonymousAttribute.cs

El atributo personalizado [AllowAnonymous] se usa para permitir el acceso anónimo a métodos de acción específicos de controladores que están decorados con el atributo [Authorize]. Se utiliza en el controlador de usuarios para permitir el acceso anónimo al método de acción autenticar. El atributo de autorización personalizado a continuación omite la autorización si el método de acción está decorado con [AllowAnonymous].

Creé un permiso anónimo personalizado (en lugar de usar uno integrado) para mantener la coherencia con el atributo de autorización personalizado y para evitar errores de referencia ambiguos entre espacios de nombres.

namespace WebApi.Authorization;

[AttributeUsage(AttributeTargets.Method)]
public class AllowAnonymousAttribute : Attribute
{ }
 

Atributo de autorización personalizado de .NET

Ruta: /Authorization/AuthorizeAttribute.cs

El atributo personalizado [Authorize] se agrega a los métodos de acción del controlador que requieren que el usuario esté autenticado y, opcionalmente, tenga un rol específico. Si se especifica un rol (p. ej., [Authorize(Role.Admin)]), la ruta está restringida a los usuarios en ese rol; de lo contrario, la ruta está restringida a todos los usuarios autenticados, independientemente del rol.

Cuando una clase de controlador está decorada con el atributo [Authorize], todos los métodos de acción en el controlador están restringidos a solicitudes autorizadas, excepto los métodos decorados con el atributo personalizado [AllowAnonymous] anterior.

La autorización se realiza mediante el método OnAuthorization que comprueba si hay un usuario autenticado adjunto a la solicitud actual (context.HttpContext.Items["User"]) y que el usuario tiene un rol autorizado (si se especifica).

En caso de autorización exitosa, no se realiza ninguna acción y la solicitud se pasa al método de acción del controlador; si la autorización falla, se devuelve una respuesta 401 Unauthorized.

namespace WebApi.Authorization;

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using WebApi.Entities;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorizeAttribute : Attribute, IAuthorizationFilter
{
    private readonly IList<Role> _roles;

    public AuthorizeAttribute(params Role[] roles)
    {
        _roles = roles ?? new Role[] { };
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        // skip authorization if action is decorated with [AllowAnonymous] attribute
        var allowAnonymous = context.ActionDescriptor.EndpointMetadata.OfType<AllowAnonymousAttribute>().Any();
        if (allowAnonymous)
            return;

        // authorization
        var user = (User)context.HttpContext.Items["User"];
        if (user == null || (_roles.Any() && !_roles.Contains(user.Role)))
        {
            // not logged in or role not authorized
            context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized };
        }
    }
}
 

Middleware JWT personalizado de .NET

Ruta: /Authorization/JwtMiddleware.cs

El middleware JWT personalizado extrae el token JWT del encabezado de solicitud Authorization (si lo hay) y lo valida con el método jwtUtils.ValidateToken(). Si la validación es exitosa, se devuelve la identificación de usuario del token y el objeto de usuario autenticado se adjunta a la colección HttpContext.Items para que sea accesible dentro del alcance de la solicitud actual.

Si la validación del token falla o no hay token, la solicitud solo puede acceder a rutas públicas (anónimas) porque no hay un objeto de usuario autenticado adjunto al contexto HTTP. La lógica de autorización que verifica el objeto de usuario adjunto está en el atributo de autorización personalizado, si la autorización falla, devuelve una respuesta 401 Unauthorized .

namespace WebApi.Authorization;

using Microsoft.Extensions.Options;
using WebApi.Helpers;
using WebApi.Services;

public class JwtMiddleware
{
    private readonly RequestDelegate _next;
    private readonly AppSettings _appSettings;

    public JwtMiddleware(RequestDelegate next, IOptions<AppSettings> appSettings)
    {
        _next = next;
        _appSettings = appSettings.Value;
    }

    public async Task Invoke(HttpContext context, IUserService userService, IJwtUtils jwtUtils)
    {
        var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
        var userId = jwtUtils.ValidateJwtToken(token);
        if (userId != null)
        {
            // attach user to context on successful jwt validation
            context.Items["User"] = userService.GetById(userId.Value);
        }

        await _next(context);
    }
}
 

Utilidades de .NET JWT

Ruta: /Authorization/JwtUtils.cs

La clase utils de JWT contiene métodos para generar y validar tokens JWT.

El método GenerateJwtToken() devuelve un token JWT que es válido durante 7 días, contiene la identificación del user especificado como "id", lo que significa que la carga del token contendrá la propiedad "id": <userId> (por ejemplo, "id": 1). El token se crea con la clase JwtSecurityTokenHandler y se firma digitalmente con la clave secreta almacenada en el archivo configuración de la aplicación.

El método ValidateJwtToken() intenta validar el token JWT proporcionado y devolver la identificación del usuario ("id") de las reclamaciones del token. Si la validación falla, se devuelve nulo.

namespace WebApi.Authorization;

using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using WebApi.Entities;
using WebApi.Helpers;

public interface IJwtUtils
{
    public string GenerateJwtToken(User user);
    public int? ValidateJwtToken(string token);
}

public class JwtUtils : IJwtUtils
{
    private readonly AppSettings _appSettings;

    public JwtUtils(IOptions<AppSettings> appSettings)
    {
        _appSettings = appSettings.Value;
    }

    public string GenerateJwtToken(User user)
    {
        // generate token that is valid for 7 days
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()) }),
            Expires = DateTime.UtcNow.AddDays(7),
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };
        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }

    public int? ValidateJwtToken(string token)
    {
        if (token == null)
            return null;

        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
        try
        {
            tokenHandler.ValidateToken(token, new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false,
                // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
                ClockSkew = TimeSpan.Zero
            }, out SecurityToken validatedToken);

            var jwtToken = (JwtSecurityToken)validatedToken;
            var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);

            // return user id from JWT token if validation successful
            return userId;
        }
        catch
        {
            // return null if validation fails
            return null;
        }
    }
}
 

Controlador de usuarios de .NET

Ruta: /Controllers/UsersController.cs

El controlador de usuarios de .NET define y maneja todas las rutas/puntos finales para la API que se relacionan con los usuarios, esto incluye la autenticación y las operaciones CRUD estándar. Dentro de cada ruta, el controlador llama al servicio de usuario para realizar la acción requerida, lo que permite que el controlador permanezca "ajustado" y completamente separado de la lógica comercial y el código de acceso a los datos.

Las acciones del controlador están protegidas con JWT utilizando el atributo de nivel de clase [Authorize], con la excepción del método Authenticate() que permite el acceso público anulando el atributo [Authorize] en el controlador con el atributo [AllowAnonymous] en el método de acción. Elegí este enfoque para que cualquier nuevo método de acción agregado al controlador sea seguro de forma predeterminada a menos que se haga público explícitamente. La lógica de autenticación se encuentra en el atributo de autorización personalizado.

Cuando se especifica un rol en el atributo de autorización (p. ej., [Authorize(Role.Admin)]), la ruta está restringida a los usuarios en el rol o los roles especificados.

El método de acción GetById(int id) contiene una lógica de autorización personalizada adicional que permite que solo los usuarios administradores accedan a cualquier registro de usuario, los usuarios que no son administradores solo pueden acceder a su propio registro.

namespace WebApi.Controllers;

using Microsoft.AspNetCore.Mvc;
using WebApi.Authorization;
using WebApi.Entities;
using WebApi.Models.Users;
using WebApi.Services;

[Authorize]
[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
    private IUserService _userService;

    public UsersController(IUserService userService)
    {
        _userService = userService;
    }

    [AllowAnonymous]
    [HttpPost("[action]")]
    public IActionResult Authenticate(AuthenticateRequest model)
    {
        var response = _userService.Authenticate(model);
        return Ok(response);
    }

    [Authorize(Role.Admin)]
    [HttpGet]
    public IActionResult GetAll()
    {
        var users = _userService.GetAll();
        return Ok(users);
    }

    [HttpGet("{id:int}")]
    public IActionResult GetById(int id)
    {
        // only admins can access other user records
        var currentUser = (User)HttpContext.Items["User"];
        if (id != currentUser.Id && currentUser.Role != Role.Admin)
            return Unauthorized(new { message = "Unauthorized" });

        var user =  _userService.GetById(id);
        return Ok(user);
    }
}
 

Enumeración de funciones de .NET

Ruta: /Entities/Role.cs

La enumeración de funciones define todas las funciones disponibles en la API de ejemplo. Lo creé para evitar pasar roles como cadenas, así que en lugar de 'Admin' podemos usar Role.Admin.

namespace WebApi.Entities;

public enum Role
{
    Admin,
    User
}
 

Entidad de usuario de .NET

Ruta: /Entities/User.cs

La clase de entidad de usuario representa los datos de un usuario en la aplicación. Las clases de entidad representan los datos centrales de una aplicación .NET y se usan comúnmente con un ORM como Entity Framework para asignar datos almacenados en una base de datos relacional (por ejemplo, SQL Server, MySQL, SQLite, etc.). Las entidades también se utilizan para devolver datos de respuesta HTTP de los métodos de acción del controlador y para pasar datos entre diferentes partes de la aplicación (por ejemplo, entre servicios y controladores). Si es necesario devolver varios tipos de entidades u otros datos personalizados desde un método de controlador, se debe crear una clase de modelo personalizado en la carpeta Models para la respuesta.

La propiedad PasswordHash está decorada con el atributo [JsonIgnore] para evitar que se devuelva en las respuestas API HTTP.

namespace WebApi.Entities;

using System.Text.Json.Serialization;

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Username { get; set; }
    public Role Role { get; set; }

    [JsonIgnore]
    public string PasswordHash { get; set; }
}
 

Excepción de la aplicación .NET

Ruta: /Helpers/AppException.cs

La excepción de aplicación es una clase de excepción personalizada que se utiliza para diferenciar entre excepciones controladas y no controladas en la API de .NET. Las excepciones controladas son generadas por el código de la aplicación y se utilizan para devolver mensajes de error amigables, por ejemplo, excepciones de lógica de negocios o validación causadas por parámetros de solicitud no válidos, mientras que las excepciones no controladas son generadas por el marco .NET o causadas por errores en el código de la aplicación.

namespace WebApi.Helpers;

using System.Globalization;

// custom exception class for throwing application specific exceptions (e.g. for validation) 
// that can be caught and handled within the application
public class AppException : Exception
{
    public AppException() : base() {}

    public AppException(string message) : base(message) { }

    public AppException(string message, params object[] args) 
        : base(String.Format(CultureInfo.CurrentCulture, message, args))
    {
    }
}
 

Clase de configuración de la aplicación .NET

Ruta: /Helpers/AppSettings.cs

La clase de configuración de la aplicación contiene propiedades definidas en el archivo appsettings.json y se usa para acceder a la configuración de la aplicación a través de objetos que se inyectan en las clases mediante la extensión . NET integrado en el sistema de inyección de dependencia (DI). Por ejemplo, el servicio de usuario accede a la configuración de la aplicación a través de un IOptions<AppSettings> appSettings objeto que se inyecta en el constructor.

La asignación de secciones de configuración a clases se realiza al inicio en el archivo Program.cs.

namespace WebApi.Helpers;

public class AppSettings
{
    public string Secret { get; set; }
}
 

Contexto de datos .NET

Ruta: /Helpers/DataContext.cs

La clase de contexto de datos se utiliza para acceder a los datos de la aplicación a través de Entity Framework. Se deriva de la clase DbContext de Entity Framework y tiene una propiedad Users pública para acceder y administrar los datos de los usuarios. El contexto de datos es utilizado por el servicio de usuario para manejar todas las operaciones de datos de bajo nivel (CRUD).

options.UseInMemoryDatabase() configura Entity Framework para crear y conectarse a una base de datos en memoria para que la API se pueda probar sin una base de datos real, esto se puede actualizar fácilmente para conectarse a una base de datos real servidor como SQL Server, Oracle, MySql, etc. Para ver una API de ejemplo que usa SQLite en desarrollo y SQL Server en producción, consulte .NET 6.0 - User Registration and Login Tutorial with Example API.

namespace WebApi.Helpers;

using Microsoft.EntityFrameworkCore;
using WebApi.Entities;

public class DataContext : DbContext
{
    public DbSet<User> Users { get; set; }

    private readonly IConfiguration Configuration;

    public DataContext(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        // in memory database used for simplicity, change to a real db for production applications
        options.UseInMemoryDatabase("TestDb");
    }
}
 

Middleware del controlador de errores globales .NET

Ruta: /Helpers/ErrorHandlerMiddleware.cs

El controlador de errores global se utiliza para capturar todos los errores y eliminar la necesidad de un código de manejo de errores duplicado en toda la API de .NET. Está configurado como middleware en el archivo Program.cs.

Los errores de tipo AppException se tratan como errores personalizados (específicos de la aplicación) que devuelven una respuesta 400 Bad Request, la clase KeyNotFoundException incorporada de .NET se usa para devolver respuestas 404 Not Found, todas las demás excepciones no se controlan y devuelven una respuesta 500 Internal Server Error.

Consulte el servicio de usuario para ver ejemplos de errores personalizados y errores no encontrados generados por la API.

namespace WebApi.Helpers;

using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;

public class ErrorHandlerMiddleware
{
    private readonly RequestDelegate _next;

    public ErrorHandlerMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception error)
        {
            var response = context.Response;
            response.ContentType = "application/json";

            switch(error)
            {
                case AppException e:
                    // custom application error
                    response.StatusCode = (int)HttpStatusCode.BadRequest;
                    break;
                case KeyNotFoundException e:
                    // not found error
                    response.StatusCode = (int)HttpStatusCode.NotFound;
                    break;
                default:
                    // unhandled error
                    response.StatusCode = (int)HttpStatusCode.InternalServerError;
                    break;
            }

            var result = JsonSerializer.Serialize(new { message = error?.Message });
            await response.WriteAsync(result);
        }
    }
}
 

Modelo de solicitud de autenticación de .NET

Ruta: /Models/Users/AuthenticateRequest.cs

El modelo de solicitud de autenticación define los parámetros para las solicitudes POST entrantes a la ruta /users/authenticate, se adjunta a la ruta configurándola como el parámetro para Authenticate método de acción del controlador de usuarios. Cuando la ruta recibe una solicitud HTTP POST, los datos del cuerpo se vinculan a una instancia de la clase AuthenticateRequest, se validan y se pasan al método.

Las anotaciones de datos .NET se utilizan para manejar automáticamente la validación del modelo, el atributo [Required] establece tanto el nombre de usuario como la contraseña como campos obligatorios, por lo que si falta alguno, la API devuelve un mensaje de error de validación. .

namespace WebApi.Models.Users;

using System.ComponentModel.DataAnnotations;

public class AuthenticateRequest
{
    [Required]
    public string Username { get; set; }

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

Modelo de respuesta de autenticación de .NET

Ruta: /Models/Users/AuthenticateResponse.cs

El modelo de respuesta de autenticación define los datos devueltos por el método Authenticate del controlador de usuarios después de una autenticación exitosa. Incluye detalles básicos de usuario y un token JWT.

namespace WebApi.Models.Users;

using WebApi.Entities;

public class AuthenticateResponse
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Username { get; set; }
    public Role Role { get; set; }
    public string Token { get; set; }

    public AuthenticateResponse(User user, string token)
    {
        Id = user.Id;
        FirstName = user.FirstName;
        LastName = user.LastName;
        Username = user.Username;
        Role = user.Role;
        Token = token;
    }
}
 

Servicio de usuario .NET

Ruta: /Services/UserService.cs

El servicio de usuario contiene la lógica principal para la autenticación, la generación de tokens JWT y la obtención de datos de usuario.

La parte superior del archivo UserService.cs contiene la interfaz IUserService que define los métodos públicos para el servicio de usuario, debajo de la interfaz se encuentra el clase concreto UserService que implementa la interfaz.

El método Authenticate() encuentra un usuario por nombre de usuario y verifica la contraseña contra la contraseña hash en la base de datos usando BCrypt, en caso de éxito, los detalles del usuario se devuelven con un token JWT. Para obtener más información sobre hash y verificación de contraseñas, consulte .NET 6.0 - Hash and Verify Passwords with BCrypt.

El método GetAll() devuelve una lista de todos los usuarios del sistema, y el método GetById() devuelve el usuario con la identificación especificada.

namespace WebApi.Services;

using BCrypt.Net;
using Microsoft.Extensions.Options;
using WebApi.Authorization;
using WebApi.Entities;
using WebApi.Helpers;
using WebApi.Models.Users;

public interface IUserService
{
    AuthenticateResponse Authenticate(AuthenticateRequest model);
    IEnumerable<User> GetAll();
    User GetById(int id);
}

public class UserService : IUserService
{
    private DataContext _context;
    private IJwtUtils _jwtUtils;
    private readonly AppSettings _appSettings;

    public UserService(
        DataContext context,
        IJwtUtils jwtUtils,
        IOptions<AppSettings> appSettings)
    {
        _context = context;
        _jwtUtils = jwtUtils;
        _appSettings = appSettings.Value;
    }


    public AuthenticateResponse Authenticate(AuthenticateRequest model)
    {
        var user = _context.Users.SingleOrDefault(x => x.Username == model.Username);

        // validate
        if (user == null || !BCrypt.Verify(model.Password, user.PasswordHash))
            throw new AppException("Username or password is incorrect");

        // authentication successful so generate jwt token
        var jwtToken = _jwtUtils.GenerateJwtToken(user);

        return new AuthenticateResponse(user, jwtToken);
    }

    public IEnumerable<User> GetAll()
    {
        return _context.Users;
    }

    public User GetById(int id) 
    {
        var user = _context.Users.Find(id);
        if (user == null) throw new KeyNotFoundException("User not found");
        return user;
    }
}
 

Configuración de la aplicación .NET 6 JSON

Ruta: /appsettings.json

El archivo appsettings.json es el archivo de configuración base en una aplicación .NET que contiene configuraciones para todos los entornos (p. ej., Development, Production ). Puede anular valores para diferentes entornos mediante la creación de archivos appsettings específicos del entorno (p. ej., appsettings.Development.json, appsettings.Production.json).

IMPORTANTE: La propiedad "Secret" se usa para firmar y verificar tokens JWT para autenticación, cámbielo a una cadena aleatoria para asegurarse de que nadie más pueda generar un JWT con el mismo secreto y obtener acceso no autorizado a su API. Una forma rápida y fácil es unir un par de GUID para crear una cadena aleatoria larga (por ejemplo, desde https://www.guidgenerator.com/).

{
    "AppSettings": {
        "Secret": "THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING"
    },
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
        }
    }
}
 

Programa .NET 6

Ruta: /Program.cs

El archivo de programa .NET 6 contiene top-level statements que el nuevo compilador C# 10 convierte en un método y clase Main() para el programa .NET . El método Main() es el punto de entrada para una aplicación .NET, cuando se inicia una aplicación busca el método Main() para comenzar la ejecución. Las top-level statements se pueden ubicar en cualquier parte del proyecto, pero generalmente se colocan en el archivo Program.cs, solo un archivo puede contener top-level statements dentro de una aplicación .NET.

La clase WebApplication maneja el inicio de la aplicación, la administración de por vida, la configuración del servidor web y más. Primero se crea un WebApplicationBuilder llamando al método estático WebApplication.CreateBuilder(args), el constructor se usa para configurar servicios para inyección de dependencia (DI), se crea una instancia de WebApplication llamando a builder.Build(), la instancia de la aplicación se usa para configurar la tubería de solicitud HTTP (middleware), luego la aplicación se inicia llamando a app.Run().

Envolví las secciones add services... y configure HTTP... entre llaves {} para agruparlas visualmente, los corchetes son completamente opcionales.

Internamente, la clase WebApplicationBuilder llama al método de extensión ConfigureWebHostDefaults() que configura el alojamiento para la aplicación web, incluida la configuración de Kestrel como servidor web, la adición de middleware de filtrado de host y la habilitación de IIS integración. Para obtener más información sobre la configuración predeterminada del generador, consulte https://docs.microsoft.com/aspnet/core/fundamentals/host/generic-host#default-builder-settings.

Usuarios de prueba

Se crean un par de usuarios de prueba cerca de la parte inferior del archivo en la base de datos en memoria, uno en cada rol (Admin y User).

using BCryptNet = BCrypt.Net.BCrypt;
using System.Text.Json.Serialization;
using WebApi.Authorization;
using WebApi.Entities;
using WebApi.Helpers;
using WebApi.Services;

var builder = WebApplication.CreateBuilder(args);

// add services to DI container
{
    var services = builder.Services;
    var env = builder.Environment;

    services.AddDbContext<DataContext>();
    services.AddCors();
    services.AddControllers().AddJsonOptions(x =>
    {
        // serialize enums as strings in api responses (e.g. Role)
        x.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
    });

    // configure strongly typed settings object
    services.Configure<AppSettings>(builder.Configuration.GetSection("AppSettings"));

    // configure DI for application services
    services.AddScoped<IJwtUtils, JwtUtils>();
    services.AddScoped<IUserService, UserService>();
}

var app = builder.Build();

// configure HTTP request pipeline
{
    // global cors policy
    app.UseCors(x => x
        .AllowAnyOrigin()
        .AllowAnyMethod()
        .AllowAnyHeader());

    // global error handler
    app.UseMiddleware<ErrorHandlerMiddleware>();

    // custom jwt auth middleware
    app.UseMiddleware<JwtMiddleware>();

    app.MapControllers();
}

// create hardcoded test users in db on startup
{
    var testUsers = new List<User>
    {
        new User { Id = 1, FirstName = "Admin", LastName = "User", Username = "admin", PasswordHash = BCryptNet.HashPassword("admin"), Role = Role.Admin },
        new User { Id = 2, FirstName = "Normal", LastName = "User", Username = "user", PasswordHash = BCryptNet.HashPassword("user"), Role = Role.User }
    };

    using var scope = app.Services.CreateScope();
    var dataContext = scope.ServiceProvider.GetRequiredService<DataContext>();
    dataContext.Users.AddRange(testUsers);
    dataContext.SaveChanges();
}

app.Run("http://localhost:4000");
 

Archivo .NET 6 CSProj

Ruta: /WebApi.csproj

El csproj (proyecto C#) es un archivo basado en MSBuild que contiene el marco de destino y la información de dependencia del paquete NuGet para la aplicación. La función ImplicitUsings está habilitada, lo que le dice al compilador que genere automáticamente un conjunto de directivas de uso globales basadas en el tipo de proyecto, eliminando la necesidad de incluir muchas declaraciones de uso comunes en cada archivo de clase. Las instrucciones de uso global se generan automáticamente cuando crea el proyecto y se pueden encontrar en el archivo /obj/Debug/net6.0/WebApi.GlobalUsings.g.cs.

Para obtener más información sobre el archivo de proyecto de C#, consulte .NET + MSBuild - C# Project File (.csproj) in a Nutshell.

<Project Sdk="Microsoft.NET.Sdk.Web">
    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="BCrypt.Net-Next" Version="4.0.2" />
        <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.2" />
        <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.2" />
        <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.15.1" />
    </ItemGroup>
</Project>

 


Suscríbete o Sígueme para actualizaciones

Suscríbete a mi canal de YouTube o sígueme en Twitter, Facebook o GitHub para recibir notificaciones cuando publique contenido nuevo.

Aparte de la codificación...

Actualmente estoy intentando viajar por Australia en motocicleta con mi esposa Tina en un par de Royal Enfield Himalayan. Puedes seguir nuestras aventuras en YouTube, Instagram y Facebook.


¿Necesita Ayuda .NET?

Buscar fiverr para encontrar ayuda rápidamente de desarrolladores .NET experimentados.