Как обрабатывать загрузку файлов с аутентификацией на основе JWT?

116

Я пишу веб-приложение на Angular, где аутентификация обрабатывается токеном JWT, что означает, что каждый запрос имеет заголовок «Authentication» со всей необходимой информацией.

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

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

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

  1. Создать временную незащищенную ссылку для скачивания на сервере
  2. Передайте информацию для аутентификации в качестве параметра URL-адреса и обработайте случай вручную.
  3. Получите данные через XHR и сохраните файл на стороне клиента.

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

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

2 вроде как минимум столько же работы.

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

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

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

Марко Ригеле
источник
Под удаленным вы имеете в виду, что загружаемые файлы находятся в другом домене, чем приложение Angular? Вы управляете пультом дистанционного управления (имеете доступ к его изменению) или нет?
robertjd
Я имею в виду, что данных файла нет на клиенте (браузере); файл размещен в том же домене, и я контролирую серверную часть. Я обновлю вопрос, чтобы сделать его менее двусмысленным.
Марко Ригеле
Сложность варианта 2 зависит от вашей серверной части. Если вы можете указать своему бэкэнду, чтобы он проверял строку запроса в дополнение к заголовку авторизации для JWT, когда он проходит через уровень аутентификации, все готово. Какой бэкэнд вы используете?
технеций

Ответы:

47

Вот способ загрузить его на клиент, используя атрибут загрузки , API выборки и URL.createObjectURL . Вы должны получить файл с помощью JWT, преобразовать полезную нагрузку в большой двоичный объект, поместить этот большой двоичный объект в объектный URL, установить источник тега привязки на этот объектный URL и щелкнуть этот объектный URL в 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атрибута будет конечным именем файла. При желании вы можете извлечь предполагаемое имя файла из заголовка ответа с размещением содержимого, как описано в других ответах .

Технеций
источник
1
Мне все время интересно, почему никто не принимает во внимание этот ответ. Это просто, и поскольку мы живем в 2017 году, поддержка платформы неплохая.
Рафаль Пастушак
1
Но поддержка iosSafari для атрибута загрузки выглядит довольно красной :(
Мартин Кремер,
1
Это отлично сработало для меня в хроме. Для firefox это сработало после того, как я добавил привязку к документу: document.body.appendChild (anchor); Не нашел решения для Edge ...
Tompi
12
Это решение работает, но справляется ли оно с проблемами UX с большими файлами? Если мне иногда нужно загрузить файл размером 300 МБ, загрузка может занять некоторое время, прежде чем щелкнуть ссылку и отправить ее в диспетчер загрузки браузера. Мы могли бы потратить усилия, используя api fetch-progress и создать собственный пользовательский интерфейс загрузки ... но есть также сомнительная практика загрузки файла размером 300 МБ в js-land (в памяти?), Чтобы просто передать его для загрузки управляющий делами.
scvnc
1
@Tompi я тоже не мог сделать эту работу за край и IE
Zappa
34

Техника

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

Цитата Волоски:

Вы можете решить эту проблему, создав подписанный запрос, например, как это делает 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}

интерфейс (от jojoyuji )

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

function clickedOnDownloadButton() {

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

}
Эсекиас Динелла
источник
2
Это круто, но я не понимаю, чем он отличается с точки зрения безопасности от варианта № 2 OP (токен как параметр строки запроса). На самом деле, я могу представить, что подписанный запрос может быть более ограничительным, т.е. просто разрешен доступ к определенной конечной точке. Но OP №2 кажется проще / меньше шагов, что в этом плохого?
Тайлер Кольер
4
В зависимости от вашего веб-сервера полный URL-адрес может регистрироваться в его файлах журнала. Возможно, вы не хотите, чтобы ваши ИТ-специалисты имели доступ ко всем токенам.
Ezequias Dinella
2
Кроме того, URL-адрес со строкой запроса будет сохранен в истории вашего пользователя, что позволит другим пользователям того же компьютера получить доступ к URL-адресу.
Ezequias Dinella
1
Наконец, что делает это очень небезопасным, URL-адрес отправляется в заголовке Referer всех запросов для любого ресурса, даже сторонних ресурсов. Итак, если вы, например, используете Google Analytics, вы отправите Google токен URL и все им.
Ezequias Dinella
1
Этот текст был взят отсюда: stackoverflow.com/questions/643355/…
Ezequias Dinella
11

Альтернативой уже упомянутым существующим подходам «fetch / createObjectURL» и «download-token» является стандартная форма POST, предназначенная для нового окна . Как только браузер прочитает заголовок вложения в ответе сервера, он закроет новую вкладку и начнет загрузку. Этот же подход также хорошо работает для отображения ресурса, такого как PDF, в новой вкладке.

Это лучше поддерживает старые браузеры и позволяет избежать необходимости управлять новым типом токена. Это также будет иметь лучшую долгосрочную поддержку, чем базовая аутентификация по URL-адресу, поскольку поддержка имени пользователя и пароля в URL-адресе удаляется браузерами .

На стороне клиента мы используем, target="_blank"чтобы избежать навигации даже в случаях сбоя, что особенно важно для SPA (одностраничных приложений).

Основное предостережение заключается в том, что проверка JWT на стороне сервера должна получать токен из данных POST, а не из заголовка . Если ваша платформа автоматически управляет доступом к обработчикам маршрутов с помощью заголовка Authentication, вам может потребоваться пометить обработчик как неаутентифицированный / анонимный, чтобы вы могли вручную проверить JWT для обеспечения надлежащей авторизации.

Форма может быть динамически создана и немедленно уничтожена, чтобы она была должным образом очищена (примечание: это можно сделать на простом JS, но здесь для ясности используется JQuery) -

function DownloadWithJwtViaFormPost(url, id, token) {
    var jwtInput = $('<input type="hidden" name="jwtToken">').val(token);
    var idInput = $('<input type="hidden" name="id">').val(id);
    $('<form method="post" target="_blank"></form>')
                .attr("action", url)
                .append(jwtInput)
                .append(idInput)
                .appendTo('body')
                .submit()
                .remove();
}

Просто добавьте любые дополнительные данные, которые нужно отправить в качестве скрытых входных данных, и убедитесь, что они добавлены в форму.

Джеймс
источник
1
Я считаю, что это решение сильно недооценено. Это просто, чисто и отлично работает.
Юра Федорив
6

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

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

Фред
источник
2
Это решение, которое я использую сейчас, но я не удовлетворен им, потому что это довольно много работы, и я надеюсь, что "там" есть лучшее решение ...
Марко Ригеле
3
Я думаю, что это самое чистое решение, и я не вижу в нем много работы. Но я бы выбрал меньшее время действия токена (например, 3 минуты) или сделал его одноразовым, сохранив список токенов на сервере и удалив использованные токены (не принимая токены, которых нет в моем списке. ).
nabinca
5

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


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

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

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

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

http://jwt:eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwIiwibmFtZSI6IiIsImlhdCI6MH0.KsKmQOZM-jcy4l_7NFsv1lWfpH8ofniVCv75ZRQrWno@some.url/file/35/download

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


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

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

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

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.');
    }
}
АльбиносЗасуха
источник
Этот подход кажется многообещающим, но я не вижу способа получить таким образом доступ к токену JWT. Можете ли вы указать мне на какой-либо ресурс, как сервер анализирует этот странный URL-адрес и где получить доступ к значению токена jwt?
Иржи Ветиска
1
@JiriVetyska LOL ОБЕЩАЕТ? Токен даже более понятен, чем передача его в заголовках ахахахха
Liquid Core