Làm thế nào để xử lý tải xuống tệp với xác thực dựa trên JWT?


116

Tôi đang viết một ứng dụng web trong Angular, nơi xác thực được xử lý bởi mã thông báo JWT, nghĩa là mọi yêu cầu đều có tiêu đề "Xác thực" với tất cả thông tin cần thiết.

Điều này hoạt động tốt cho các cuộc gọi REST, nhưng tôi không hiểu mình nên xử lý các liên kết tải xuống như thế nào đối với các tệp được lưu trữ trên chương trình phụ trợ (các tệp nằm trên cùng một máy chủ nơi lưu trữ các dịch vụ web).

Tôi không thể sử dụng các <a href='...'/>liên kết thông thường vì chúng sẽ không mang bất kỳ tiêu đề nào và quá trình xác thực sẽ không thành công. Tương tự cho các câu thần chú khác nhau củawindow.open(...) .

Một số giải pháp tôi đã nghĩ đến:

  1. Tạo liên kết tải xuống tạm thời không an toàn trên máy chủ
  2. Chuyển thông tin xác thực dưới dạng tham số url và xử lý trường hợp theo cách thủ công
  3. Lấy dữ liệu thông qua XHR và lưu phía máy khách tệp.

Tất cả những điều trên đều ít đạt yêu cầu.

1 là giải pháp tôi đang sử dụng ngay bây giờ. Tôi không thích nó vì hai lý do: thứ nhất nó không phải là bảo mật lý tưởng, thứ hai nó hoạt động nhưng nó đòi hỏi khá nhiều công việc, đặc biệt là trên máy chủ: để tải xuống thứ gì đó, tôi cần gọi một dịch vụ tạo mới "ngẫu nhiên "url, lưu trữ nó ở đâu đó (có thể trên DB) trong một thời gian và trả lại cho máy khách. Máy khách lấy url và sử dụng window.open hoặc tương tự với nó. Khi được yêu cầu, url mới sẽ kiểm tra xem nó có còn hợp lệ không và sau đó trả lại dữ liệu.

2 dường như ít nhất là nhiều công việc.

3 dường như rất nhiều công việc, ngay cả khi sử dụng các thư viện có sẵn và rất nhiều vấn đề tiềm ẩn. (Tôi cần cung cấp thanh trạng thái tải xuống của riêng mình, tải toàn bộ tệp vào bộ nhớ và sau đó yêu cầu người dùng lưu tệp cục bộ).

Tuy nhiên, nhiệm vụ có vẻ khá cơ bản, vì vậy tôi đang tự hỏi liệu có điều gì đơn giản hơn nhiều mà tôi có thể sử dụng không.

Tôi không nhất thiết phải tìm kiếm một giải pháp "theo cách Angular". Javascript thông thường sẽ ổn.


Ý bạn là từ xa rằng các tệp có thể tải xuống nằm trên một miền khác với ứng dụng Angular? Bạn có điều khiển điều khiển từ xa (có quyền truy cập để sửa đổi chương trình phụ trợ của nó) hay không?
robertjd

Ý tôi là dữ liệu tệp không có trên máy khách (trình duyệt); tệp được lưu trữ trên cùng một miền và tôi có quyền kiểm soát phần phụ trợ. Tôi sẽ cập nhật câu hỏi để làm cho nó ít mơ hồ hơn.
Marco Righele

Độ khó của tùy chọn 2 là phụ thuộc vào chương trình phụ trợ của bạn. Nếu bạn có thể yêu cầu chương trình phụ trợ của mình kiểm tra chuỗi truy vấn ngoài tiêu đề ủy quyền cho JWT khi nó đi qua lớp xác thực, thì bạn đã hoàn tất. Bạn đang sử dụng chương trình phụ trợ nào?
Technetium

Câu trả lời:


47

Đây là cách tải xuống ứng dụng trên máy khách bằng cách sử dụng thuộc tính tải xuống , API tìm nạpURL.createObjectURL . Bạn sẽ tìm nạp tệp bằng cách sử dụng JWT của mình, chuyển đổi trọng tải thành một blob, đặt blob vào một objectURL, đặt nguồn của thẻ anchor thành objectURL đó và nhấp vào objectURL đó trong 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);
    });

Giá trị của downloadthuộc tính sẽ là tên tệp cuối cùng. Nếu muốn, bạn có thể khai thác một tên tệp dự định từ tiêu đề phản hồi bố trí nội dung như được mô tả trong các câu trả lời khác .


1
Tôi cứ tự hỏi tại sao không ai xem xét phản hồi này. Nó đơn giản và vì chúng tôi đang sống vào năm 2017, hỗ trợ nền tảng khá tốt.
Rafal Pastuszak

1
Nhưng hỗ trợ của iosSafari cho thuộc tính tải xuống trông khá đỏ :(
Martin Cremer

1
Điều này làm việc tốt cho tôi trong chrome. Đối với firefox nó đã hoạt động sau khi tôi thêm neo vào tài liệu: document.body.appendChild (anchor); Không tìm thấy bất kỳ giải pháp cho Edge ...
Tompi

11
Giải pháp này hoạt động nhưng giải pháp này có xử lý được các mối lo ngại về UX với các tệp lớn không? Nếu đôi khi tôi cần tải xuống tệp 300MB, có thể mất một chút thời gian để tải xuống trước khi nhấp vào liên kết và gửi tệp đó đến trình quản lý tải xuống của trình duyệt. Chúng tôi có thể dành nỗ lực để sử dụng api tiến trình tìm nạp và xây dựng giao diện người dùng tiến trình tải xuống của riêng mình .. nhưng sau đó cũng có một thực tế đáng ngờ là tải tệp 300mb vào js-land (trong bộ nhớ?) Để chỉ chuyển nó ra tải xuống giám đốc.
scvnc

1
@Tompi, tôi cũng không thể làm cho điều này hoạt động cho Edge và IE
zappa

34

Kỹ thuật

Dựa trên lời khuyên này của Matias Woloski từ Auth0, nhà truyền bá JWT nổi tiếng, tôi đã giải quyết nó bằng cách tạo một yêu cầu đã ký với Hawk .

Trích dẫn Woloski:

Cách bạn giải quyết vấn đề này là tạo một yêu cầu đã ký như AWS chẳng hạn.

Ở đây bạn có một ví dụ về kỹ thuật này, được sử dụng cho các liên kết kích hoạt.

phụ trợ

Tôi đã tạo một API để ký các url tải xuống của mình:

Yêu cầu:

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

Phản ứng:

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

Với một URL đã ký, chúng tôi có thể lấy tệp

Yêu cầu:

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

Phản ứng:

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

frontend (bởi jojoyuji )

Bằng cách này, bạn có thể thực hiện tất cả chỉ với một cú nhấp chuột của người dùng:

function clickedOnDownloadButton() {

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

}

2
Điều này thật tuyệt nhưng tôi không hiểu nó khác như thế nào, từ góc độ bảo mật, so với tùy chọn số 2 của OP (mã thông báo dưới dạng tham số chuỗi truy vấn). Trên thực tế, tôi có thể tưởng tượng rằng yêu cầu đã ký có thể hạn chế hơn, tức là chỉ được phép truy cập vào một điểm cuối cụ thể. Nhưng số 2 của OP có vẻ dễ dàng hơn / ít bước hơn, điều đó có gì sai?
Tyler Collier

4
Tùy thuộc vào máy chủ web của bạn, URL đầy đủ có thể được ghi vào các tệp nhật ký của nó. Bạn có thể không muốn nhân viên CNTT của mình có quyền truy cập vào tất cả các mã thông báo.
Ezequias Dinella

2
Ngoài ra, URL có chuỗi truy vấn sẽ được lưu trong lịch sử người dùng của bạn, cho phép những người dùng khác của cùng một máy truy cập vào URL.
Ezequias Dinella

1
Cuối cùng và điều làm cho điều này rất không an toàn là, URL được gửi trong tiêu đề Người giới thiệu của tất cả các yêu cầu cho bất kỳ tài nguyên nào, thậm chí là tài nguyên của bên thứ ba. Vì vậy, nếu bạn sử dụng Google Analytics chẳng hạn, bạn sẽ gửi cho Google mã thông báo URL trong và tất cả cho họ.
Ezequias Dinella

1
Văn bản này được lấy từ đây: stackoverflow.com/questions/643355/...
Ezequias Dinella

10

Một giải pháp thay thế cho các phương pháp tiếp cận "tìm nạp / tạoObjectURL" và "mã thông báo tải xuống" hiện có đã được đề cập là BÀI ĐĂNG biểu mẫu chuẩn nhắm mục tiêu một cửa sổ mới . Khi trình duyệt đọc tiêu đề tệp đính kèm trên phản hồi của máy chủ, trình duyệt sẽ đóng tab mới và bắt đầu tải xuống. Cách tiếp cận tương tự này cũng hoạt động hiệu quả khi hiển thị tài nguyên như PDF trong tab mới.

Điều này hỗ trợ tốt hơn cho các trình duyệt cũ hơn và tránh phải quản lý một loại mã thông báo mới. Điều này cũng sẽ có hỗ trợ lâu dài tốt hơn so với xác thực cơ bản trên URL, vì hỗ trợ cho tên người dùng / mật khẩu trên url đang bị trình duyệt xóa .

Về phía khách hàng, chúng tôi sử dụngtarget="_blank" để tránh điều hướng ngay cả trong trường hợp thất bại, điều này đặc biệt quan trọng đối với các SPA (ứng dụng trang đơn).

Thông báo trước chính là các server-side validation JWT phải có mã thông báo từ dữ liệu POSTkhông từ tiêu đề . Nếu khuôn khổ của bạn quản lý quyền truy cập vào trình xử lý định tuyến tự động bằng tiêu đề Xác thực, bạn có thể cần phải đánh dấu trình xử lý của mình là chưa được xác thực / ẩn danh để bạn có thể xác thực JWT theo cách thủ công để đảm bảo ủy quyền thích hợp.

Biểu mẫu có thể được tạo động và hủy ngay lập tức để nó được dọn dẹp đúng cách (lưu ý: điều này có thể được thực hiện trong JS đơn giản, nhưng JQuery được sử dụng ở đây để rõ ràng) -

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();
}

Chỉ cần thêm bất kỳ dữ liệu bổ sung nào bạn cần gửi dưới dạng đầu vào ẩn và đảm bảo rằng chúng được thêm vào biểu mẫu.


1
Tôi tin rằng giải pháp này được ủng hộ rất nhiều. Nó dễ dàng, sạch sẽ và hoạt động hoàn hảo.
Yura Fedoriv

6

Tôi sẽ tạo mã thông báo để tải xuống.

Trong góc, thực hiện một yêu cầu đã xác thực để lấy mã thông báo tạm thời (giả sử một giờ) sau đó thêm nó vào url dưới dạng tham số nhận. Bằng cách này, bạn có thể tải xuống tệp theo bất kỳ cách nào bạn thích (window.open ...)


2
Đây là giải pháp tôi đang sử dụng bây giờ, nhưng tôi không hài lòng với nó vì nó là khá nhiều công việc và tôi hy vọng có một giải pháp tốt hơn "ngoài kia" ...
Marco Righele

3
Tôi nghĩ đây là giải pháp sạch nhất hiện có và tôi không thể thấy nhiều việc ở đó. Nhưng tôi sẽ chọn thời gian hiệu lực nhỏ hơn của mã thông báo (ví dụ: 3 phút) hoặc biến nó thành mã thông báo một lần bằng cách giữ danh sách các mã thông báo trên máy chủ và xóa các mã thông báo đã sử dụng (không chấp nhận các mã thông báo không có trong danh sách của tôi ).
nabinca

5

Một giải pháp bổ sung: sử dụng xác thực cơ bản. Mặc dù nó yêu cầu một chút công việc trên phần phụ trợ, các mã thông báo sẽ không hiển thị trong nhật ký và không phải thực hiện ký URL.


Phía khách hàng

URL mẫu có thể là:

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

Ví dụ với mã thông báo giả:

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

Sau đó, bạn có thể đưa phần này vào <a href="...">hoặc window.open("...")- trình duyệt xử lý phần còn lại.


Phía máy chủ

Việc triển khai ở đây là tùy thuộc vào bạn và phụ thuộc vào thiết lập máy chủ của bạn - nó không khác quá nhiều so với việc sử dụng ?token=tham số truy vấn.

Sử dụng Laravel, tôi đã thực hiện một cách dễ dàng và chuyển đổi mật khẩu xác thực cơ bản thành Authorization: Bearer <...>tiêu đề JWT , để phần mềm trung gian xác thực thông thường xử lý phần còn lại:

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.');
    }
}

Cách tiếp cận này có vẻ đầy hứa hẹn, nhưng tôi không thấy cách nào để truy cập vào mã thông báo JWT theo cách này. Bạn có thể chỉ cho tôi một số tài nguyên làm thế nào máy chủ phân tích cú pháp url lạ này và nơi để truy cập giá trị mã thông báo jwt?
Jiri Vetyska

1
@JiriVetyska LOL KHUYẾN MÃI? Mã thông báo thậm chí còn rõ ràng hơn so với việc chuyển nó trong tiêu đề ahahahha
Liquid Core
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.