Как я могу передать токен авторизации при загрузке файла? - PullRequest
0 голосов
/ 18 февраля 2019

У меня есть веб-приложение, в котором внешний интерфейс Angular (7) связывается с REST API на сервере и использует OpenId Connect (OIDC) для аутентификации.Я использую HttpInterceptor, который добавляет заголовок Authorization к моим HTTP-запросам для предоставления токена аутентификации серверу.Пока все хорошо.

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

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

Однако теперь, когда я добавил аутентификацию, это больше не работает, потому что браузер не включает токен аутентификации взапрос при получении документа - и поэтому я получаю 401-Unathorized ответ от сервера.

Итак, я больше не могу полагаться на обычную HTML-ссылку - мне нужно создать свой собственный HTTP-запрос и добавитьТок аутентификации явно.Но тогда как я могу убедиться, что пользовательский интерфейс такой же, как если бы пользователь щелкнул ссылку?В идеале я хотел бы, чтобы файл сохранялся с именем файла, предложенным сервером, а не с общим именем файла.

Ответы [ 2 ]

0 голосов
/ 18 февраля 2019

Angular (7) интерфейс взаимодействует с REST API на сервере

, а затем:

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

, который говорит мне, что ваш RESTful API нена самом деле RESTful.

Причина в том, что указанный выше запрос GET не является частью парадигмы RESTful API.Это базовый HTTP-запрос GET, который дает ответ не-JSON типа контента, и этот ответ не представляет состояние ресурса RESTful.

Это всего лишь семантика URL и ничего не меняет,но вы, как правило, сталкиваетесь с проблемами такого рода, когда начинаете смешивать вещи в гибридном API.

Однако теперь, когда я добавил аутентификацию, это больше не работает, потому что браузер невключить токен аутентификации в запрос при извлечении документа.

Нет, он работает правильно.Это сервер , который выдает 401 несанкционированный ответ.

Я понимаю, что вы говорите.Тег <a> больше не позволяет загружать URL-адрес, поскольку этот URL-адрес теперь требует аутентификации.С учетом вышесказанного для сервера довольно странно требовать аутентификацию HEADER для GET-запроса в контексте, где ни один из них не может быть предоставлен.Это не проблема, уникальная для вашего опыта, так как я видел, как это часто случается.Мысль о том, чтобы переключиться на токен JWT и подумать, что это решает все.

Использование createObjectURL() для преобразования ответа в новое окно, является своего рода хаком, который имеет другие побочные эффекты.Например, блокировщики всплывающих окон, мутация истории браузера и неспособность пользователя видеть загрузку, отменить загрузку или просмотреть ее в истории загрузок своего браузера.Вы также должны задаться вопросом о всей памяти, которую загрузка загружает в браузере, и переключение на base64 просто приведет к уменьшению потребления памяти.

Вы должны решить эту проблему, установив аутентификацию сервера.

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

Гибридный API RESTful заслуживает подхода гибридной аутентификации.

0 голосов
/ 18 февраля 2019

Я собрал воедино что-то, что «работает на моей машине», частично основываясь на этом ответе и других подобных вещах - хотя мои усилия «угловатые», поскольку они упакованы как директива многократного использования,В этом нет ничего особенного (большая часть кода выполняет основную работу по выяснению того, какое имя файла должно быть основано на заголовке content-disposition, отправленном сервером).

download-file.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).

Еще одна вещь, о которой стоит упомянуть, это то, что если вызывается веб-APIнаходится на сервере, который использует CORS, это может помешать клиентскому коду получить доступ к заголовку ответа о расположении контента.Чтобы разрешить доступ к этому заголовку, сервер может отправить соответствующий заголовок access-control-allow-headers.

...