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

Ben*_*ter 5 asp.net-web-api

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

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

/api/posts?tags=aspnet
Run Code Online (Sandbox Code Playgroud)

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

/api/posts?blogId=10&tags=aspnet
Run Code Online (Sandbox Code Playgroud)

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

POST: /api/posts
{
    "blogid" : "10",
    "title" : "This is a blog post."
}
Run Code Online (Sandbox Code Playgroud)

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

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

{
    bool ValidateBlogId(int blogId);
    string GetDefaultBlog();
}
Run Code Online (Sandbox Code Playgroud)

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

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

[更新]

在关于Twitter的讨论并考虑到@ 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
Run Code Online (Sandbox Code Playgroud)

我的用于加载单个项目的查询逻辑已更新为按帖子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
    }
}
Run Code Online (Sandbox Code Playgroud)

验证blogId现在只是一个装饰我的控制器/动作的情况[ValidateBlog].

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

Ali*_*tad 11

我担心这可能不是你正在寻找的答案类型,但它可能会给讨论添加一点点谦虚.

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

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

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


Ale*_*sky 6

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

所以,所有的拳头 - 验证.你需要一个简单的模型,如下所示:

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

    [Required]
    public string Title { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

对于此模型,最好实现自定义验证规则.如果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;
    }
}
Run Code Online (Sandbox Code Playgroud)

(这里和之后,我假设使用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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

因此,如果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);
    }
}
Run Code Online (Sandbox Code Playgroud)

而已.我刚刚在我的VS中尝试过,似乎很有效.

我认为它应该满足你的要求.