Как обрабатывать загрузки файлов с помощью проверки подлинности на основе JWT?
Я пишу webapp в Angular, где проверка подлинности выполняется с помощью токена JWT, что означает, что каждый запрос имеет заголовок "Аутентификация" со всей необходимой информацией.
Это хорошо работает для вызовов REST, но я не понимаю, как я должен обрабатывать ссылки для загрузки файлов, размещенных на бэкэнд (файлы находятся на том же сервере, на котором размещены веб-службы).
Я не могу использовать регулярные ссылки <a href='...'/>
, так как они не будут иметь никакого заголовка, и аутентификация не удастся. То же самое для различных заклинаний window.open(...)
.
Некоторые решения, о которых я думал:
- Создайте временную незащищенную ссылку для скачивания на сервере
- Передайте информацию аутентификации в качестве параметра url и вручную обработайте этот случай
- Получить данные через XHR и сохранить клиентскую часть файла.
Все вышеизложенное менее удовлетворительное.
1 - это решение, которое я использую прямо сейчас. Мне это не нравится по двум причинам: во-первых, это не идеальная безопасность, во-вторых, она работает, но для этого требуется довольно много работы, особенно на сервере: загрузить что-то, что мне нужно, вызвать службу, которая генерирует новую "случайную" url, где-то хранит его (возможно, в БД) и возвращает его клиенту. Клиент получает URL-адрес и использует window.open или аналогичный с ним. При запросе новый URL-адрес должен проверить, действительно ли он действителен, а затем вернуть данные.
2 кажется по крайней мере такой же работой.
3 кажется большой работой, даже с использованием доступных библиотек и множеством потенциальных проблем. (Мне нужно будет предоставить свою собственную строку состояния загрузки, загрузить весь файл в память, а затем попросить пользователя сохранить файл локально).
Задача кажется довольно простой, поэтому мне интересно, есть ли что-то гораздо более простое, что я могу использовать.
Я не обязательно ищу решение "способ Angular". Обычный Javascript будет в порядке.
Ответы
Ответ 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}
Таким образом вы можете сделать все это одним нажатием кнопки:
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.');
}
}