Ảnh chụp hộ chiếu là tệp nhạy cảm nhất mà phần lớn ứng dụng từng nhận. Nó chứa khuôn mặt, họ tên đầy đủ, ngày sinh và số giấy tờ, và chỉ chừng ấy đã đủ để mở một tài khoản ở nơi khác. Thế nhưng trong nhiều luồng tải lên, bức ảnh bị sao chép năm, sáu lần trước khi có ai hỏi liệu nó có cần tồn tại hay không.

Bài viết này nhìn câu hỏi từ phía kỹ thuật. Bài trích dẫn những gì GDPR nói về việc lưu giữ dữ liệu, đi qua những nơi ảnh hộ chiếu lặng lẽ chồng chất, và mô tả một mô hình chúng tôi gọi là đọc, trả về, quên đi: ảnh được đọc, kết quả được trả về, và không phần nào của bức ảnh được giữ lại. Cuối bài là phần vẫn thuộc về bạn, vì một số doanh nghiệp bắt buộc phải giữ bản sao, và từ tháng 7 năm 2027 EU nêu rõ điều đó trong một đạo luật mới.

Đây là blog của doc.cheap, một API OCR hộ chiếu và giấy tờ tùy thân đọc giấy tờ tùy thân và trả kết quả dưới dạng JSON. Hãy đọc các phần nói về sản phẩm với ý thức đó.

GDPR thực sự yêu cầu gì

GDPR không nói "đừng bao giờ lưu ảnh hộ chiếu". Nó nói một điều hữu ích hơn: hãy giữ những gì bạn cần, trong khoảng thời gian bạn cần, và không lâu hơn. Điều 5(1) của Reg. (EU) 2016/679 đặt ra các nguyên tắc. Hai trong số đó quyết định phần lớn thiết kế.

Nguyên tắc Nội dung Điều 5(1) (bản tiếng Anh) Ý nghĩa đối với một bức ảnh
Tối thiểu hóa dữ liệu, điểm (c) "adequate, relevant and limited to what is necessary in relation to the purposes for which they are processed" Nếu quy trình của bạn cần họ tên, ngày sinh và số giấy tờ, thì bản thân bức ảnh có thể không còn cần thiết sau khi các thông tin đó đã được đọc.
Giới hạn lưu trữ, điểm (e) "kept in a form which permits identification of data subjects for no longer than is necessary for the purposes for which the personal data are processed" Mỗi bản sao đều cần một ngày kết thúc, và "chúng tôi chưa kịp xóa" không phải là một ngày kết thúc.

Điều 5(2) bổ sung trách nhiệm giải trình: bạn phải chứng minh được rằng mình tuân thủ các nguyên tắc này. Và Điều 25(1), bảo vệ dữ liệu ngay từ khâu thiết kế, yêu cầu "appropriate technical and organisational measures, such as pseudonymisation, which are designed to implement data-protection principles, such as data minimisation, in an effective manner", tức là các biện pháp kỹ thuật và tổ chức phù hợp, ví dụ giả danh hóa, nhằm áp dụng hiệu quả những nguyên tắc như tối thiểu hóa dữ liệu.

Đọc cùng nhau, các điều này biến một câu hỏi pháp lý thành một câu hỏi kỹ thuật. Càng ít bản sao của một bức ảnh, càng ít nơi bạn phải mô tả, bảo mật, sao lưu và cuối cùng là dọn sạch. Một bản sao chưa từng được ghi ra là bản duy nhất không cần đến công việc nào trong số đó.

Ảnh hộ chiếu rốt cuộc nằm ở đâu

Phần lớn các nhóm cố ý lưu ảnh ở một nơi. Vấn đề là những nơi không ai chọn. Dưới đây là danh sách để bạn đối chiếu với luồng của chính mình.

Nơi Ảnh đến đó bằng cách nào
Bucket tải lên Client tải tệp lên kho lưu trữ đối tượng trước, rồi backend đọc từ đó. Đối tượng tồn tại lâu hơn request.
Log request Một middleware ghi log lưu lại body của request, và ảnh base64 chính là body của request.
Báo cáo lỗi Công cụ theo dõi ngoại lệ đính kèm payload của request bị lỗi.
Hàng đợi và thử lại Một thông điệp công việc mang theo ảnh, và hàng đợi thư chết (dead-letter queue) giữ các thông điệp thất bại trong nhiều tuần.
Bản sao lưu và snapshot Snapshot cơ sở dữ liệu hoặc ổ đĩa chụp hôm đó chứa mọi bức ảnh được ghi trước nó, rất lâu sau khi dòng dữ liệu đã bị xóa.
Ticket hỗ trợ Người dùng gửi lại ảnh qua email "vì tải lên không được".
Phân tích và ghi lại phiên Một công cụ ghi lại trang, kể cả bản xem trước của tệp đã chọn.
Nhà cung cấp OCR Dịch vụ đọc giấy tờ giữ bản sao riêng, theo quy tắc lưu giữ riêng của họ.

Dòng cuối cùng là dòng bạn kiểm soát ít nhất. Log của chính mình thì bạn sửa được. Một bản sao do nhà cung cấp giữ chịu sự chi phối của các thiết lập bên đó, và bạn phải hỏi xem chúng là gì.

Mô hình: đọc, trả về, quên đi

Mô hình này phát biểu rất đơn giản. Bức ảnh chỉ tồn tại trong bộ nhớ, trong thời gian của một request. Thứ rời khỏi request là kết quả đọc: các giá trị được trích xuất. Còn bức ảnh thì hoàn toàn không rời đi.

  1. Gửi ảnh thẳng đến bước nhận dạng. Không có bucket tải lên ở giữa. Nếu buộc phải dùng bucket cho tệp lớn, hãy đặt thời gian sống của đối tượng tính bằng phút và xóa nó khi lệnh gọi trả về.
  2. Dùng ngay những gì bạn cần từ phản hồi. Các ảnh cắt như ảnh chân dung của người mang giấy tờ chỉ tồn tại trong phản hồi đó. Nếu luồng của bạn so sánh ảnh selfie với ảnh chân dung, hãy làm ngay lúc này.
  3. Giữ kết quả đọc, không giữ bức ảnh. Lưu các trường mà quy trình của bạn cần, theo quy tắc lưu giữ của riêng bạn. Ngày sinh và số giấy tờ vẫn là dữ liệu cá nhân, nên chúng cũng cần ngày kết thúc.
  4. Giữ ảnh tránh xa log và báo cáo lỗi. Loại bỏ body tại biên, ở một chỗ duy nhất, thay vì trông chờ mọi nơi gọi đều nhớ làm việc đó.
  5. Ghi lại kết quả. Với mỗi nơi trong bảng trên, ghi chú xem ảnh có thể đến đó không và vì sao không. Ghi chú đó chính là trách nhiệm giải trình mà Điều 5(2) yêu cầu.

API của chúng tôi làm gì với bức ảnh

Đây là cách doc.cheap xử lý cùng câu hỏi đó, như trang lưu giữ dữ liệu và quyền riêng tư của nó mô tả.

  • Ảnh không bao giờ được lưu. Nó nằm trong bộ nhớ suốt request, được chuyển cho bộ máy nhận dạng, và biến mất khi phản hồi được ghi xong. Không ổ đĩa, kho đối tượng hay log nào nhận nó.
  • Các ảnh cắt cũng không được lưu. Ảnh cắt giấy tờ, ảnh chân dung của người mang giấy tờ và chữ ký được trả về trong phản hồi của chính lệnh gọi tạo ra chúng. Một lần quét được đọc lại sau đó qua GET /v1/scans/{id} có mọi ô ảnh đặt là null.
  • Thứ có thể được giữ là kết quả đọc, và chỉ trong khoảng thời gian bạn yêu cầu. Tùy chọn retain_hours đặt khoảng đó cho từng request, từ 0 đến 8760 giờ (một năm). Giá trị chỉ định rõ luôn được ưu tiên hơn thiết lập của tài khoản.
  • retain_hours: 0 không ghi gì cả. Không phải một dòng hết hạn ngay lập tức, mà là không có dòng nào. Không có gì để dọn, không có gì trong bản sao lưu và không có gì để xuất. Lần quét đó vẫn được tính là một lần quét.
  • Thiết lập mặc định của tài khoản lo phần còn lại. Khi request không nêu khoảng thời gian, thiết lập lịch sử của tài khoản được áp dụng: 24 giờ, 7 ngày, 1 tháng hoặc 1 năm. Tài khoản mới bắt đầu với 1 năm để bảng điều khiển hiển thị lịch sử. Rút ngắn thiết lập sẽ áp dụng cho cả các dòng đã lưu, mỗi dòng tính từ thời điểm tạo của chính nó.
  • Một dòng được giữ lại có kèm một ảnh nhỏ: ảnh thu nhỏ tối đa 96 px ở cạnh dài nhất và tối đa 16 KiB, hiển thị trong nhật ký thao tác của bảng điều khiển để có thể nhận ra một dòng. Không thể đọc nó qua API. Ảnh thu nhỏ mất đi khi dòng mất đi.
  • Một lần quét có thể bị xóa sớm. Khóa live gửi DELETE /v1/scans/{id}, thao tác này xóa kết quả, dòng lịch sử và ảnh thu nhỏ. Không thể hoàn tác.

Hướng dẫn kiểm soát thời gian lưu lịch sử trình bày các thiết lập từng bước, và trang cách chúng tôi xử lý dữ liệu đưa ra phần tóm tắt.

Đây là một lệnh gọi không lưu giữ bằng Python với requests. Khóa sandbox công khai sk_sandbox_public được in trong tài liệu và không cần đăng ký: nó cho tổng cộng 10 giấy tờ được nhận dạng miễn phí cho mỗi địa chỉ, và tối đa 10 request mỗi giờ. Không lưu giữ là một thiết lập của chính tài khoản của bạn, nên cần khóa live của bạn. Sandbox công khai không phải là một tài khoản: nó giữ một bản ghi của mỗi lần quét, kèm ảnh nhỏ, cho nhật ký riêng của dịch vụ, vì vậy hãy gửi cho nó ảnh thử, không bao giờ gửi giấy tờ thật.

import base64
import uuid

import requests

API = "https://api.doc.cheap/v1/scans"
KEY = "sk_sandbox_public"  # khóa live của riêng bạn khi chạy production


def read_and_forget(path):
    with open(path, "rb") as f:
        image = base64.b64encode(f.read()).decode("ascii")
    response = requests.post(
        API,
        headers={
            "Authorization": f"Bearer {KEY}",
            "Idempotency-Key": str(uuid.uuid4()),
        },
        json={
            "image": image,
            # 0: với khóa live, không có gì về lần quét này được ghi lại phía API.
            # False: không cắt ảnh chân dung, vì luồng này không dùng.
            "options": {"retain_hours": 0, "return_portrait": False},
        },
        timeout=30,
    )
    response.raise_for_status()
    scan = response.json()
    del image  # bản sao cục bộ bị bỏ ngay khi lệnh gọi trả về
    if scan["meta"]["status"] != "recognized":
        return None
    # Giữ kết quả đọc mà quy trình của bạn cần, theo quy tắc lưu giữ của riêng bạn.
    return {
        "scan_id": scan["meta"]["id"],
        "document_number": scan["document"]["number"],
        "expiry_date": scan["document"]["expiry_date"],
        "birth_date": scan["holder"]["birth_date"],
        "mrz_status": scan["mrz"]["status"],
    }

Không lưu giữ có một cái giá nên biết trước khi nó làm bạn bất ngờ. Thông thường, Idempotency-Key cho phép một lần thử lại trả về kết quả đầu tiên. Với retain_hours: 0, không có kết quả nào được lưu để trả về, nên trong 24 giờ, một lần thử lại với cùng khóa sẽ bị từ chối với HTTP 409 và mã idempotency_replay_unavailable, thay vì được trả lời hai lần. Hãy hiểu phản hồi đó là "lệnh gọi đầu tiên đã thành công" và dùng kết quả bạn đã có.

Đọc lại lần quét cho thấy mặt còn lại của thiết kế. Khóa sandbox không đọc lại được gì cả, bất kể id nào. Chúng tôi đã gửi request này bằng sk_sandbox_public vào ngày 5 tháng 10 năm 2026:

curl https://api.doc.cheap/v1/scans/<SCAN_ID> \
  -H "Authorization: Bearer sk_sandbox_public"

Kết quả trả về là HTTP 404 (thông điệp đã được rút gọn):

{
  "error": {
    "code": "not_found",
    "message": "No scan with id …",
    "docs_url": "https://doc.cheap/docs/errors/not_found"
  }
}

Khóa live cũng nhận cùng mã 404 đó với lần quét thực hiện bằng retain_hours: 0, và với bất kỳ lần quét nào đã hết khoảng lưu giữ. Nếu bạn đang so sánh các dịch vụ ở điểm này, trang so sánh API OCR hộ chiếu là nơi để bắt đầu; hãy hỏi mỗi dịch vụ xem bức ảnh đi đâu, chứ không chỉ hỏi nó trả về gì.

Phần vẫn là việc của bạn

Đọc, trả về, quên đi loại bỏ các bản sao phía API. Nó không quyết định doanh nghiệp của bạn phải giữ những gì. Với một số doanh nghiệp, câu trả lời là "một bản sao", và luật nói như vậy.

Đạo luật chống rửa tiền mới của EU, Reg. (EU) 2024/1624, áp dụng từ ngày 10 tháng 7 năm 2027. Điều 90 viết: "It shall apply from 10 July 2027, except in relation to obliged entities referred to in Article 3, points (3)(n) and (o), to which it shall apply from 10 July 2029." Nghĩa là quy định áp dụng từ ngày 10 tháng 7 năm 2027, riêng các đối tượng có nghĩa vụ tại Điều 3, điểm (3)(n) và (o) thì từ ngày 10 tháng 7 năm 2029. Điều 77 về lưu giữ hồ sơ buộc các đối tượng có nghĩa vụ, như ngân hàng và các công ty tài chính khác, phải giữ:

"a copy of the documents and information obtained in the performance of customer due diligence pursuant to Chapter III, including information obtained through electronic identification means;"

Nói cách khác, đó là bản sao các tài liệu và thông tin thu được khi thẩm định khách hàng, kể cả thông tin thu được qua phương tiện định danh điện tử.

Điều 77(3) ấn định thời hạn: hồ sơ được "retained for a period of 5 years commencing on the date of the termination of the business relationship", tức là giữ 5 năm kể từ khi quan hệ kinh doanh kết thúc, và sau đó "obliged entities shall delete personal data upon expiry of the five-year period", nghĩa là hết năm năm thì phải xóa dữ liệu cá nhân. Điều 77(2) cho phép, với một số điều kiện, "a retention of the references to such information" thay cho bản sao, tức là chỉ giữ các tham chiếu đến thông tin đó.

Vì vậy, nếu bạn là đối tượng có nghĩa vụ, việc không lưu giữ ở phía API không xóa bỏ nghĩa vụ giữ hồ sơ của bạn. Nó chỉ thay đổi nơi hồ sơ nằm. Kho lưu trữ của chính bạn trở thành bản sao duy nhất, và nguyên tắc giới hạn lưu trữ ở trên vẫn áp dụng cho nó: năm năm sau khi quan hệ kết thúc, nó phải bị xóa. Việc thiết kế là làm cho kho đó có chủ đích, với một nơi, một người phụ trách, mã hóa, kiểm soát truy cập và một tác vụ xóa, thay vì đống bản sao vô tình như trong bảng ở trên.

Nếu bạn không phải là đối tượng có nghĩa vụ, hãy hỏi câu hỏi đơn giản trước: có gì trong quy trình của bạn cần đến bức ảnh sau khi các trường đã được đọc không? Thường thì câu trả lời thật lòng là không.

Danh sách kiểm tra

  • Mọi nơi trong bảng "ảnh hộ chiếu rốt cuộc nằm ở đâu" đều đã được đối chiếu với luồng của bạn.
  • Ảnh đi thẳng đến bước nhận dạng, hoặc qua một bucket có thời gian sống tính bằng phút.
  • Các ảnh cắt được dùng bên trong hàm xử lý phản hồi và không được ghi ra bất cứ đâu.
  • Lệnh gọi OCR đặt thời gian lưu giữ có chủ đích, retain_hours: 0 khi không cần đọc lại gì.
  • Logic thử lại xử lý phản hồi 409 idempotency_replay_unavailable.
  • Log và báo cáo lỗi loại bỏ body của request tại một biên duy nhất.
  • Các trường bạn giữ có ngày kết thúc, và có thứ gì đó xóa chúng.
  • Nếu luật yêu cầu một bản sao, nó nằm trong một kho có chủ đích duy nhất với ngày xóa riêng.

Đây là bản tóm tắt kỹ thuật, không phải tư vấn pháp lý. Nếu bạn tìm thấy một nơi ảnh có thể rò rỉ mà bài viết này bỏ sót, hãy viết cho admin@doc.cheap.

Một câu hỏi dành cho bạn: lần gần nhất bạn tìm thấy bản sao của một giấy tờ tùy thân mà không người nào định giữ là ở đâu? Hãy kể cho chúng tôi trong phần bình luận bên dưới.