11

我有一个 Web 应用程序,其中 Angular (7) 前端与服务器上的 REST API 通信,并使用 OpenId Connect (OIDC) 进行身份验证。我正在使用一个向我的 HTTP 请求HttpInterceptor添加Authorization标头以向服务器提供身份验证令牌的方法。到目前为止,一切都很好。

但是,除了传统的 JSON 数据,我的后端还负责动态生成文档。在添加身份验证之前,我可以简单地链接到这些文档,如下所示:

<a href="https://my-server.com/my-api/document?id=3">Download</a>

但是,现在我已经添加了身份验证,这不再有效,因为浏览器在获取文档时没有在请求中包含身份验证令牌 - 所以我得到了401-Unathorized服务器的响应。

所以,我不能再依赖普通的 HTML 链接——我需要创建自己的 HTTP 请求,并显式添加身份验证令牌。但是,如何确保用户体验与用户单击链接一样?理想情况下,我希望使用服务器建议的文件名而不是通用文件名保存文件。

4

2 回答 2

13

我已经拼凑出一些“在我的机器上工作”的东西,部分基于这个答案和其他类似的东西——尽管我的努力是通过被打包为一个可重用的指令来“Angular-ized”的。它没有太多内容(大部分代码都在做繁琐的工作,即根据服务器发送的标头确定文件名应该是什么content-disposition)。

下载文件.directive.ts

import { Directive, HostListener, Input } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';

@Directive({
  selector: '[downloadFile]'
})
export class DownloadFileDirective {
  constructor(private readonly httpClient: HttpClient) {}

  private downloadUrl: string;

  @Input('downloadFile')
  public set url(url: string) {
    this.downloadUrl = url;
  };

  @HostListener('click')
  public async onClick(): Promise<void> {

    // Download the document as a blob
    const response = await this.httpClient.get(
      this.downloadUrl,
      { responseType: 'blob', observe: 'response' }
    ).toPromise();

    // Create a URL for the blob
    const url = URL.createObjectURL(response.body);

    // Create an anchor element to "point" to it
    const anchor = document.createElement('a');
    anchor.href = url;

    // Get the suggested filename for the file from the response headers
    anchor.download = this.getFilenameFromHeaders(response.headers) || 'file';

    // Simulate a click on our anchor element
    anchor.click();

    // Discard the object data
    URL.revokeObjectURL(url);
  }

  private getFilenameFromHeaders(headers: HttpHeaders) {
    // The content-disposition header should include a suggested filename for the file
    const contentDisposition = headers.get('Content-Disposition');
    if (!contentDisposition) {
      return null;
    }

    /* StackOverflow is full of RegEx-es for parsing the content-disposition header,
    * but that's overkill for my purposes, since I have a known back-end with
    * predictable behaviour. I can afford to assume that the content-disposition
    * header looks like the example in the docs
    * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
    *
    * In other words, it'll be something like this:
    *    Content-Disposition: attachment; filename="filename.ext"
    *
    * I probably should allow for single and double quotes (or no quotes) around
    * the filename. I don't need to worry about character-encoding since all of
    * the filenames I generate on the server side should be vanilla ASCII.
    */

    const leadIn = 'filename=';
    const start = contentDisposition.search(leadIn);
    if (start < 0) {
      return null;
    }

    // Get the 'value' after the filename= part (which may be enclosed in quotes)
    const value = contentDisposition.substring(start + leadIn.length).trim();
    if (value.length === 0) {
      return null;
    }

    // If it's not quoted, we can return the whole thing
    const firstCharacter = value[0];
    if (firstCharacter !== '\"' && firstCharacter !== '\'') {
      return value;
    }

    // If it's quoted, it must have a matching end-quote
    if (value.length < 2) {
      return null;
    }

    // The end-quote must match the opening quote
    const lastCharacter = value[value.length - 1];
    if (lastCharacter !== firstCharacter) {
      return null;
    }

    // Return the content of the quotes
    return value.substring(1, value.length - 1);
  }
}

用法如下:

<a downloadFile="https://my-server.com/my-api/document?id=3">Download</a>

...或者,当然:

<a [downloadFile]="myUrlProperty">Download</a>

请注意,我没有在此代码中将身份验证令牌显式添加到 HTTP 请求,因为我的实现已经处理了所有 HttpClient调用HttpInterceptor(未显示)。要在没有拦截器的情况下执行此操作,只需在请求中添加标头(在我的情况下是Authorization标头)。

值得一提的是,如果正在调用的 Web API 在使用 CORS 的服务器上,它可能会阻止客户端代码访问 content-disposition 响应标头。要允许访问此标头,您可以让服务器发送适当的access-control-allow-headers标头。

于 2019-02-18T18:17:24.743 回答
-2

Angular (7) 前端与服务器上的 REST API 通信

接着:

<a href="https://my-server.com/my-api/document?id=3">Download</a>

这告诉我你的 RESTful API 并不是真正的 RESTful。

原因是上面的 GET 请求不是 RESTful API 范式的一部分。这是一个产生非 JSON 内容类型响应的基本 HTTP GET 请求,并且该响应不代表 RESTful 资源的状态。

这只是 URL 语义,并没有真正改变任何东西,但是当您开始将事物混合到混合 API 中时,您确实会遇到这类问题。

但是,现在我已经添加了身份验证,这不再有效,因为浏览器在获取文档时不会在请求中包含身份验证令牌。

不,它工作正常。产生未经授权响应的是服务器。401

我明白你在说什么。该<a>标记不再允许您下载 URL,因为该 URL 现在需要身份验证。话虽如此,在无法提供任何内容的上下文中,服务器要求对 GET 请求进行 HEADER 身份验证有点奇怪。这不是您的经验所独有的问题,因为我经常看到这种情况发生。这是切换到 JWT 令牌并认为这可以解决所有问题的心态。

使用createObjectURL()将响应变异到新窗口是一种具有其他副作用的黑客行为。例如弹出窗口阻止程序、浏览器历史记录突变以及用户无法查看下载、中止下载或在浏览器的下载历史记录中查看。您还必须想知道下载在浏览器中消耗的所有内存,切换到 base64 只会使内存消耗激增。

您应该通过修复服务器的身份验证来解决此问题。

<a href="https://my-server.com/my-api/document?id=3&auth=XXXXXXXXXXXXXXXXXXXX">Download</a>

混合 RESTful API 值得采用混合身份验证方法。

于 2019-02-18T20:04:41.830 回答