【问题标题】:Returning an attachment from a remote web service从远程 Web 服务返回附件
【发布时间】:2011-11-13 23:28:12
【问题描述】:

总结

我需要从子应用的客户端中的链接检索存储在父应用中的附件。附件可通过 Web 服务调用在父应用程序中使用——它返回内容类型为“application/octet-stream”的标准 FileContentResult。我能想到的最好方法是通过 WebRequest 检索它并将生成的响应流传递给 FileStreamResult,尽管我有一些可用的替代方法。

有谁知道,在发出 WebRequest 时,响应流是否会在响应的第一部分返回后立即可用,或者是否已缓冲,因此在检索到所有数据之前我不会收到响应?

除了下面完整问题中列出的选项之外,是否还有其他我想念的选项? (除了将附件同时保存在子数据库和父数据库中——我真的不想这样做,从那时起我也需要定期同步它们)。

TLDR 版本

我有两个通过 RESTful Web 服务进行通信的相关应用程序。父应用程序维护可能具有附件的实体集合。例如,一个请求可能有一个 Excel 电子表格作为附件。实体及其附件存储在数据库中,并且使用与访问请求相同的逻辑来控制对附件的访问。也就是说,如果您无法查看请求,您应该无法下载附件。

在子应用程序中,我为分配给特定机构的实体维护了一些集成胶水——该应用程序用于在我们的董事会和每个 Regents 学校之间进行通信。我不想维护和同步完整的实体/附件。我只想保留足够的信息以允许我连接到父应用程序中的 Web 服务并获取子应用程序的特定实例可以访问的实体的详细信息。

这适用于实体数据本身。数据量很小,并且子应用程序中的缓冲开销在访问数据时不会出现明显的延迟。如有必要,我可以在本地缓存数据以避免性能损失。

我关心的是附件。我考虑了三种不同的机制来提供从子应用程序的客户端访问附件的权限。

  1. 生成一次性使用令牌和关联的 url,允许客户端直接从父应用程序下载附件。令牌生成 Web 服务调用将确保子应用程序的用户应该有权访问附件。这样做的缺点是您只能在客户端中单击一次链接。再次点击会导致错误而不是获取附件。

  2. 在子应用中缓冲附件。在这种情况下,我将提供一个控制器/操作来在子应用程序中下载附件,然后调用 Web 服务方法来获取附件并让子应用程序将附件作为 FileContentResult 发送。这消除了只能单击一次链接的问题,但附件可能相当大,并且在子应用程序中缓冲数据可能会使下载附件的时间增加一倍,更糟糕的是,在附件下载开始。

  3. 在子应用程序中链接,但将来自 Web 服务请求的流直接提供给 FileStreamResult。在我看来,这似乎是最好的选择,因为 FileStreamResult 以块的形式读取,而不是在发送到客户端之前必须拥有所有可用的数据。我在这里看到的唯一缺点是我不能再直接处理 WebResponse,因为 FileStreamResult 直到我的操作返回后才会执行。

以下是我对 (2) 和 (3) 的 API 包装器代码的代码:

private class ResponseModel<T> : IDisposable
{
    public T Model { get; set; }
    public WebResponse Response { get; set; }

    private bool Disposed { get; set; }
    private void Dispose( bool disposing )
    {
        if (!Disposed)
        {
            if (disposing)
            {
                ((IDisposable)this.Response).Dispose();
            }
            Disposed = true;
        }
    }

    public void Dispose()
    {
        Dispose( true );
    }
}

private ResponseModel<T> GetAttachmentResponse<T>( long id ) where T : IDownloadModel, new()
{
    var request = GetRequest( string.Format( "{0}/api/getattachment/{1}/{2}", this.BaseUrl, this.Key, id ) );

    var response = request.GetResponse();
    var model = (T)Activator.CreateInstance<T>();
    var contentDisposition = response.Headers["Content-Disposition"];
    if (!string.IsNullOrEmpty( contentDisposition ))
    {
        var filename = contentDisposition.Split( new[] { ';', ' ' }, StringSplitOptions.RemoveEmptyEntries )
                                         .SingleOrDefault( s => s.StartsWith( "filename", StringComparison.OrdinalIgnoreCase ) );
        if (!string.IsNullOrEmpty( filename ))
        {
            model.Name = filename.Split( '=' ).Skip( 1 ).FirstOrDefault();
        }
    }
    if (string.IsNullOrEmpty( model.Name ))
    {
        model.Name = "untitled";
    }

    return new ResponseModel<T> { Model = model, Response = response };
}

public FileDownloadModel GetAttachment( long id )
{
    using (var response = GetAttachmentResponse<FileDownloadModel>( id ))
    {
        var reader = new BinaryReader( response.Response.GetResponseStream() );
        response.Model.Content = reader.ReadBytes( (int)response.Response.ContentLength );
        return response.Model;

    }
}

public FileStreamDownloadModel GetAttachmentStream( long id )
{
    // since we're returning the stream, we can't dispose of the response when done.
    var response = GetAttachmentResponse<FileStreamDownloadModel>( id );
    response.Model.Stream = response.Response.GetResponseStream();
    return response.Model;
}


public interface IDownloadModel
{
    string ContentType { get; }
    string Name { get; set; }
}

模型类

public class FileDownloadModel : IDownloadModel
{
    public byte[] Content { get; set; }
    public string Name { get; set; }
    public string ContentType { get { return "application/octet-stream"; } }
}

public class FileStreamDownloadModel : IDownloadModel
{
    public Stream Stream { get; set; }
    public string Name { get; set; }
    public string ContentType { get { return "application/octet-stream"; } }
}

【问题讨论】:

  • 比勒?布勒?我得到的只是这个蟋蟀。
  • 如果这两个应用程序都在您的控制之下,您可以允许来自另一个域的跨站点脚本,并使用 Javascript 显示您喜欢的任何附件。请参阅:HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "");

标签: asp.net-mvc web-services download


【解决方案1】:

我会建议选项 1 的变体 [称之为选项 1(a)]。

不要生成一次性令牌,而是“借用”MVC AntiForgeryToken 类,并让您的父应用程序将自定义令牌和 cookie 返回给子应用程序,以包含在返回给用户的表单中。

如果子应用可能在单个页面上有多个文档的链接,则在请求令牌信息时,让子应用提交唯一标识符(标识来自用户的页面请求)作为请求的一部分。然后,您可以使用此标识符生成令牌,并且可以将标识符存储为验证过程的一部分。这将为您提供一个多用途令牌,对于页面上的每个链接都是唯一的。

在唯一标识符上设置一个过期时间,然后就可以开始了。

【讨论】:

  • 我喜欢使用到期日期而不是使其成为一次性令牌的想法。对于如何处理某人与其他人共享链接的用例,您有什么建议吗?假设后者被授权使用该应用程序并使用该链接。我并不是要解决授权人员与未授权人员共享附件的问题;一旦他们下载它,我认为他们不会不恰当地分享它,但他们可能希望与授权人分享链接。
  • 如果链接令牌基于 AntiForgeryToken 类,则无法共享,因为它需要 cookie 进行身份验证。如果允许授权用户共享链接,那么只需将您的一次性令牌转换为过期令牌。但是,比起允许共享链接,我更愿意让用户能够使用下载链接共享指向该页面的链接,因为这会加强您的安全模型。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-13
  • 1970-01-01
  • 1970-01-01
  • 2012-04-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多