在多租户Web API中验证/操作输入参数

时间:2012-08-28 16:19:26

标签: asp.net-web-api

假设我们有一个多租户博客应用程序。该应用程序的每个用户可以具有由该服务托管的多个博客。

我们的API允许阅读和撰写博客文章。在某些情况下,指定BlogId是可选的,例如,获取用ASP.NET标记的所有帖子:

/api/posts?tags=aspnet

如果我们想在特定博客上查看所有使用ASP.NET标记的帖子,我们可以请求:

/api/posts?blogId=10&tags=aspnet

某些API方法需要有效的BlogId,例如在创建新博客文章时:

POST: /api/posts
{
    "blogid" : "10",
    "title" : "This is a blog post."
}

需要在服务器上验证BlogId,以确保它属于当前(经过身份验证的)用户。如果未在请求中指定,我还想推断用户的默认blogId(为简单起见,您可以假设默认是用户的第一个博客)。

我们有一个IAccountContext对象,其中包含有关当前用户的信息。如有必要,可以注入。

{
    bool ValidateBlogId(int blogId);
    string GetDefaultBlog();
}

在ASP.NET Web API中,推荐的方法是:

  1. 如果在消息正文或uri中指定了BlogId,请对其进行验证以确保它属于当前用户。如果没有,则抛出400错误。
  2. 如果请求中未指定BlogId,请从IAccountContext检索默认的BlogId并使其可用于控制器操作。我不希望控制器知道这个逻辑,这就是为什么我不想直接从我的行动中调用IAccountContext
  3. [更新]

    在推特上讨论并考虑到@ Aliostad的建议之后,我决定将博客视为资源并将其作为我的Uri模板的一部分(因此总是需要),即

    GET api/blog/1/posts -- get all posts for blog 1
    PUT api/blog/1/posts/5 -- update post 5 in blog 1
    

    我的用于加载单个项目的查询逻辑已更新为按帖子ID和博客ID加载(以避免租户加载/更新其他人的帖子)。

    唯一要做的就是验证BlogId。遗憾的是我们不能在Uri参数上使用验证属性,否则@ alexanderb的建议会起作用。相反,我选择使用ActionFilter:

    public class ValidateBlogAttribute : ActionFilterAttribute
    {
        public IBlogValidator Validator { get; set; }
    
        public ValidateBlogAttribute()
        {
            // set up a fake validator for now
            Validator = new FakeBlogValidator();
        }
    
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            var blogId = actionContext.ActionArguments["blogId"] as int?;
    
            if (blogId.HasValue && !Validator.IsValidBlog(blogId.Value))
            {
                var message = new HttpResponseMessage(HttpStatusCode.BadRequest);
                message.ReasonPhrase = "Blog {0} does not belong to you.".FormatWith(blogId);
                throw new HttpResponseException(message);
            }
    
            base.OnActionExecuting(actionContext);
        }
    }
    
    public class FakeBlogValidator : IBlogValidator
    {
        public bool IsValidBlog(int blogId)
        {
            return blogId != 999; // so we have something to test
        }
    }
    

    验证blogId现在只是用[ValidateBlog]装饰我的控制器/操作的情况。

    实际上每个人的答案都有助于解决方案,但我已将@ alexanderb标记为答案,因为它没有将验证逻辑耦合到我的控制器中。

4 个答案:

答案 0 :(得分:11)

我担心这可能不是您正在寻找的答案类型,但它可能会为讨论添加一点点。

因为你需要推断出blogId,你会看到所有麻烦,并且跳过所有的箍?我认为这就是问题所在。 REST是关于无状态的,而你似乎在服务器上持有一个单独的状态(上下文),它与HTTP的无状态特性发生冲突。

BlogId何时是操作的一个组成部分,需要明确地成为资源标识符的一部分 - 因此我将其简单地放在URL中。如果不这样做,那么问题在于 URL / URI 并非真正唯一标识资源 - 与名称暗示不同。如果 John 转到该资源,则会看到与 Amy 之后不同的资源。

这将简化设计,这也是有说服力的。当设计正确时,一切都很好。 我努力实现简单性

答案 1 :(得分:6)

这是方法,我将如何实现(考虑到,我不是ASP.NET Web API专家)。

所以,首先 - 验证。你需要一个简单的模型,如:

public class BlogPost
{
    [Required]
    [ValidateBlogId]
    public string BlogId { get; set; }

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

对于此模型,最好实现自定义验证规则。如果blogId可用,则会根据规则进行验证。实施可以是,

public class ValidateBlogId : ValidationAttribute
{
    [Inject]
    public IAccountContext Context { get; set; }

    public override bool IsValid(object value)
    {
        var blogId = value as string;
        if (!string.IsNullOrEmpty(blogId))
        {
            return Context.ValidateBlogId(blogId);
        }

        return true;
    }
}

(此前后,我假设使用Ninject,但你可以继续使用Ninject)。

接下来,您不希望公开blogId初始化的详细信息。该工作的最佳候选人是行动过滤器。

public class InitializeBlogIdAttribute : ActionFilterAttribute
{
    [Inject]
    public IAccountContext Context { get; set; }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var blogPost = actionContext.ActionArguments["blogPost"] as BlogPost;
        if (blogPost != null) 
        {
            blogPost.BlogId = blogPost.BlogId ?? Context.DefaultBlogId();
        }
    }
}

因此,如果blogPost模型被绑定且没有Id,则将应用默认值。

所以,最后是API控制器

public class PostsController : ApiController
{
    [InitializeBlogId]
    public HttpResponseMessage Post([FromBody]BlogPost blogPost) 
    {
        if (ModelState.IsValid)
        {
            // do the job
            return new HttpResponseMessage(HttpStatusCode.Ok);
        }

        return new HttpResponseMessage(HttpStatusCode.BadRequest);
    }
}

就是这样。我很快就在我的VS中尝试过,似乎有效。

我认为它应该符合您的要求。

答案 2 :(得分:3)

也可以将HttpParameterBinding用于您的场景。您可以查看MikeHongmei中的帖子了解详情。

以下示例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Metadata;

namespace MvcApplication49.Controllers
{
public class PostsController : ApiController
{
    public string Get([BlogIdBinding]int blogId, string tags = null)
    {
        return ModelState.IsValid + blogId.ToString();
    }

    public string Post([BlogIdBinding]BlogPost post)
    {
        return ModelState.IsValid + post.BlogId.ToString();
    }
}

[DataContract]
public class BlogPost
{
    [DataMember]
    public int? BlogId { get; set; }

    [DataMember(IsRequired = true)]
    public string Title { get; set; }

    [DataMember(IsRequired = true)]
    public string Details { get; set; }
}

public class BlogIdBindingAttribute : ParameterBindingAttribute
{
    public override System.Web.Http.Controllers.HttpParameterBinding GetBinding(System.Web.Http.Controllers.HttpParameterDescriptor parameter)
    {
        return new BlogIdParameterBinding(parameter);
    }
}

public class BlogIdParameterBinding : HttpParameterBinding
{
    HttpParameterBinding _defaultUriBinding;
    HttpParameterBinding _defaultFormatterBinding;

    public BlogIdParameterBinding(HttpParameterDescriptor desc)
        : base(desc)
    {
        _defaultUriBinding = new FromUriAttribute().GetBinding(desc);
        _defaultFormatterBinding = new FromBodyAttribute().GetBinding(desc);
    }

    public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
                                HttpActionContext actionContext, CancellationToken cancellationToken)
    {
        Task task = null;

        if (actionContext.Request.Method == HttpMethod.Post)
        {
            task = _defaultFormatterBinding.ExecuteBindingAsync(metadataProvider, actionContext, cancellationToken);
        }
        else if (actionContext.Request.Method == HttpMethod.Get)
        {
            task = _defaultUriBinding.ExecuteBindingAsync(metadataProvider, actionContext, cancellationToken);
        }

        return task.ContinueWith((tsk) =>
            {
                IPrincipal principal = Thread.CurrentPrincipal;

                object currentBoundValue = this.GetValue(actionContext);

                if (actionContext.Request.Method == HttpMethod.Post)
                {
                    if (currentBoundValue != null)
                    {
                        BlogPost post = (BlogPost)currentBoundValue;

                        if (post.BlogId == null)
                        {
                            post.BlogId = **<Set User's Default Blog Id here>**;
                        }
                    }
                }
                else if (actionContext.Request.Method == HttpMethod.Get)
                {
                    if(currentBoundValue == null)
                    {
                        SetValue(actionContext, **<Set User's Default Blog Id here>**);
                    }
                }
            });
    }
}

}

[<强>已更新] 我的同事Youssef建议使用ActionFilter一个非常简单的方法。以下是使用该方法的示例:

public class PostsController : ApiController
{
    [BlogIdFilter]
    public string Get(int? blogId = null, string tags = null)
    {
    }

    [BlogIdFilter]
    public string Post(BlogPost post)
    {
    }
}

public class BlogIdFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (actionContext.Request.Method == HttpMethod.Get && actionContext.ActionArguments["blogId"] == null)
        {
            actionContext.ActionArguments["blogId"] = <Set User's Default Blog Id here>;
        }
        else if (actionContext.Request.Method == HttpMethod.Post)
        {
            if (actionContext.ActionArguments["post"] != null)
            {
                BlogPost post = (BlogPost)actionContext.ActionArguments["post"];

                if (post.BlogId == null)
                {
                    post.BlogId = <Set User's Default Blog Id here>;
                }
            }
        }
    }
}

答案 3 :(得分:2)

由于并非所有控制器操作都需要这样,通常我会为此目的实现一个动作过滤器并在那里进行验证,但是您的要求还有其他问题需要这个选项,而不是一个选项。

另外,我要求客户端发送BlogId作为Uri的一部分,因为有了这个,你可以避免额外的反序列化(因为你不想在内部处理这个控制器动作)。

这里有一些要求,它们很重要:

  • 您不希望在每个操作方法中处理此问题。
  • 如果未提供ID,您希望自动获取ID。
  • 如果提供但无效(例如不属于当前用户), 你想要返回400 Bad Request。

考虑到这些要求,最好的选择是通过基本控制器来处理这个问题。可能是一个不太好的选择,但可以满足您的所有要求:

public abstract class ApiControllerBase : ApiController {

    public int BlogId { get; set; }

    public override Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken) {

        var query = controllerContext.Request.RequestUri.ParseQueryString();
        var accountContext = controllerContext.Request.GetDependencyScope().GetService(typeof(IAccountContext));
        if (query.AllKeys.Any(x => x.Equals("BlogId", StringComparison.OrdinalIgnoreCase | StringComparison.InvariantCulture))) {

            int blogId;
            if (int.TryParse(query["BlogId"], out blogId) && accountContext.ValidateBlogId(blogId)) {

                BlogId = blogId;
            }
            else {

                ModelState.AddModelError("BlogId", "BlogId is invalid");

                TaskCompletionSource<HttpResponseMessage> tcs = 
                    new TaskCompletionSource<HttpResponseMessage>();
                tcs.SetResult(
                    controllerContext.Request.CreateErrorResponse(
                        HttpStatusCode.BadRequest, ModelState));
                return tcs.Task;
            }
        }
        else {

            BlogId = accountContext.GetDefaultBlogId();
        }

        return base.ExecuteAsync(controllerContext, cancellationToken);
    }
}

您还可以考虑为RequestModel实施IValidatableObject,但这可能会使您的模型与应用程序的其他部分相关联