Zod và ủy quyền: Thực hành tốt nhất xác thực quyền người dùng
Bạn đã bao giờ gặp phải vấn đề logic xác thực quyền hỗn loạn hoặc thông báo lỗi không rõ ràng trong dự án? Bạn có mong muốn một cách thức giúp hệ thống xác thực quyền vừa an toàn lại dễ bảo trì? Bài viết này sẽ hướng dẫn bạn cách sử dụng Zod (thư viện xác thực mô hình ưu tiên TypeScript) để xây dựng hệ thống xác thực quyền người dùng mạnh mẽ, giải quyết những thách thức này. Sau khi đọc xong bài viết, bạn sẽ có khả năng tạo các mô hình xác thực quyền có cấu trúc bằng Zod, xử lý logic quyền phức tạp và cung cấp phản hồi lỗi rõ ràng.
Zod là gì?
Zod là một thư viện khai báo và xác thực mô hình ưu tiên TypeScript, cho phép bạn khai báo các bộ xác thực và tự động suy luận kiểu TypeScript tĩnh. Lợi thế cốt lõi của Zod nằm ở API đơn giản, khả năng kết hợp mạnh mẽ và cơ chế xử lý lỗi xuất sắc, những đặc tính này khiến nó trở thành lựa chọn lý tưởng cho việc xử lý xác thực quyền.
Sử dụng cơ bản của Zod rất đơn giản. Bạn có thể định nghĩa một mô hình, sau đó sử dụng phương thức parse hoặc safeParse để xác thực dữ liệu. Ví dụ, tạo một mô hình chuỗi và xác thực đầu vào:
import { z } from "zod";
// Tạo một mô hình chuỗi
const chuoiSchema = z.string();
// Xác thực dữ liệu
chuoiSchema.parse("ca"); // => "ca"
chuoiSchema.parse(12); // => Ném ra ZodError
Bạn có thể tìm thấy thêm thông tin cơ bản về Zod trong tệp README.md.
Thách thức cốt lõi của xác thực quyền
Khi xây dựng hệ thống quyền, chúng ta thường đối mặt với những thách thức sau:
- Logic quyền phức tạp: Người dùng có thể có nhiều vai trò, mỗi vai trò lại có tập quyền khác nhau.
- Kiểm tra quyền động: Quyền có thể thay đổi động dựa trên thuộc tính tài nguyên hoặc trạng thái người dùng.
- Phản hồi lỗi rõ ràng: Khi xác thực quyền thất bại, cần thông báo rõ ràng cho người dùng biết lý do.
- Tích hợp với hệ thống hiện có: Xác thực quyền cần tích hợp liền mạch vào các tầng hệ thống như tuyến đường, API, v.v.
Khả năng kết hợp mô hình và tính năng xác thực tùy chỉnh của Zod giúp nó có thể đối mặt hiệu quả với những thách thức này.
Xây dựng mô hình quyền cơ bản bằng Zod
Hãy bắt đầu bằng cách xây dựng mô hình quyền cơ bản. Đầu tiên, chúng ta có thể xác định các vai trò và quyền phổ biến của người dùng. Sử dụng kiểu liệt kê (Zod enums) của Zod có thể xác định rõ ràng các vai trò được phép:
import { z } from "zod";
// Xác định vai trò người dùng
const VaiTroEnum = z.enum(["nguoidung", "bien_tap_vien", "quan_tri_vien"]);
// Xác định quyền
const enumQuyen = z.enum([
"doc",
"ghi",
"xoa",
"quan_ly_nguoi_dung"
]);
// Xác định mô hình quyền người dùng
const QuyềnNguoiDungSchema = z.object({
vaiTro: VaiTroEnum,
quyen: z.array(enumQuyen)
});
// Xác thực quyền người dùng
const quyenNguoiDung = {
vaiTro: "bien_tap_vien",
quyen: ["doc", "ghi"]
};
QuyềnNguoiDungSchema.parse(quyenNguoiDung); // Xác thực thành công
Mô hình cơ bản này đảm bảo người dùng chỉ có thể có các vai trò và quyền được định nghĩa trước. Kiểu liệt kê của Zod được định nghĩa chi tiết trong src/types.ts, cung cấp xác thực liệt kê an toàn về kiểu.
Xử lý logic quyền phức tạp
Đối với logic quyền phức tạp hơn, chúng ta có thể sử dụng phương thức refine của Zod để thêm các quy tắc xác thực tùy chỉnh. Ví dụ, chúng ta có thể muốn đảm bảo vai trò quản trị viên có tất cả các quyền:
const QuyềnNguoiDungSchema = z.object({
vaiTro: VaiTroEnum,
quyen: z.array(enumQuyen)
}).refine(data => {
// Quản trị viên phải có tất cả các quyền
if (data.vaiTro === "quan_tri_vien") {
const tatCaQuyen = enumQuyen.options;
return tatCaQuyen.every(quyen => data.quyen.includes(quyen));
}
return true;
}, {
message: "Quản trị viên phải có tất cả các quyền",
path: ["quyen"] // Chỉ định đường dẫn lỗi
});
// Ví dụ xác thực thất bại
const adminKhongHopLe = {
vaiTro: "quan_tri_vien",
quyen: ["doc"]
};
try {
QuyềnNguoiDungSchema.parse(adminKhongHopLe);
} catch (e) {
if (e instanceof z.ZodError) {
console.log(e.issues); // Xuất thông tin lỗi chi tiết
}
}
Cơ chế xử lý lỗi của Zod sẽ trả về thông tin lỗi chi tiết, bao gồm đường dẫn lỗi và thông báo tùy chỉnh. Bạn có thể xem chi tiết triển khai xử lý lỗi trong src/ZodError.ts.
Xác thực quyền cấp tài nguyên
Trong ứng dụng thực tế, quyền thường liên quan đến tài nguyên cụ thể. Ví dụ, người dùng chỉ có thể chỉnh sửa bài viết do chính họ tạo ra. Chúng ta có thể tạo một mô hình xác thực quyền tài nguyên chung:
// Xác định loại tài nguyên
const LoaiTaiNguyenEnum = z.enum(["bai_viet", "binh_luan", "nguoi_dung"]);
// Xác định mô hình quyền tài nguyên
const QuyềnTaiNguyenSchema = z.object({
loaiTaiNguyen: LoaiTaiNguyenEnum,
idTaiNguyen: z.string().uuid(),
hanhDong: enumQuyen,
idNguoiDung: z.string().uuid(),
idChuSoHuuTaiNguyen: z.string().uuid()
}).refine(data => {
// Kiểm tra người dùng có phải là chủ sở hữu tài nguyên không
const laChuSoHuu = data.idNguoiDung === data.idChuSoHuuTaiNguyen;
// Chủ sở hữu có thể thực hiện bất kỳ hành động nào
if (laChuSoHuu) return true;
// Kiểm tra quyền dựa trên loại tài nguyên và hành động
switch(data.loaiTaiNguyen) {
case "bai_viet":
return data.hanhDong === "doc" ||
(data.hanhDong === "ghi" && data.idNguoiDung);
case "binh_luan":
return data.hanhDong === "doc" || data.hanhDong === "ghi";
default:
return false;
}
}, {
message: "Người dùng không có quyền thực hiện hành động này",
path: ["hanhDong"]
});
Mô hình này kết hợp ID người dùng, ID chủ sở hữu tài nguyên, loại tài nguyên và hành động, thực hiện kiểm soát quyền chi tiết cấp tài nguyên.
Xây dựng middleware kiểm tra quyền
Để sử dụng các mô hình quyền này trong ứng dụng thực tế, chúng ta có thể tạo một middleware kiểm tra quyền. Dưới đây là ví dụ về middleware Express:
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
// Mở rộng giao diện yêu cầu Express
declare global {
namespace Express {
interface Request {
nguoiDung?: {
id: string;
vaiTro: string;
quyen: string[];
};
}
}
}
// Tạo middleware kiểm tra quyền
const kiemTraQuyen = (quyenYeuCau: string) => {
return (req: Request, res: Response, next: NextFunction) => {
// Xác thực đối tượng người dùng
const NguoiDungSchema = z.object({
id: z.string().uuid(),
vaiTro: VaiTroEnum,
quyen: z.array(enumQuyen)
});
const ketQua = NguoiDungSchema.safeParse(req.nguoiDung);
if (!ketQua.success) {
return res.status(400).json({
error: "Dữ liệu người dùng không hợp lệ",
details: ketQua.error.format()
});
}
const nguoiDung = ketQua.data;
// Kiểm tra quyền quản trị viên
if (nguoiDung.vaiTro === "quan_tri_vien") {
return next();
}
// Kiểm tra người dùng có quyền cần thiết không
if (nguoiDung.quyen.includes(quyenYeuCau)) {
return next();
}
// Xác thực quyền thất bại
return res.status(403).json({
error: "Quền không đủ",
yeuCau: quyenYeuCau,
quyenNguoiDung: nguoiDung.quyen
});
};
};
// Sử dụng middleware bảo vệ tuyến đường
app.post("/bai_viet", kiemTraQuyen("ghi"), (req, res) => {
// Xử lý logic tạo bài viết
});
Middleware này sử dụng Zod để xác thực đối tượng người dùng và kiểm tra người dùng có quyền cần thiết không. Nếu xác thực thất bại, nó sẽ trả về phản hồi lỗi có cấu trúc, giúp gỡ lỗi và phản hồi cho người dùng.
Xử lý tổ hợp quyền phức tạp
Trong các ứng dụng lớn, quyền thường không phải là danh sách đơn giản mà là tổ hợp phức tạp. Các kiểu kết hợp của Zod (như z.intersection và z.union) có thể giúp chúng ta xử lý các tình huống này. Ví dụ, chúng ta có thể định nghĩa một mô hình kép vừa kiểm tra vai trò vừa kiểm tra quyền:
// Xác định mô hình kiểm tra vai trò
const KiemTraVaiTroSchema = z.object({
vaiTro: z.enum(["quan_tri_vien", "bien_tap_vien"])
});
// Xác định mô hình kiểm tra quyền
const KiemTraQuyenSchema = z.object({
quyen: z.array(z.enum(["xoa"]))
});
// Tạo mô hình kiểm tra quyền tổ hợp
const QuyengXoaSchema = z.intersection(
KiemTraVaiTroSchema,
KiemTraQuyenSchema
);
// Hoặc sử dụng union cho phép tổ hợp quyền khác nhau
const QuyengBienTapSchema = z.union([
z.object({ vaiTro: z.enum(["quan_tri_vien"]) }),
z.object({
vaiTro: z.enum(["bien_tap_vien"]),
quyen: z.array(z.enum(["bien_tap"]))
})
]);
// Xác thực các tổ hợp quyền khác nhau
QuyengXoaSchema.parse({
vaiTro: "quan_tri_vien",
quyen: ["xoa"]
}); // Thành công
QuyengBienTapSchema.parse({
vaiTro: "bien_tap_vien",
quyen: ["bien_tap"]
}); // Thành công
Khả năng kết hợp này cho phép Zod xử lý nhiều mô hình quyền phức tạp, từ kiểm soát truy cập dựa trên vai trò (RBAC) đơn giản đến kiểm soát truy cập dựa trên thuộc tính (ABAC) phức tạp hơn.
Xử lý lỗi xác thực quyền
Điểm mạnh của Zod không chỉ nằm ở khả năng xác thực mà còn ở việc xử lý lỗi chi tiết. Khi xác thực quyền thất bại, Zod sẽ ném ra một đối tượng ZodError chứa thông tin lỗi chi tiết. Chúng ta có thể sử dụng những thông tin này để cung cấp phản hồi có mục tiêu.
try {
// Cố gắng xác thực quyền
QuyềnNguoiDungSchema.parse(dulieuKhongHopLe);
} catch (loi) {
if (loi instanceof z.ZodError) {
// Định dạng thông tin lỗi
const loiDinhDang = loi.format();
// Trích xuất lỗi trường
const loiTruong = loi.flatten().fieldErrors;
// Xuất thông báo lỗi tùy chỉnh
console.log("Xác thực quyền thất bại:");
for (const [truong, loi] of Object.entries(loiTruong)) {
console.log(`- ${truong}: ${loi.join(", ")}`);
}
}
}
Bạn có thể tìm thấy triển khai xử lý lỗi của Zod trong src/ZodError.ts, nó cung cấp nhiều phương thức định dạng và xử lý lỗi. Chúng ta cũng có thể tự định nghĩa thông báo lỗi thông qua hàm setErrorMap trong src/errors.ts để phù hợp hơn với nhu cầu ứng dụng.
Tổng kết và thực hành tốt nhất
Lợi thế chính khi sử dụng Zod cho xác thực quyền bao gồm:
- An toàn kiểu: Tích hợp TypeScript của Zod đảm bảo các mô hình quyền và mã ứng dụng đồng bộ.
- Khả năng kết hợp: Các mô hình đơn giản có thể kết hợp thành logic quyền phức tạp.
- Thông báo lỗi rõ ràng: Báo cáo lỗi chi tiết giúp gỡ lỗi và phản hồi cho người dùng.
- Trách nhiệm đơn lẻ: Tách logic xác thực quyền khỏi logic kinh doanh, nâng cao khả năng bảo trì mã.
Dưới đây là một số thực hành tốt nhất khi sử dụng Zod cho xác thực quyền:
- Bắt đầu với mô hình đơn giản, từng bước xây dựng logic quyền phức tạp.
- Tận dụng kiểu kết hợp của Zod (như
intersectionvàunion) để xử lý tình huống quyền phức tạp. - Sử dụng phương thức
refineđể thêm quy tắc quyền tùy chỉnh, nhưng giữ các quy tắc này đơn giản và dễ hiểu. - Tận dụng tối đa thông tin lỗi của Zod, cung cấp phản hồi có giá trị cho người dùng và nhà phát triển.
- Sử dụng các mô hình quyền ở nhiều tầng ứng dụng, bao gồm xác thực yêu cầu API, kiểm tra trước thao tác cơ sở dữ liệu, v.v.
Bằng cách kết hợp khả năng xác thực mạnh mẽ của Zod với các mô hình quyền hiện đại, bạn có thể xây dựng hệ thống quyền vừa an toàn lại linh hoạt, cung cấp nền tảng bảo mật vững chắc cho ứng dụng của mình. Dù bạn đang xây dựng hệ thống blog đơn giản hay ứng dụng doanh nghiệp phức tạp, Zod đều giúp bạn xử lý dễ dàng hơn sự phức tạp của xác thực quyền.