วิธีจัดการการดาวน์โหลดไฟล์ด้วยการรับรองความถูกต้องโดยใช้ JWT


116

ฉันกำลังเขียนเว็บแอปใน Angular ที่มีการจัดการการตรวจสอบความถูกต้องโดยโทเค็น JWT ซึ่งหมายความว่าทุกคำขอจะมีส่วนหัว "การตรวจสอบสิทธิ์" พร้อมข้อมูลที่จำเป็นทั้งหมด

ใช้งานได้ดีสำหรับการโทร REST แต่ฉันไม่เข้าใจว่าฉันควรจัดการลิงก์ดาวน์โหลดสำหรับไฟล์ที่โฮสต์บนแบ็กเอนด์อย่างไร (ไฟล์อยู่บนเซิร์ฟเวอร์เดียวกันกับที่โฮสต์บริการเว็บ)

ฉันไม่สามารถใช้<a href='...'/>ลิงก์ปกติได้เนื่องจากไม่มีส่วนหัวใด ๆ และการตรวจสอบความถูกต้องจะล้มเหลว window.open(...)เหมือนกันสำหรับคาถาต่างๆของ

วิธีแก้ปัญหาบางอย่างที่ฉันนึกถึง:

  1. สร้างลิงค์ดาวน์โหลดชั่วคราวที่ไม่ปลอดภัยบนเซิร์ฟเวอร์
  2. ส่งผ่านข้อมูลการพิสูจน์ตัวตนเป็นพารามิเตอร์ url และจัดการเคสด้วยตนเอง
  3. รับข้อมูลผ่าน XHR และบันทึกไฟล์ฝั่งไคลเอ็นต์

ทั้งหมดที่กล่าวมาน้อยกว่าที่น่าพอใจ

1 เป็นวิธีแก้ปัญหาที่ฉันใช้อยู่ตอนนี้ ฉันไม่ชอบด้วยเหตุผลสองประการประการแรกมันไม่เหมาะกับการรักษาความปลอดภัยอย่างที่สองมันใช้งานได้ แต่ต้องใช้งานค่อนข้างมากโดยเฉพาะบนเซิร์ฟเวอร์: เพื่อดาวน์โหลดสิ่งที่ฉันต้องเรียกใช้บริการที่สร้าง "สุ่ม" ใหม่ "url เก็บไว้ที่ใดที่หนึ่ง (อาจอยู่ในฐานข้อมูล) สักระยะหนึ่งแล้วส่งคืนให้กับลูกค้า ไคลเอนต์ได้รับ url และใช้ window.open หรือคล้ายกับมัน เมื่อได้รับการร้องขอ url ใหม่ควรตรวจสอบว่ายังถูกต้องหรือไม่จากนั้นจึงส่งคืนข้อมูล

2 ดูเหมือนว่าอย่างน้อยก็ทำงานได้มาก

3 ดูเหมือนจะใช้งานได้มากแม้กระทั่งการใช้ไลบรารีที่มีอยู่และปัญหาที่อาจเกิดขึ้นมากมาย (ฉันจะต้องให้แถบสถานะการดาวน์โหลดของฉันเองโหลดทั้งไฟล์ในหน่วยความจำแล้วขอให้ผู้ใช้บันทึกไฟล์ในเครื่อง)

แม้ว่างานนี้จะค่อนข้างธรรมดาดังนั้นฉันจึงสงสัยว่ามีอะไรที่ง่ายกว่านี้ที่ฉันสามารถใช้ได้หรือไม่

ฉันไม่จำเป็นต้องมองหาวิธีแก้ปัญหา "the Angular way" Javascript ธรรมดาก็ใช้ได้


โดยระยะไกลหมายความว่าไฟล์ที่ดาวน์โหลดได้อยู่ในโดเมนอื่นที่ไม่ใช่แอป Angular คุณควบคุมรีโมท (สามารถเข้าถึงเพื่อแก้ไขแบ็กเอนด์ได้) หรือไม่?
robertjd

ฉันหมายความว่าข้อมูลไฟล์ไม่ได้อยู่บนไคลเอนต์ (เบราว์เซอร์); ไฟล์นั้นโฮสต์บนโดเมนเดียวกันและฉันมีสิทธิ์ควบคุมแบ็กเอนด์ ฉันจะอัปเดตคำถามเพื่อให้ไม่คลุมเครือ
Marco Righele

ความยากของตัวเลือก 2 ขึ้นอยู่กับแบ็กเอนด์ของคุณ หากคุณสามารถบอกแบ็กเอนด์ของคุณให้ตรวจสอบสตริงข้อความค้นหาเพิ่มเติมจากส่วนหัวการอนุญาตสำหรับ JWT เมื่อผ่านเลเยอร์การพิสูจน์ตัวตนคุณก็ทำเสร็จแล้ว คุณใช้แบ็กเอนด์ใด
Technetium

คำตอบ:


47

นี่คือวิธีการที่จะดาวน์โหลดได้ที่ลูกค้าใช้แอตทริบิวต์ดาวน์โหลด , ดึงข้อมูล APIและURL.createObjectURL คุณจะดึงไฟล์โดยใช้ JWT ของคุณแปลงเพย์โหลดเป็นหยดใส่หยดลงใน objectURL ตั้งค่าแหล่งที่มาของแท็กจุดยึดให้กับ objectURL นั้นและคลิก object นั้น 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
ฉันสงสัยอยู่เสมอว่าทำไมไม่มีใครพิจารณาคำตอบนี้ เป็นเรื่องง่ายและเนื่องจากเราอยู่ในปี 2560 การสนับสนุนแพลตฟอร์มจึงค่อนข้างดี
Rafal Pastuszak

1
แต่การรองรับ iosSafari สำหรับแอตทริบิวต์การดาวน์โหลดดูเป็นสีแดงสวย :(
Martin Cremer

1
สิ่งนี้ใช้ได้ดีสำหรับฉันใน Chrome สำหรับ firefox มันใช้งานได้หลังจากที่ฉันเพิ่มจุดยึดในเอกสาร: document.body.appendChild (จุดยึด); ไม่พบวิธีแก้ปัญหาใด ๆ สำหรับ Edge ...
Tompi

12
โซลูชันนี้ใช้งานได้ แต่โซลูชันนี้จัดการกับปัญหา UX กับไฟล์ขนาดใหญ่หรือไม่ หากบางครั้งฉันจำเป็นต้องดาวน์โหลดไฟล์ขนาด 300MB อาจต้องใช้เวลาสักครู่ในการดาวน์โหลดก่อนที่จะคลิกลิงก์และส่งไปยังตัวจัดการการดาวน์โหลดของเบราว์เซอร์ เราสามารถใช้ความพยายามในการใช้ API ความคืบหน้าในการดึงข้อมูลและสร้าง UI ความคืบหน้าการดาวน์โหลดของเราเอง .. แต่ก็มีข้อปฏิบัติที่น่าสงสัยเช่นกันในการโหลดไฟล์ 300mb ลงใน js-land (ในหน่วยความจำ?) เพื่อส่งต่อไปยังการดาวน์โหลดเท่านั้น ผู้จัดการ.
scvnc

1
@Tompi ฉันก็ไม่สามารถทำงานนี้กับ Edge และ IE ได้
zappa

34

เทคนิค

ขึ้นอยู่กับคำแนะนำนี้ของ Matias Woloski จาก Auth0 ที่รู้จักกัน JWT ศาสนาผมแก้ไขได้โดยการสร้างการร้องขอการเซ็นสัญญากับฮอว์ก

อ้างถึง 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}

ส่วนหน้า (โดยjojoyuji )

ด้วยวิธีนี้คุณสามารถทำได้ทั้งหมดในคลิกเดียวของผู้ใช้:

function clickedOnDownloadButton() {

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

}

2
สิ่งนี้ยอดเยี่ยม แต่ฉันไม่เข้าใจว่ามันแตกต่างจากมุมมองด้านความปลอดภัยอย่างไรมากกว่าตัวเลือกของ OP # 2 (โทเค็นเป็นพารามิเตอร์สตริงการสืบค้น) อันที่จริงฉันสามารถจินตนาการได้ว่าคำขอที่ลงนามอาจมีข้อ จำกัด มากกว่ากล่าวคืออนุญาตให้เข้าถึงปลายทางเฉพาะ แต่ OP ของ # 2 ดูเหมือนง่ายขึ้น / ขั้นตอนน้อยลงมีอะไรผิดปกติ?
Tyler Collier

4
URL แบบเต็มอาจถูกล็อกในไฟล์บันทึกทั้งนี้ขึ้นอยู่กับเว็บเซิร์ฟเวอร์ของคุณ คุณอาจไม่ต้องการให้คนไอทีของคุณเข้าถึงโทเค็นทั้งหมด
Ezequias Dinella

2
นอกจากนี้ URL ที่มีสตริงข้อความค้นหาจะถูกบันทึกไว้ในประวัติของผู้ใช้ของคุณทำให้ผู้ใช้รายอื่นในเครื่องเดียวกันสามารถเข้าถึง URL ได้
Ezequias Dinella

1
สุดท้ายและสิ่งที่ทำให้สิ่งนี้ไม่ปลอดภัยมากคือ URL จะถูกส่งไปในส่วนหัวของผู้อ้างอิงของคำขอทั้งหมดสำหรับทรัพยากรใด ๆ แม้แต่ทรัพยากรของบุคคลที่สาม ดังนั้นหากคุณใช้ Google Analytics เช่นคุณจะส่งโทเค็น URL และทั้งหมดให้ Google
Ezequias Dinella

1
ข้อความนี้นำมาจากที่นี่: stackoverflow.com/questions/643355/…
Ezequias Dinella

11

ทางเลือกที่มีอยู่ "ดึงข้อมูล / createObjectURL" และ "ดาวน์โหลดเข็ม" วิธีการดังกล่าวแล้วเป็นมาตรฐานในแบบฟอร์มการโพสต์ที่เป้าหมายหน้าต่างใหม่ เมื่อเบราว์เซอร์อ่านส่วนหัวของไฟล์แนบในการตอบกลับของเซิร์ฟเวอร์เบราว์เซอร์จะปิดแท็บใหม่และเริ่มการดาวน์โหลด วิธีการเดียวกันนี้ยังทำงานได้ดีสำหรับการแสดงทรัพยากรเช่น PDF ในแท็บใหม่

สิ่งนี้รองรับเบราว์เซอร์รุ่นเก่าได้ดีขึ้นและหลีกเลี่ยงไม่ต้องจัดการโทเค็นประเภทใหม่ นอกจากนี้ยังจะมีที่ดีกว่าในระยะยาวกว่าการสนับสนุนการตรวจสอบสิทธิ์ขั้นพื้นฐานที่ URL ตั้งแต่การสนับสนุนสำหรับ username / password ที่ 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

ฉันจะสร้างโทเค็นสำหรับดาวน์โหลด

ภายในเชิงมุมให้ส่งคำขอที่พิสูจน์ตัวตนเพื่อรับโทเค็นชั่วคราว (พูดเป็นชั่วโมง) จากนั้นเพิ่มลงใน url เป็นพารามิเตอร์ get ด้วยวิธีนี้คุณสามารถดาวน์โหลดไฟล์ในแบบที่คุณต้องการ (window.open ... )


2
นี่เป็นวิธีแก้ปัญหาที่ฉันใช้อยู่ในตอนนี้ แต่ฉันไม่พอใจกับมันเพราะมันทำงานค่อนข้างหนักและฉันหวังว่าจะมีทางออกที่ดีกว่านี้ ...
Marco Righele

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 ได้อย่างไร
Jiri Vetyska

1
@JiriVetyska ฮ่า ๆ สัญญา? โทเค็นนั้นชัดเจนยิ่งกว่าการส่งผ่านในส่วนหัว ahahahha
Liquid Core
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.