Giải mã chuỗi truy vấn trong ASP.NET Core MVC bằng Action Filter

Gần đây tôi đã có thời gian làm việc với một nhà phát triển cần hỗ trợ một định dạng bảo mật cũ. Một ứng dụng cũ đã mã hóa tất cả các giá trị vào trong một tham số chuỗi truy vấn duy nhất, sau đó thực hiện một yêu cầu HTTP đến một điểm cuối ASP.NET Core MVC.

Bài viết này sẽ trình bày cách chúng ta có thể tận dụng Action Filter (bộ lọc hành động) trong ASP.NET Core MVC để làm cho trải nghiệm phát triển trong các action method (phương thức hành động) của chúng ta giống với trải nghiệm mà chúng ta đã sử dụng suốt thời gian qua.

Vấn đề

Các hệ thống cũ có thể không có quyền truy cập vào tất cả các giao thức và mẫu bảo mật mà chúng ta có ngày nay, vì vậy chúng có thể phải thực hiện một số giải pháp giải quyết vấn đề sáng tạo.

Trong trường hợp của chúng ta, chúng ta có một tham số chuỗi truy vấn là secret được mã hóa bằng TripleDES và chúng ta mong đợi người nhận giải mã nó để lấy ra các giá trị.

/?secret=1w58y%2BC60jon25f%2F4VvVHUOX%2FIxs%2FEVx

Kỹ thuật này có thể xảy ra vì các thông tin bí mật trong URL có thể được ghi vào trong log. Dù lý do là gì, chúng ta có một vấn đề để giải quyết.

Endpoint của chúng ta sẽ có hai tham số number name chứ không phải là secret chúng ta mong đợi hai tham số number và name sẽ nhận được giá trị khi dữ liệu được gửi tới endpoint.

[Route(""), HttpPost]
public IActionResult IndexPost(int number, string name)
{
    return View("Index",
        new IndexModel
        {
            Number = number,
            Name = name
        });
}

Chúng ta có một giải pháp đơn giản cho vấn đề này và nó liên quan đến Action Filter.

Giải pháp sử dụng Action FilterAction Argument

Action Filter (Bộ lọc hành động) là một cách tuyệt vời để chặn các yêu cầu đến và bổ sung các quy trình thực thi. Trong trường hợp của chúng ta, chúng ta muốn chuyển đổi tham số chuỗi truy vấn hiện có (secret), giải mã các giá trị của nó và gán giá trị đó cho các tham số number name của phương thức hành động (action method).

Cuối cùng chúng ta sẽ sử dụng một thuộc tính EncryptedParametersAttribute được triển khai từ ActionFilterAttribute để gắn trên các phương thức hành động.

[Route(""), HttpPost]
[EncryptedParameters("secret")]
public IActionResult IndexPost(int number, string name)
{
    return View("Index",
        new IndexModel
        {
            Number = number,
            Name = name
        });
}

Hãy xem mã triển khai attribute EncryptedParametersAttribute.

using System;
using System.Globalization;
using System.Linq;
using System.Web;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Secrets.Models;

namespace Secrets.Controllers
{
    public class EncryptedParametersAttribute : ActionFilterAttribute
    {
        public string ParameterName { get; }

        public EncryptedParametersAttribute(string parameterName = "secret")
        {
            ParameterName = parameterName;
        }

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            var config = context.HttpContext.RequestServices.GetRequiredService<IOptions<CryptoEngine.Secrets>>();
            var encrypted = context.HttpContext.Request.Query[ParameterName].FirstOrDefault();

            // decrypt secret
            var decrypted = CryptoEngine.Decrypt(encrypted, config.Value.Key);
            var collection = HttpUtility.ParseQueryString(decrypted);
            var actionParameters = context.ActionDescriptor.Parameters;

            foreach (var parameter in actionParameters)
            {
                try
                {
                    var value = collection[parameter.Name];

                    if (value == null)
                        continue;

                    // set the action arguments to the values 
                    // from the encrypted parameter
                    context.ActionArguments[parameter.Name] =
                        ConvertToType(value, parameter.ParameterType);
                }
                catch (Exception e)
                {
                    context.ModelState.TryAddModelException(parameter.Name, e);
                }
            }
        }

        private static object? ConvertToType(string value, Type type)
        {
            var underlyingType = Nullable.GetUnderlyingType(type);

            if (value.Length > 0)
            {
                if (type == typeof(DateTimeOffset) || underlyingType == typeof(DateTimeOffset))
                {
                    return DateTimeOffset.Parse(value, CultureInfo.InvariantCulture);
                }

                if (type == typeof(DateTime) || underlyingType == typeof(DateTime))
                {
                    return DateTime.Parse(value, CultureInfo.InvariantCulture);
                }

                if (type == typeof(Guid) || underlyingType == typeof(Guid))
                {
                    return new Guid(value);
                }

                if (type == typeof(Uri) || underlyingType == typeof(Uri))
                {
                    if (Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var uri))
                    {
                        return uri;
                    }

                    return null;
                }
            }
            else
            {
                if (type == typeof(Guid))
                {
                    return default(Guid);
                }

                if (underlyingType != null)
                {
                    return null;
                }
            }

            if (underlyingType is not null)
            {
                return Convert.ChangeType(value, underlyingType);
            }

            return Convert.ChangeType(value, type);
        }
    }
}

Bằng cách sử dụng phương thức ActionExecutingContext, chúng ta có quyền truy cập vào các tham số của điểm cuối mục tiêu của chúng ta. Sau đó, vấn đề chỉ là giải mã dữ liệu trong tham số truy vấn secret của chúng ta, và gán giá trị cho các tham số của phương thức hành động bằng cách sử dụng các tham số đã biết của chúng ta.

public override void OnActionExecuting(ActionExecutingContext context)
{
    var config = context.HttpContext.RequestServices.GetRequiredService<IOptions<CryptoEngine.Secrets>>();
    var encrypted = context.HttpContext.Request.Query[ParameterName].FirstOrDefault();

    // decrypt secret
    var decrypted = CryptoEngine.Decrypt(encrypted, config.Value.Key);
    var collection = HttpUtility.ParseQueryString(decrypted);
    var actionParameters = context.ActionDescriptor.Parameters;

    foreach (var parameter in actionParameters)
    {
        try
        {
            var value = collection[parameter.Name];

            if (value == null)
                continue;

            // set the action arguments to the values 
            // from the encrypted parameter
            context.ActionArguments[parameter.Name] =
                ConvertToType(value, parameter.ParameterType);
        }
        catch (Exception e)
        {
            context.ModelState.TryAddModelException(parameter.Name, e);
        }
    }
}

Việc chuyển đổi các kiểu dữ liệu trong ví dụ trên của chúng ta chỉ xử lý các kiểu nguyên thủy và kiểu nullable nhưng không xử lý các kiểu dữ liệu phức tạp. Vui lòng sửa đổi mã này để đáp ứng nhu cầu cụ thể của bạn.

Đối với những người quan tâm đến lớp mã hóa CryptoEngine, đây là nó, nhưng tôi không coi mình là một chuyên gia mã hóa.

using System;
using System.Security.Cryptography;
using System.Text;

namespace Secrets.Models
{
    /// <summary>
    /// modified from the following post
    /// https://dotnetcodr.com/2015/10/23/encrypt-and-decrypt-plain-string-with-triple-des-in-c/
    /// </summary>
    public static class CryptoEngine
    {
        public class Secrets
        {
            public string Key { get; set; }
        }

        public static string Encrypt(string source, string key)
        {
            var byteHash = MD5.HashData(Encoding.UTF8.GetBytes(key));
            var tripleDes = new TripleDESCryptoServiceProvider
            {
                Key = byteHash, 
                Mode = CipherMode.ECB
            };
            
            var byteBuff = Encoding.UTF8.GetBytes(source);
            return Convert.ToBase64String(tripleDes.CreateEncryptor()
                .TransformFinalBlock(byteBuff, 0, byteBuff.Length));
        }

        public static string Decrypt(string encodedText, string key)
        {
            var byteHash = MD5.HashData(Encoding.UTF8.GetBytes(key));
            var tripleDes = new TripleDESCryptoServiceProvider
            {
                Key = byteHash, 
                Mode = CipherMode.ECB
            };
            var byteBuff = Convert.FromBase64String(encodedText);
            return Encoding.UTF8.GetString(
                tripleDes
                    .CreateDecryptor()
                    .TransformFinalBlock(byteBuff, 0, byteBuff.Length));
        }
    }
}

Trong thực tế

Tôi đã tạo một ứng dụng ASP.NET Core MVC đơn giản để chứng minh bộ lọc hành động này đang được sử dụng. Bắt đầu với view, chúng ta sẽ mã hóa một số giá trị và gửi chúng lên điểm cuối sử dụng attribute EncryptedParametersAttribute của chúng ta.

@model IndexModel
@inject Microsoft.Extensions.Options.IOptions<CryptoEngine.Secrets> config
@{
    ViewData["Title"] = "Home Page";
    var values = "name=Khalid&number=57";
    var secret = CryptoEngine.Encrypt(values, config.Value.Key);
    var url = Url.Action("IndexPost", new {secret});
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>

    <p>Click this link to transmit secret values via Querystring</p>
    <form action="@url" method="POST">
        <p>@url</p>
        <button type="submit">Submit</button>
    </form>
</div>

@if (Model != null)
{
    <section style="margin-top: 1em">
        <div class="text-center">
            <h2>Secrets</h2>
            <div>
                <label asp-for="Name"></label>
                @Model.Name
            </div>
            <div>
                <label asp-for="Number"></label>
                @Model.Number
            </div>
        </div>
    </section>
}

Khi chúng ta gửi biểu mẫu, chúng ta sẽ nhận được dữ liệu được mã hóa trong tham số truy vấn secret, attribute EncryptedParametersAttribute sẽ tiến hành giải mã chúng, gắn dữ liệu cho tham số number, name và hiển thị lên trang.

Phần kết luận

Chúng ta có thể sử dụng kỹ thuật này để chuyển đổi bất kỳ chuỗi truy vấn đến thành bất kỳ tập hợp tham số nào khác. Kỹ thuật này mạnh mẽ và có thể được áp dụng cho bất kỳ phương thức hành động nào của MVC hoặc có thể được áp dụng toàn cục.

Như thường lệ, hãy sử dụng mã này làm điểm bắt đầu và thay đổi mã để đáp ứng nhu cầu cụ thể của bạn.

Cảm ơn bạn đã giành thời gian đọc bài viết này!

Nếu Comdy hữu ích và giúp bạn tiết kiệm thời gian làm việc

Bạn có thể vui lòng đưa Comdy vào whitelist của trình chặn quảng cáo ❤️ để hỗ trợ chúng tôi trong việc trả tiền cho dịch vụ lưu trữ web để duy trì hoạt động của trang web.

ASP.NET Core MVCASP.NET Core.NET CoreLập Trình C#
Bài Viết Liên Quan:
Tạo liên kết đến các endpoint của ASP.NET Core
Trung Nguyen 02/11/2021
Tạo liên kết đến các endpoint của ASP.NET Core

Trong bài viết này, chúng ta sẽ tìm hiểu cách đặt tên cho các endpoint để có thể tạo liên kết đến chúng từ bất kỳ đâu trong ứng dụng ASP.NET Core.

Cách thêm model vào dự án ASP.NET Core
Trung Nguyen 31/10/2021
Cách thêm model vào dự án ASP.NET Core

Trong bài viết này, chúng ta sẽ khám phá Model trong ASP.NET Core và các sub-framework khác như ASP.NET Core MVC, Razor Pages, Endpoints và Blazor.

Sử dụng Svelte trong dự án ASP.NET Core
Trung Nguyen 30/10/2021
Sử dụng Svelte trong dự án ASP.NET Core

Bài viết này sẽ giới thiệu cho bạn những điều cơ bản về việc thiết lập một dự án ASP.NET Core để sử dụng các component Svelte phía máy khách.

Background Service trong ASP.NET Core
Trung Nguyen 28/10/2021
Background Service trong ASP.NET Core

Bài viết này hướng dẫn cách triển khai một background service trong ứng dụng ASP.NET Core và cách giao tiếp với background service từ một HTTP request.