Подтвердить что ты не робот

Как обрабатывать загрузки файлов с помощью проверки подлинности на основе JWT?

Я пишу webapp в Angular, где проверка подлинности выполняется с помощью токена JWT, что означает, что каждый запрос имеет заголовок "Аутентификация" со всей необходимой информацией.

Это хорошо работает для вызовов REST, но я не понимаю, как я должен обрабатывать ссылки для загрузки файлов, размещенных на бэкэнд (файлы находятся на том же сервере, на котором размещены веб-службы).

Я не могу использовать регулярные ссылки <a href='...'/>, так как они не будут иметь никакого заголовка, и аутентификация не удастся. То же самое для различных заклинаний window.open(...).

Некоторые решения, о которых я думал:

  • Создайте временную незащищенную ссылку для скачивания на сервере
  • Передайте информацию аутентификации в качестве параметра url и вручную обработайте этот случай
  • Получить данные через XHR и сохранить клиентскую часть файла.

Все вышеизложенное менее удовлетворительное.

1 - это решение, которое я использую прямо сейчас. Мне это не нравится по двум причинам: во-первых, это не идеальная безопасность, во-вторых, она работает, но для этого требуется довольно много работы, особенно на сервере: загрузить что-то, что мне нужно, вызвать службу, которая генерирует новую "случайную" url, где-то хранит его (возможно, в БД) и возвращает его клиенту. Клиент получает URL-адрес и использует window.open или аналогичный с ним. При запросе новый URL-адрес должен проверить, действительно ли он действителен, а затем вернуть данные.

2 кажется по крайней мере такой же работой.

3 кажется большой работой, даже с использованием доступных библиотек и множеством потенциальных проблем. (Мне нужно будет предоставить свою собственную строку состояния загрузки, загрузить весь файл в память, а затем попросить пользователя сохранить файл локально).

Задача кажется довольно простой, поэтому мне интересно, есть ли что-то гораздо более простое, что я могу использовать.

Я не обязательно ищу решение "способ Angular". Обычный Javascript будет в порядке.

4b9b3361

Ответ 1

Вот способ загрузить его на клиент с помощью атрибута загрузки, API выборки и URL.createObjectURL. Вы должны получить файл, используя свой JWT, преобразовать полезную нагрузку в большой двоичный объект, поместить большой двоичный объект в objectURL, установить источник тега привязки для этого objectURL и щелкнуть этот objectURL в javascript.

let anchor = document.createElement("a");
document.body.appendChild(anchor);
let file = 'https://www.example.com/some-file.pdf';

let headers = new Headers();
headers.append('Authorization', 'Bearer MY-TOKEN');

fetch(file, { headers })
    .then(response => response.blob())
    .then(blobby => {
        let objectUrl = window.URL.createObjectURL(blobby);

        anchor.href = objectUrl;
        anchor.download = 'some-file.pdf';
        anchor.click();

        window.URL.revokeObjectURL(objectUrl);
    });

Значением атрибута download будет конечное имя файла. При желании вы можете извлечь предполагаемое имя файла из заголовка ответа о расположении контента, как описано в других ответах.

Ответ 2

Техника

Основанный на этот совет Матиаса Волоски из Auth0, известного JWT-евангелиста, я решил его, создав подписанный запрос с Hawk.

Цитата Woloski:

Как вы решаете это, создавая, например, подписанный запрос, например AWS.

Здесь у вас есть пример этого метода, который используется для активации ссылок.

бэкенд

Я создал API для подписи моих загружаемых URL:

Запрос:

POST /api/sign
Content-Type: application/json
Authorization: Bearer...
{"url": "https://path.to/protected.file"}

Ответ:

{"url": "https://path.to/protected.file?bewit=NTUzMDYzZTQ2NDYxNzQwMGFlMDMwMDAwXDE0NTU2MzU5OThcZDBIeEplRHJLVVFRWTY0OWFFZUVEaGpMOWJlVTk2czA0cmN6UU4zZndTOD1c"}

С подписанным URL-адресом мы можем получить файл

Запрос:

GET https://path.to/protected.file?bewit=NTUzMDYzZTQ2NDYxNzQwMGFlMDMwMDAwXDE0NTU2MzU5OThcZDBIeEplRHJLVVFRWTY0OWFFZUVEaGpMOWJlVTk2czA0cmN6UU4zZndTOD1c

Ответ:

Content-Type: multipart/mixed; charset="UTF-8"
Content-Disposition': attachment; filename=protected.file
{BLOB}

frontend (jojoyuji)

Таким образом вы можете сделать все это одним нажатием кнопки:

function clickedOnDownloadButton() {

  postToSignWithAuthorizationHeader({
    url: 'https://path.to/protected.file'
  }).then(function(signed) {
    window.location = signed.url;
  });

}

Ответ 3

Я бы сгенерировал токены для загрузки.

Внутри angular сделать аутентифицированный запрос для получения временного токена (скажем, час), а затем добавить его к URL-адресу в качестве параметра get. Таким образом вы можете скачивать файлы любым способом (window.open...)

Ответ 4

Дополнительное решение: использование базовой аутентификации. Хотя для этого требуется немного поработать с бэкэндом, токены не будут видны в журналах, и подписывать URL-адреса не нужно.


Сторона клиента

Пример URL может быть:

http://jwt:<user jwt token>@some.url/file/35/download

Пример с фиктивным токеном:

http://jwt:eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwIiwibmFt[email protected]some.url/file/35/download

Затем вы можете добавить это в <a href="..."> или window.open("...") - остальное обрабатывает браузер.


Сторона сервера

Реализация здесь зависит от вас и зависит от настроек вашего сервера - она не слишком отличается от использования параметра ?token= query.

Используя Laravel, я пошел простым путем и преобразовал пароль базовой аутентификации в заголовок JWT Authorization: Bearer <...>, позволяя промежуточному программному обеспечению обычной аутентификации обрабатывать все остальное:

class CarryBasic
{
    /**
     * @param Request $request
     * @param \Closure $next
     * @return mixed
     */
    public function handle($request, \Closure $next)
    {
        // if no basic auth is passed,
        // or the user is not "jwt",
        // send a 401 and trigger the basic auth dialog
        if ($request->getUser() !== 'jwt') {
            return $this->failedBasicResponse();
        }

        // if there _is_ basic auth passed,
        // and the user is JWT,
        // shove the password into the "Authorization: Bearer <...>"
        // header and let the other middleware
        // handle it.
        $request->headers->set(
            'Authorization',
            'Bearer ' . $request->getPassword()
        );

        return $next($request);
    }

    /**
     * Get the response for basic authentication.
     *
     * @return void
     * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
     */
    protected function failedBasicResponse()
    {
        throw new UnauthorizedHttpException('Basic', 'Invalid credentials.');
    }
}