JWT 기반 인증으로 파일 다운로드를 처리하는 방법은 무엇입니까?


116

저는 인증이 JWT 토큰에 의해 처리되는 Angular에서 webapp을 작성하고 있습니다. 즉, 모든 요청에는 필요한 모든 정보가 포함 된 "Authentication"헤더가 있습니다.

이것은 REST 호출에서 잘 작동하지만 백엔드에서 호스팅되는 파일에 대한 다운로드 링크를 처리하는 방법을 이해하지 못합니다 (파일은 웹 서비스가 호스팅되는 동일한 서버에 있습니다).

일반 <a href='...'/>링크는 헤더가 없어 인증이 실패하기 때문에 사용할 수 없습니다 . 의 다양한 주문에 대해서도 동일합니다 window.open(...).

내가 생각한 몇 가지 솔루션 :

  1. 서버에 보안되지 않은 임시 다운로드 링크 생성
  2. 인증 정보를 URL 매개 변수로 전달하고 케이스를 수동으로 처리합니다.
  3. XHR을 통해 데이터를 가져오고 파일 클라이언트 측에 저장합니다.

위의 모든 것이 만족스럽지 않습니다.

1은 내가 지금 사용하고있는 솔루션입니다. 나는 두 가지 이유로 좋아하지 않습니다. 첫째는 보안 상 이상적이지 않고, 둘째는 작동하지만 특히 서버에서 상당히 많은 작업이 필요합니다. 새로운 "무작위"를 생성하는 서비스를 호출해야하는 것을 다운로드하려면 "url, 어딘가에 (아마도 DB에) 저장 한 다음 클라이언트에 반환합니다. 클라이언트는 URL을 가져 와서 window.open 또는 이와 유사한 것을 사용합니다. 요청시 새 URL은 여전히 ​​유효한지 확인한 다음 데이터를 반환해야합니다.

2는 적어도 일이 많은 것 같습니다.

3은 사용 가능한 라이브러리를 사용하는 경우에도 많은 작업과 많은 잠재적 문제로 보입니다. (내 자신의 다운로드 상태 표시 줄을 제공하고 전체 파일을 메모리에로드 한 다음 사용자에게 파일을 로컬에 저장하도록 요청해야합니다.)

이 작업은 매우 기본적인 작업으로 보이므로 사용할 수있는 훨씬 더 간단한 작업이 있는지 궁금합니다.

필자는 "각도 방식"솔루션을 반드시 찾고있는 것은 아닙니다. 일반 Javascript가 좋습니다.


원격으로 다운로드 가능한 파일이 Angular 앱과 다른 도메인에 있음을 의미합니까? 리모컨을 제어합니까 (백엔드를 수정할 수있는 권한이 있습니까)?
robertjd apr

파일 데이터가 클라이언트 (브라우저)에 없음을 의미합니다. 파일이 동일한 도메인에서 호스팅되고 백엔드를 제어합니다. 모호하지 않게 질문을 업데이트하겠습니다.
마르코 Righele

옵션 2의 난이도는 백엔드에 따라 다릅니다. 백엔드가 인증 계층을 통과 할 때 JWT에 대한 권한 부여 헤더와 함께 쿼리 문자열을 확인하도록 지시 할 수 있다면 완료된 것입니다. 어떤 백엔드를 사용하고 있습니까?
Technetium

답변:


47

다음 은 download 속성 , fetch APIURL.createObjectURL을 사용 하여 클라이언트에 다운로드하는 방법 입니다. JWT를 사용하여 파일을 가져오고, 페이로드를 blob으로 변환하고, blob을 objectURL에 넣고, 앵커 태그의 소스를 해당 objectURL로 설정하고, 자바 스크립트에서 해당 objectURL을 클릭합니다.

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 년에 살고 있기 때문에 플랫폼 지원이 상당히 좋습니다.
Rafal Pastuszak

1
그러나 다운로드 속성에 대한 iosSafari 지원 :( 꽤 빨간 보인다
마틴 크레머

1
이것은 크롬에서 잘 작동했습니다. firefox의 경우 문서에 앵커를 추가 한 후에 작동했습니다. document.body.appendChild (anchor); ... 에지에 대한 해결책을 찾을 수 없습니다
Tompi

12
이 솔루션은 작동하지만이 솔루션은 대용량 파일에 대한 UX 문제를 처리합니까? 가끔 300MB 파일을 다운로드해야하는 경우 링크를 클릭하여 브라우저의 다운로드 관리자에게 보내기 전에 다운로드하는 데 시간이 걸릴 수 있습니다. 우리는 fetch-progress api를 사용하고 우리 자신의 다운로드 진행률 UI를 구축하는 데 노력을 기울일 수 있습니다. 그러나 300MB 파일을 js-land (메모리에?)에로드하여 단순히 다운로드로 전달하는 의문스러운 관행도 있습니다. 매니저.
scvnc

1
에지와 IE에 대해이 작업을 만들 수 없습니다 너무 난을 @Tompi
자파

34

기술

JWT 전도사로 알려진 Auth0의 Matias Woloski의 조언 을 바탕으로 Hawk 와 함께 서명 된 요청을 생성하여 문제를 해결했습니다 .

Woloski 인용 :

이를 해결하는 방법은 예를 들어 AWS와 같은 서명 된 요청을 생성하는 것입니다.

여기 활성화 링크에 사용되는이 기술 의 예가 있습니다.

백엔드

다운로드 URL에 서명하는 API를 만들었습니다.

의뢰:

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}

프론트 엔드 (by jojoyuji )

이렇게하면 한 번의 사용자 클릭으로 모든 작업을 수행 할 수 있습니다.

function clickedOnDownloadButton() {

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

}

2
이것은 멋지지만 보안 관점에서 OP의 옵션 # 2 (token as query string parameter)와 다른 점을 이해하지 못합니다. 사실 서명 된 요청이 더 제한적일 수 있다고 상상할 수 있습니다. 즉, 특정 엔드 포인트에 액세스 할 수 있습니다. 그러나 OP의 # 2는 더 쉽고 / 더 적은 단계로 보입니다. 무엇이 문제입니까?
Tyler Collier

4
웹 서버에 따라 전체 URL이 로그 파일에 기록 될 수 있습니다. IT 직원이 모든 토큰에 액세스하는 것을 원하지 않을 수도 있습니다.
Ezequias Dinella

2
또한 쿼리 문자열이 포함 된 URL이 사용자 기록에 저장되어 같은 컴퓨터의 다른 사용자가 URL에 액세스 할 수 있습니다.
Ezequias Dinella

1
마지막으로 이것이 매우 안전하지 않은 이유는 URL이 모든 리소스, 심지어 타사 리소스에 대한 모든 요청의 Referer 헤더로 전송된다는 것입니다. 예를 들어 Google Analytics를 사용하는 경우 Google에 URL 토큰을 모두 전송합니다.
Ezequias Dinella

1
이 텍스트는 여기에서 가져온 것입니다 : stackoverflow.com/questions/643355/…
Ezequias Dinella

10

이미 언급 된 기존 "fetch / createObjectURL"및 "download-token"접근 방식의 대안 은 새 창을 대상으로 하는 표준 양식 POST입니다 . 브라우저가 서버 응답에서 첨부 파일 헤더를 읽으면 새 탭을 닫고 다운로드를 시작합니다. 이 동일한 접근 방식은 새 탭에서 PDF와 같은 리소스를 표시하는데도 잘 작동합니다.

이것은 이전 브라우저를 더 잘 지원하고 새로운 유형의 토큰을 관리 할 필요가 없습니다. 또한 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
저는이 솔루션이 크게지지를 받고 있다고 생각합니다. 쉽고 깨끗하며 완벽하게 작동합니다.
Yura Fedoriv

6

다운로드 할 토큰을 생성합니다.

angular 내에서 임시 토큰을 얻기 위해 인증 된 요청 (예 : 1 시간)을 만든 다음 URL에 get 매개 변수로 추가합니다. 이렇게하면 원하는 방식으로 파일을 다운로드 할 수 있습니다 (window.open ...).


2
이것이 제가 지금 사용하고있는 솔루션이지만, 작업량이 많고 "밖에서"더 나은 솔루션이 있기를 바라기 때문에 만족스럽지 않습니다. ...
Marco Righele

3
나는 이것이 가능한 가장 깨끗한 솔루션이라고 생각하고 거기에서 많은 작업을 볼 수 없습니다. 하지만 토큰의 유효 시간 (예 : 3 분)을 더 작게 선택하거나 서버에 토큰 목록을 유지하고 사용한 토큰을 삭제하여 일회성 토큰으로 만들 것입니다 (내 목록에없는 토큰은 허용하지 않음). ).
nabinca 2015

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을 사용하여 쉬운 경로로 이동하여 기본 인증 비밀번호를 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.');
    }
}

이 접근 방식은 유망 해 보이지만 이러한 방식으로 JWT 토큰에 액세스 할 수있는 방법은 없습니다. 서버가이 이상한 URL을 구문 분석하는 방법과 jwt 토큰 값에 액세스 할 위치를 알려줄 수 있습니까?
Jiri Vetyska

1
@JiriVetyska LOL 약속? 토큰은 헤더에 전달하는 것보다 훨씬 더 명확합니다. ahahahha
Liquid Core
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.