Как добавить ошибки проверки запроса в ModelStateDictionary в ASP.NET MVC? - PullRequest
3 голосов
/ 19 мая 2010

Изучение безопасности системы, которую я создаю с помощью ASP.NET MVC 2, привело меня к открытию функции проверки запросов ASP.NET - действительно очень полезной функции. Но, очевидно, я не просто хочу предоставить пользователям «Желтый экран смерти», когда они вводят данные с помощью HTML, поэтому я стараюсь найти лучшее решение.

Моя идея состоит в том, чтобы найти все поля, которые содержат недопустимые данные, и добавить их в ModelStateDictionary, прежде чем вызывать действие, чтобы они автоматически появлялись в пользовательском интерфейсе как сообщения об ошибках. Немного погуглив, кажется, что никто не реализовал это, прежде чем я нахожу загадочным, поскольку это кажется таким очевидным. Кто-нибудь здесь есть предложение о том, как это сделать? Моя собственная идея состоит в том, чтобы поставить пользовательский ControllerActionInvoker для контроллера, как описано здесь , который каким-то образом проверяет это и модифицирует ModelStateDictionary, но я застрял на том, как сделать этот последний бит.

Просто перехватывать HttpRequestValidationException исключений не кажется полезным, поскольку на самом деле он не содержит всей необходимой мне информации.

Я сам ответил на вопрос, но мне все равно было бы интересно услышать о любых решениях, которые являются более элегантными / надежными.

1 Ответ

1 голос
/ 02 июня 2010

Посмотрев немного на то, как MVC выполняет привязку модели, я сам придумал решение. Я расширяю класс Controller пользовательской реализацией, которая переопределяет метод Execute следующим образом:

public abstract class ExtendedController : Controller
{      
    protected override void Execute(RequestContext requestContext)
    {
        ActionInvoker = new ExtendedActionInvoker(ModelState);
        ValidateRequest = false;
        base.Execute(requestContext);
    }
}

Чтобы я мог контролировать, когда происходит проверка запроса, я добавил следующее к web.config:

<httpRuntime requestValidationMode="2.0"/>

Суть действия происходит в пользовательской реализации класса ControllerActionInvoker:

public class ExtendedActionInvoker : ControllerActionInvoker
{
    private ModelStateDictionary _modelState;
    private const string _requestValidationErrorKey = "RequestValidationError";

    public ExtendedActionInvoker(ModelStateDictionary modelState)
    {
        _modelState = modelState;
    }

    protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
    {
        var action = base.FindAction(controllerContext, controllerDescriptor, actionName);
        controllerContext.RequestContext.HttpContext.Request.ValidateInput();

        return action;
    }

    protected override object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
    {
        try
        {
            return base.GetParameterValue(controllerContext, parameterDescriptor);
        }
        catch (HttpRequestValidationException)
        {
            var fieldName = parameterDescriptor.ParameterName;
            _modelState.AddModelError(fieldName, ModelRes.Shared.ValidationRequestErrorMessage);
            _modelState.AddModelError(_requestValidationErrorKey, ModelRes.Shared.ValidationRequestErrorMessage);

            var parameterType = parameterDescriptor.ParameterType;

            if (parameterType.IsPrimitive || parameterType == typeof(string))
            {
                return GetValueFromInput(parameterDescriptor.ParameterName, parameterType, controllerContext);
            }

            var complexActionParameter = Activator.CreateInstance(parameterType);
            foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(complexActionParameter))
            {
                object propertyValue = GetValueFromInput(descriptor.Name, descriptor.PropertyType, controllerContext);
                if (propertyValue != null)
                {
                    descriptor.SetValue(complexActionParameter, propertyValue);
                }
            }
            return complexActionParameter;
        }
    }

    private object GetValueFromInput(string parameterName, Type parameterType, ControllerContext controllerContext)
    {
        object propertyValue;
        controllerContext.RouteData.Values.TryGetValue(parameterName, out propertyValue);
        if (propertyValue == null)
        {
            propertyValue = controllerContext.HttpContext.Request.Params[parameterName];
        }

        if (propertyValue == null)
            return null;
        else
            return TypeDescriptor.GetConverter(parameterType).ConvertFrom(propertyValue);
    }
}

Для этого нужно выполнить проверку запроса после того, как действие было найдено. Это не приведет к немедленной ошибке, если запрос недействителен, но при вызове GetParameterValue возникнет исключение. Чтобы избежать этого, я переопределяю этот метод и заключаю базовый вызов в try-catch. Если возникает исключение, я в основном заново реализую привязку модели (я не даю никаких обещаний относительно качества этого кода) и добавляю ошибку к объекту ModelStateDictionary для значения.

В качестве бонуса, поскольку я хотел вернуть ошибку в стандартном формате для моих методов ajax, я также добавил пользовательскую реализацию InvokeActionMethod.

protected override ActionResult InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
{
    if (_modelState.ContainsKey(_requestValidationErrorKey))
    {
        var errorResult = new ErrorResult(_modelState[_requestValidationErrorKey].Errors[0].ErrorMessage, _modelState);

        var type = controllerContext.Controller.GetType();
        var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
        if (methods.Where(m => m.Name == actionDescriptor.ActionName).First().ReturnType == typeof(JsonResult))
            return (controllerContext.Controller as ExtendedControllerBase).GetJson(errorResult);
    }

    return base.InvokeActionMethod(controllerContext, actionDescriptor, parameters);
}
...