Як обробити завантаження файлів за допомогою аутентифікації на основі JWT?


116

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

Це чудово працює для 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, перетворите корисний вантаж у blob, покладете blob в objectURL, встановіть джерело тега прив’язки до цього objectURL та натисніть на цей об'єкт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 (прив’язка); Не знайшли жодного рішення для Edge ...
Tompi,

12
Це рішення працює, але чи вирішує це рішення проблеми з UX з великими файлами? Якщо мені іноді потрібно завантажити файл розміром 300 Мб, це може зайняти деякий час, перш ніж натиснути на посилання та надіслати його менеджеру завантажень броузера. Ми могли б витратити зусилля, використовуючи api-прогрес прогресу та створити власний інтерфейс прогресу завантаження .. але тоді є також сумнівна практика завантаження 300-мегабійного файлу в js-land (на пам'ять?), Щоб просто передати його на завантаження менеджер.
scvnc

1
@Tompi я теж не міг зробити цю роботу для Edge та IE
zappa

34

Техніка

Спираючись на цю пораду Матіаса Волоскі з Auth0, відомого євангеліста JWT, я вирішив її, створивши підписаний запит з Хоуком .

Цитуючи Волоського:

Наприклад, ви вирішите це шляхом генерування підписаного запиту, як, наприклад, 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 ОП (маркер як параметр рядка запиту). Насправді я можу уявити, що підписаний запит може бути більш обмежувальним, тобто просто дозволений доступ до певної кінцевої точки. Але ОП № 2 здається простішим / меншим кроком, що з цим погано?
Тайлер Коллер

4
Залежно від вашого веб-сервера, повна URL-адреса може увійти до файлів журналу. Можливо, ви не хочете, щоб ваші ІТ-люди мали доступ до всіх жетонів.
Езекіа Дінелла

2
Крім того, URL-адреса із рядком запиту буде збережена в історії вашого користувача, дозволяючи іншим користувачам тієї ж машини отримати доступ до URL-адреси.
Езекіа Дінелла

1
Нарешті, і що робить це дуже небезпечним, це те, що URL-адреса надсилається у заголовку Referer для всіх запитів на будь-який ресурс, навіть на сторонні ресурси. Отже, якщо, наприклад, використовуючи Google Analytics, ви надішлете Google маркер URL-адреси та всі до них.
Езекіа Дінелла

1
Цей текст був узятий звідси: stackoverflow.com/questions/643355 / ...
Ezequias Dinella

10

Альтернативою вже згаданим підходам "fetch / createObjectURL" та "token-token" є стандартна форма POST, яка спрямована на нове вікно . Як тільки браузер прочитає заголовок вкладення у відповіді сервера, він закриє нову вкладку і розпочне завантаження. Цей же підхід також добре працює для відображення такого ресурсу, як PDF у новій вкладці.

Це має кращу підтримку для старих браузерів і уникає необхідності керувати новим типом маркера. Це також матиме кращу довгострокову підтримку, ніж базовий auth для URL-адреси, оскільки браузер видаляє підтримку імені користувача / пароля в URL- адресі .

На стороні клієнта ми використовуємо target="_blank"для уникнення навігації навіть у випадках відмови, що особливо важливо для SPA-пакетів (додатки для однієї сторінки).

Основний застереження полягає в тому, що перевірка JWT на стороні сервера повинна отримувати маркер з даних POST, а не з заголовка . Якщо ваша система керує доступом до обробників маршрутів автоматично за допомогою заголовка аутентифікації, можливо, вам потрібно буде позначити обробника як неаутентифікований / анонімний, щоб ви могли вручну перевірити 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

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

У куті зробіть аутентифікований запит, щоб отримати тимчасовий маркер (скажімо, годину), а потім додайте його до URL як параметр get. Таким чином ви можете завантажувати файли будь-яким зручним для вас способом (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 PROMISING? Маркер навіть більш зрозумілий, ніж передавання його в заголовках ахахаха
Рідке ядро
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.