En los 2 post anteriores vimos algunas de las buenas prácticas que deberíamos implementar En este tercer post veremos algunas más relacionadas con la seguridad y las configuraciones.
Seguridad
Primero que nada, usar siempre el canal de comunicación HTTPS. Este es un protocolo que cifra la comunicación de extremo a extremo lo cual dificulta la lectura de los datos si es interceptada.
Por otro lado, para securizar nuestras APIS, el uso de tokens JWT es lo más utilizado. JWT es un estándar open-source que nos permite transmitir datos entre el cliente y el servidor en una estructura de objeto JSON de forma segura. Net posee un soporte integrado que facilita la implementación de su uso veamos cómo se configura:
builder.Services.AddAuthentication(opt => { opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { //Configuration in here }; });
Luego debemos registrarlo para que esté disponible:
app.UseAuthentication(); app.UseAuthorization();
No solamente es posible utilizarlo para autenticacion, podemos implementar autorizaci[on por medio de Clains de roles que vienen configurado en el JWT.
Ten presente que para esto necesitaras un Identity Provider las 2 herramientas más conocidas son: IdentityServer4, el cual soporta OAuth2 y OpenID connect, o también, puedes utilizar el servicio en la nube de Azure llamado Azure B2C. Puedes utilizar de forma gratuita soporta 50 mil inicios de sesión por mes y 50 usuarios registrados sin costo.
Configuración de servicios
A partir de la versión 6 de .net y en versiones superiores tenemos la configuración de los servicios en nuestra clase program. Veamos un ejemplo de cómo configurar CORS Policies para nuestras APIs.
builder.Services.AddCors(options => { options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader()); });
Este código funciona correctamente y no tendremos inconvenientes. A medida que hemos agregado configuraciones en nuestro clase Program será más largo hasta llegar a ser ilegible dependiendo el tamaño de nuestra aplicación. El mejor camino es escribir una clase de extensión estática para reducir la cantidad de líneas de código en nuestro archivo principal y ser más puntual. Recuerda Single responsibility principle.
public static class ServiceExtensions { public static void ConfigureCors(this IServiceCollection services) { services.AddCors(options => { options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader()); }); } }
Luego podremos volver a nuestro Program (program.cs) e invoquemos la extensión reduciendo el código de esta clase.
builder.Services.ConfigureCors();
Este es solo un ejemplo, pero es muy útil para muchas cosas, como por ejemplo escribir extensiones de servicios o extensión para la inyección de nuestros repositorios.
Configurar los entornos de ejecución
Al crear nuestra aplicación siempre estaremos trabajando en nuestro entorno de desarrollo. Es posible configurar varios entornos de ejecución para nuestra aplicación como un entorno de pruebas o producción. Por esta razón, la mejor de las prácticas, es tener separados nuestros entornos de ejecución.
Para realizar esta configuración, luego de crear nuestro proyecto, veremos que tenemos disponible el archivo appsettings.json. Al lado tiene un símbolo + el cual nos permite extenderlo y ver que ese archivo está conformado por appsettings.Development.json.
Es posible agregar 2 archivos nuevos: uno para test y otro para producción:
Una vez que tengamos diferenciados este archivo y configurados correctamente para cada entorno, dependerá de lo que tengamos configurado en nuestra variable de entorno Environment (ASPNETCORE_ENVIRONMENT) se tomará un archivo u otro.
Eliminar codigo duplicado
Un buen camino para evitar código duplicado es la utilización de ActionsFilters. Estos nos permiten ejecutar código antes o después de la ejecución de una solicitud. Si necesitamos por ejemplo ejecutar código de validación de nuestros modelos no es necesario escribirlos dentro de nuestros actions.
Seguramente siempre tenemos este código dentro de nuestros métodos:
if (!ModelState.IsValid) { }
Para evitar este código siempre en nuestros métodos podemos crear un filtro:
public class ModelValidationAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { if (!context.ModelState.IsValid) { context.Result = new BadRequestObjectResult(context.ModelState); } } }
public class ModelValidationAttribute : ActionFilterAttribute{ public override void OnActionExecuting(ActionExecutingContext context) { if (!context.ModelState.IsValid) { context.Result = new BadRequestObjectResult(context.ModelState); } }} |
El siguiente paso será registrar nuestro filtro como servicio:
builder.Services.AddScoped<ModelValidationAttribute>();
Con esto último quedará disponible para nuestros métodos Actions.
Routing
Una buena práctica en el enrutamiento es utilizar atributos en lugar de el enrutamiento normal. De esta manera los atributos nos ayudarán a hacer coincidir los nombres de los parámetros con los parámetros reales de los métodos. Por otro lado, lo hará más legible.
[Route("api/[controller]")] public class CustomerController: Controller { [Route("{id}")] [HttpGet] public IActionResult GetCustomerById(Guid id) { } }
o podemos usar un camino más corto:
[Route("api/customers")] public class CustomersController: Controller { [HttpGet("{id}")] public IActionResult GetCustomerById(Guid id) { } }
Recuerdo que siempre debes respetar la convención de nomenclaturas predeterminada para las rutas: nombres descriptivos para las acciones, pero para los endpoints sustantivos y verbos. Veamos un ejemplo:
[Route("api/customers")] public class CustomersController : Controller { [HttpGet] public IActionResult GetAllCustomers() { } [HttpGet("{id}"] public IActionResult GetCustomerById(Guid id) { } }
Cambiar el tipo de contenido retornado
El uso más común hoy es el retornar objetos json desde nuestras API. Pero en muchos casos me he encontrado con la necesidad de retornar un XML debido a que el cliente consumía este formato porque utilizaba WCF.
Debemos crear una configuración para que los servicios para formatear la respuesta según sea necesario. Primero, aceptaremos la negociación.
builder.Services.AddMvc(config => { // Add XML Content Negotiation config.RespectBrowserAcceptHeader = true; });
Usar Async
Una buena práctica es utilizar métodos asincrónicos para evitar posibles cuellos de botella en el rendimiento de los servicios y mejoraremos la capacidad de respuesta de nuestros servicios APIs. Veremos realmente mejoras cuando debemos salir de nuestra aplicación como consumir datos desde una base de datos o un servicio de terceros.
veamos un ejemplo sincronico:
[HttpGet] public IActionResult Get() { var customers = _repository.Customers.GetAllCustomers(); _logger.LogInfo($"Returned all customer from database."); return Ok(customers); }
Ahora veámoslo de forma asincrónica.
[HttpGet] public async Task<IActionResult> Get() { var customers = await _repository.Customers.GetAllCustomerAsync(); _logger.LogInfo($"Returned all customers from database."); return Ok(customers); }
Ten en cuenta que este es un ejemplo básico. Si quieres que indaguemos más en detalle dejame en los comentarios y lo haremos.
Conclusiones
Este es el último post relacionado con las buenas prácticas para nuestras Web Apis. Espero que lo hayas disfrutado y que te sea útil en tu dia a dia en el desarrollo. Si deseas agregar alguna práctica más que recomiendas como importante. Dejame el comentario y con gusto lo haré. Gracias.