Bảo mật cấp hàng (RLS) PostgreSQL đảm bảo người dùng chỉ nhìn thấy các hàng của riêng họ trong bảng

Nội dung

    Khi xây dựng bất kỳ ứng dụng nào mà người dùng chỉ được xem dữ liệu của riêng họ, bạn sẽ gặp phải một vấn đề chung. Vấn đề này là: Làm thế nào để đảm bảo người dùng không xem được dữ liệu của người khác?

    PostgreSQL có một tính năng tích hợp cho việc này. Tính năng này được gọi là Bảo mật cấp hàng (Row-Level Security – RLS). Nó không yêu cầu plugin hay công cụ bổ sung. Bạn chỉ cần sử dụng SQL.

    Chúng tôi sẽ trình bày cách RLS hoạt động thông qua một ví dụ thực tế.

    Bảo mật cấp hàng (RLS) là gì?

    RLS cho phép bạn định nghĩa các quy tắc. Các quy tắc này giới hạn những hàng dữ liệu mà người dùng có thể xem hoặc sửa đổi trong một bảng. Các quy tắc này được lưu trữ trực tiếp trong cơ sở dữ liệu. Chúng không nằm trong mã ứng dụng của bạn.

    Điều này có nghĩa là: ngay cả khi có lỗi trong truy vấn từ giao diện người dùng (frontend) hoặc máy chủ (backend), cơ sở dữ liệu vẫn thực thi quyền truy cập chính xác.

    Phiên bản PostgreSQL nào hỗ trợ RLS?

    RLS đã có mặt từ PostgreSQL 9.5. Phiên bản này được phát hành vào năm 2016. Nếu bạn đang sử dụng bất kỳ phiên bản hiện đại nào, bạn có thể dùng RLS.

    Một trường hợp sử dụng nhanh

    Hãy tưởng tượng bạn đang xây dựng một ứng dụng danh sách việc cần làm. Mỗi người dùng có các nhiệm vụ riêng của họ.

    Bạn không muốn Người dùng A xem các nhiệm vụ của Người dùng B. Điều gì sẽ xảy ra nếu ai đó quên thêm mệnh đề WHERE user_id = ? vào một truy vấn?

    Với RLS, Postgres sẽ tự động xử lý vấn đề này cho bạn.

    Xây dựng từng bước

    Bước 1: Tạo bảng

    Bạn cần tạo bảng todos với cấu trúc sau:

    create table todos (
    id serial primary key,
    user_id uuid not null,
    task text not null,
    is_done boolean default false
    );

    Bước 2: Bật RLS

    Sử dụng lệnh sau để kích hoạt RLS cho bảng todos:

    alter table todos enable row level security;

    Bây giờ RLS đã được kích hoạt. Tuy nhiên, không ai có thể truy cập bất kỳ hàng dữ liệu nào cho đến khi chúng ta thiết lập một chính sách.

    Bước 3: Mô phỏng người dùng đã đăng nhập

    Trong một ứng dụng thực tế, bạn sẽ truyền ID của người dùng đã đăng nhập vào kết nối cơ sở dữ liệu. Ở đây, chúng ta sẽ mô phỏng điều đó:

    set session “app.user_id” = ‘123e4567-e89b-12d3-a456-426614174000’;

    Chúng ta đang thiết lập một biến phiên để giả định đây là người dùng hiện tại của chúng ta.

    Bước 4: Thêm chính sách SELECT

    Chính sách này quy định: chỉ trả về các hàng dữ liệu mà user_id khớp với phiên hiện tại.

    create policy select_own_todos
    on todos
    for select
    using (
    user_id = current_setting(‘app.user_id’)::uuid
    );

    Bây giờ, nếu bạn chạy truy vấn:

    select * from todos;

    Bạn sẽ chỉ thấy các nhiệm vụ của riêng mình.

    Bước 5: Thêm chính sách INSERT, UPDATE, DELETE

    Bạn muốn người dùng có thể thêm nhiệm vụ cho chính họ? Bạn sẽ cần chính sách này:

    create policy insert_own_todos
    on todos
    for insert
    with check (
    user_id = current_setting(‘app.user_id’)::uuid
    );

    Đối với các thao tác cập nhật và xóa, ý tưởng tương tự:

    create policy update_own_todos
    on todos
    for update
    using (
    user_id = current_setting(‘app.user_id’)::uuid
    );

    create policy delete_own_todos
    on todos
    for delete
    using (
    user_id = current_setting(‘app.user_id’)::uuid
    );

    Vậy là xong. Postgres hiện thực thi các quy tắc truy cập ở cấp độ hàng.

    Hiệu suất

    RLS có thêm chi phí. Mỗi truy vấn bây giờ phải đánh giá các điều kiện bổ sung cho mỗi hàng. Đối với các chính sách đơn giản như user_id = current_setting(…), tác động là nhỏ.

    Tuy nhiên, nếu bạn có:

    • Các kết nối phức tạp trong chính sách của bạn.
    • Các truy vấn con lồng nhau hoặc mệnh đề EXISTS.
    • Nhiều chế độ xem (views) được kích hoạt RLS liên kết với nhau.

    Khi đó, mọi thứ có thể chậm lại. Trình lập kế hoạch (planner) có thể không sử dụng các chỉ mục (indexes) một cách hiệu quả.

    Bạn có thể kiểm tra điều này bằng cách sử dụng EXPLAIN ANALYZE.

    Mẹo: Giữ các chính sách RLS đơn giản và phù hợp với các trường đã được lập chỉ mục.

    Khi nào không nên sử dụng RLS

    RLS rất mạnh mẽ, nhưng không phải lúc nào cũng là công cụ phù hợp. Bạn có thể muốn tránh nó khi:

    • Bạn không kiểm soát tất cả các đường dẫn truy cập (ví dụ: các công cụ báo cáo thô bỏ qua biến phiên).
    • Bạn đã thực thi quyền truy cập ở lớp ứng dụng và muốn tránh trùng lặp.
    • Bạn cần các quy tắc truy cập rất động, thay đổi thường xuyên hoặc quá phức tạp đối với SQL.
    • Nhóm của bạn không quen thuộc với các chi tiết nội bộ của Postgres – điều này có thể gây nhầm lẫn trong quá trình gỡ lỗi.

    Trong những trường hợp này, một thư viện ủy quyền (authorization library) tốt ở lớp ứng dụng có thể phục vụ bạn tốt hơn.

    Cách tắt RLS

    Vô hiệu hóa RLS cũng dễ dàng:

    alter table todos disable row level security;

    Lệnh này loại bỏ tất cả hành vi RLS khỏi bảng đó. Bất kỳ chính sách nào đã được định nghĩa vẫn được lưu trữ – chúng chỉ không hoạt động.

    Một số điều cần lưu ý

    Đừng bỏ qua biến phiên. Nếu app.user_id không được đặt, Postgres sẽ không biết người dùng là ai. Các chính sách của bạn sẽ thất bại một cách âm thầm. Bạn sẽ nhận được kết quả trống.

    Bạn có thể làm cho nó an toàn hơn với:

    current_setting(‘app.user_id’, true)

    Lệnh này trả về NULL nếu không tìm thấy cài đặt.

    RLS phát huy hiệu quả ở đâu

    • Các ứng dụng đa người thuê (multi-tenant apps).
    • Các bảng điều khiển (dashboards) nơi người dùng chỉ xem dữ liệu của riêng họ.
    • Các công cụ nội bộ với các quy tắc truy cập nghiêm ngặt.

    Kết luận (Phần 1)

    RLS cần một số thiết lập ban đầu. Nhưng một khi đã được thiết lập, bạn không cần phải lặp lại mệnh đề WHERE user_id = ? trong mỗi truy vấn.

    RLS hoạt động một cách thầm lặng. Nó hoạt động ngầm và đảm bảo mỗi người dùng chỉ thấy những gì họ được phép. Nó không phải là phép thuật, nhưng nó đáng tin cậy. Đôi khi, đó chính xác là những gì bạn cần.

    Tiếp theo là gì?

    Trong bài viết này, chúng ta đã giữ mọi thứ đơn giản – một người dùng, một danh sách nhiệm vụ.

    Nhưng các ứng dụng trong thế giới thực hiếm khi đơn giản như vậy. Nếu bạn đang xây dựng một ứng dụng đa người thuê, nơi người dùng thuộc về các nhóm hoặc công ty, hoặc nếu bạn cần các vai trò khác nhau như quản trị viên và biên tập viên, có nhiều điều cần xem xét hơn.

    Phần 2 sắp tới sẽ đề cập đến:

    • Cách mô hình hóa quyền truy cập đa người thuê bằng cách sử dụng tenant_id.
    • Cách thiết lập các quy tắc dựa trên vai trò.
    • Cách truyền các yêu cầu JWT (JSON Web Token) từ ứng dụng của bạn vào Postgres để RLS hoạt động an toàn.

    Chúng ta vẫn sẽ sử dụng cùng một lược đồ todos – nhưng thông minh hơn.

    Nhưng nếu ứng dụng của bạn có các nhóm? Hoặc quản trị viên? Hoặc người dùng đăng nhập bằng mã thông báo JWT? Đó là những gì bài viết này sẽ nói đến. Chúng ta sẽ tiếp tục sử dụng ví dụ ứng dụng todos – nhưng thông minh hơn bây giờ.

    Giả sử bạn có các nhóm

    Mỗi người dùng thuộc về một nhóm (hoặc người thuê). Mỗi nhiệm vụ thuộc về một nhóm. Người dùng chỉ nên truy cập dữ liệu từ nhóm của họ.

    Đây là cách bạn thiết lập điều đó.

    1. Thêm các bảng

    Bạn cần tạo bảng tenantsusers:

    create table tenants (
    id uuid primary key,
    name text not null
    );

    create table users (
    id uuid primary key,
    tenant_id uuid not null references tenants(id),
    email text not null,
    role text default ‘user’
    );

    Bây giờ cập nhật bảng todos để thêm cột tenant_id:

    alter table todos add column tenant_id uuid not null references tenants(id);

    2. Bật RLS

    Kích hoạt RLS cho bảng todos:

    alter table todos enable row level security;

    3. Đặt các biến phiên

    Thông thường, bạn sẽ trích xuất thông tin này từ một JWT. Ở đây chúng ta sẽ mô phỏng nó:

    set session “app.user_id” = ‘…’;
    set session “app.tenant_id” = ‘…’;
    set session “app.role” = ‘user’;

    Các giá trị này hiện có sẵn bên trong các chính sách SQL của bạn.

    4. Viết các chính sách

    Quyền đọc (chỉ cùng nhóm)

    create policy tenant_read_access
    on todos
    for select
    using (
    tenant_id = current_setting(‘app.tenant_id’)::uuid
    );

    Chèn vào nhóm của riêng bạn

    create policy tenant_insert_access
    on todos
    for insert
    with check (<
    tenant_id = current_setting(‘app.tenant_id’)::uuid
    );

    Ý tưởng tương tự cho cập nhật và xóa:

    create policy tenant_modify_access
    on todos
    for update
    using (
    tenant_id = current_setting(‘app.tenant_id’)::uuid
    );

    create policy tenant_delete_access
    on todos
    for delete
    using (
    tenant_id = current_setting(‘app.tenant_id’)::uuid
    );

    Bây giờ thêm vai trò (Người dùng so với Quản trị viên)

    Giả sử quản trị viên có thể chỉnh sửa tất cả các nhiệm vụ trong nhóm của họ. Người dùng thông thường chỉ có thể chỉnh sửa nhiệm vụ của riêng họ.

    Đây là cách bạn cập nhật chính sách SELECT:

    create policy read_with_roles
    on todos
    for select
    using (
    tenant_id = current_setting(‘app.tenant_id’)::uuid
    and (
    current_setting(‘app.role’) = ‘admin’
    or user_id = current_setting(‘app.user_id’)::uuid
    )
    );

    Logic tương tự áp dụng cho các thao tác cập nhật và xóa. Bạn có thể đưa kiểm tra vai trò vào một chế độ xem (view) hoặc hàm SQL nếu mọi thứ bắt đầu trở nên phức tạp.

    Còn về mã thông báo JWT?

    Nếu bạn không sử dụng một dịch vụ như Supabase, bạn sẽ phải tự mình thực hiện điều đó.

    Khi người dùng đăng nhập:

    • Giải mã JWT.
    • Trích xuất user_id, tenant_idrole.
    • Truyền chúng vào Postgres như sau:

    set session “app.user_id” = ‘…’;
    set session “app.tenant_id” = ‘…’;
    set session “app.role” = ‘…’;

    Các giá trị này sẽ được RLS sử dụng thông qua current_setting(). Nếu bạn đang sử dụng một nhóm kết nối (connection pool), hãy đảm bảo rằng lệnh SET này xảy ra sau khi kết nối được lấy ra. Không phải trước đó.

    Giữ an toàn

    Một số lời khuyên để tránh những bất ngờ:

    • Luôn ép kiểu UUID một cách rõ ràng. ::uuid là người bạn của bạn.
    • Sử dụng current_setting(‘key’, true) để tránh lỗi nếu một khóa không được đặt. Lệnh này trả về NULL nếu cài đặt không được tìm thấy.
    • Thêm FORCE ROW LEVEL SECURITY nếu bạn muốn Postgres áp dụng RLS bất kể điều gì:

    alter table todos force row level security;

    Khi mọi thứ trở nên phức tạp

    RLS rất tuyệt vời cho đến khi nó không còn như vậy. Đây là những điểm có thể gây khó khăn:

    • Bạn cần logic truy cập phức tạp (ví dụ: truy cập tạm thời, liên kết chia sẻ).
    • Bạn muốn nhật ký kiểm tra (audit logs) cho mỗi lần truy cập – RLS không ghi lại các quyết định truy cập.
    • Bạn quên đặt các biến phiên – người dùng bị chặn một cách âm thầm.
    • Nhóm của bạn không quen thuộc với các chi tiết nội bộ của Postgres.

    Tuy nhiên, đối với hầu hết các ứng dụng dựa trên nhóm, nó hoạt động rất tốt.

    Kết luận (Phần 2)

    Chỉ với một vài chính sách bổ sung và một vài cài đặt phiên, cơ sở dữ liệu Postgres của bạn có thể xử lý quyền truy cập đa người thuê và quyền dựa trên vai trò.

    Không cần dịch vụ riêng biệt. Không cần các thủ thuật phần mềm trung gian. Hãy để Postgres thực hiện công việc nặng nhọc. Bạn chỉ cần truyền vào các yêu cầu phù hợp.

    Par1
    Par2

    Để lại một bình luận

    Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

    Chat with us
    Hello! How can I help you today?