Giới thiệu
Khi xây dựng ứng dụng server với khả năng phân quyền và đăng nhập, một câu hỏi thường gặp là làm thế nào để lưu trữ người dùng và vai trò của họ trong cơ sở dữ liệu. Bài viết này hướng dẫn cách sử dụng TkbmMWAuthorizationManager để giải quyết vấn đề này. Bạn có thể tham khảo thêm bài viết trước: REST easy with kbmMW #4 – Quản lý truy cập.
Đầu tiên, cần một server hỗ trợ đăng nhập. Trong ví dụ này, tôi chọn server REST FishFact. Chi tiết triển khai server này có trong bài viết kbmMW #12 – Fishfact demo sử dụng HTTP.sys transport.
Thêm bảo mật đăng nhập
Trên server hiện có, thêm TkbmMWAuthorizationManager vào form chính (Unit1).
Tiếp theo, xác định cách lưu trữ và truy xuất thông tin người dùng từ cơ sở dữ liệu. Vì server này đã sử dụng ORM, chúng ta sẽ dùng ORM để quản lý người dùng. Hãy tạo một lớp mô tả người dùng:
[kbmMW_Table('name:user')]
TUser = class
private
FID: kbmMWNullable<string>;
FName: kbmMWNullable<string>;
FPassword: kbmMWNullable<string>;
FRole: kbmMWNullable<string>;
public
[kbmMW_Field('name:"id", primary:true, generator:shortGuid', ftString, 38)]
property ID: kbmMWNullable<string> read FID write FID;
[kbmMW_Field('name:"name"', ftString, 50)]
[kbmMW_NotNull]
property Name: kbmMWNullable<string> read FName write FName;
// Hệ thống bảo mật không nên lưu mật khẩu dạng plaintext, chỉ lưu hash SHA256.
// Trong trường hợp đó, cần 64 ký tự.
[kbmMW_Field('name:"password"', ftString, 50)]
property Password: kbmMWNullable<string> read FPassword write FPassword;
[kbmMW_Field('name:"role"', ftString, 30)]
property Role: kbmMWNullable<string> read FRole write FRole;
end;
Lưu ý: Trong ví dụ này, mật khẩu được lưu dưới dạng plaintext để dễ minh họa. Trong hệ thống thực tế, tuyệt đối không làm vậy. Thay vào đó, hãy lưu kết quả hash của mật khẩu. Phần sau sẽ giải thích chi tiết.
Trong sự kiện Form.OnCreate, cần đảm bảo ORM tạo bảng user và định nghĩa các vai trò (role) mà server chấp nhận:
procedure TfrmMain.FormCreate(Sender: TObject);
begin
FORM := TkbmMWORM.Create;
FORM.OpenDatabase(kbmMWSQLiteConnectionPool1);
FORM.CreateOrUpgradeTable(TUser);
// Thêm vai trò duy nhất ngoài anonymous
AuthMgr.AddRole('AdminRole');
kbmMWServer1.AutoRegisterServices;
end;
Điểm đáng chú ý là lệnh gọi CreateOrUpgradeTable đảm bảo tạo bảng user trong cơ sở dữ liệu và định nghĩa vai trò AdminRole.
Đăng ký lớp TUser với kbmMW trong phần initialization của Unit1:
...
initialization
TkbmMWRTTI.EnableRTTI([TUser]);
kbmMWRegisterKnownClasses([TUser]);
end.
Triển khai đăng nhập với Authorization Manager
Giờ ta sẽ viết logic cho sự kiện OnLogin của authorization manager. Authorization manager cần biết ai đang đăng nhập (actor). Danh sách actor có thể được định nghĩa khi khởi động server hoặc tạo động khi cần. Phương pháp tạo động linh hoạt hơn và được áp dụng ở đây:
procedure TfrmMain.AuthMgrLogin(Sender: TObject; const AActorName,
ARoleName: string; var APassPhrase: string;
var AActor: TkbmMWAuthorizationActor; var ARole: TkbmMWAuthorizationRole;
var AMessage: string);
var
user: TUser;
begin
// Tìm kiếm người dùng với tên và mật khẩu
user := ORM.Query<TUser>(['Name', 'Password'], [AActorName, APassPhrase]);
if user <> nil then
try
// Kiểm tra vai trò của người dùng có được định nghĩa không
ARole := AuthMgr.Roles.Get(user.Role.Value);
if ARole = nil then
AMessage := 'Role not supported'
else
begin
// Kiểm tra actor đã tồn tại chưa, nếu chưa thì tạo mới
AActor := AuthMgr.GetActor(AActorName);
if AActor = nil then
AActor := AuthMgr.AddActor(AActorName, APassPhrase, ARoleName);
AMessage := 'User found and is allowed login';
end;
finally
user.Free;
end
else
AMessage := 'User not found';
end;
Đoạn code trên dùng ORM để truy vấn người dùng theo tên và mật khẩu. Nếu tìm thấy, nó kiểm tra vai trò của người dùng có tồn tại trong authorization manager không. Nếu có, nó kiểm tra và tạo actor tương ứng.
Cập nhật và xóa người dùng
Khi người dùng thay đổi mật khẩu, cần cập nhật cả cơ sở dữ liệu và authorization manager:
procedure TUnit1.UpdateUserPassword(const AUserName, ANewPassword: string);
var
user: TUser;
begin
AuthMgr.ChangeActorPassword(AUserName, ANewPassword);
user := ORM.Query<TUser>(['Name'], [AUserName]);
if user <> nil then
try
user.Password := ANewPassword;
ORM.Update(user);
finally
user.Free;
end;
end;
Để xóa người dùng:
procedure TUnit1.RemoveUser(const AUserName: string);
begin
AuthMgr.DeleteActor(AUserName);
ORM.Delete<TUser>(['Name'], [AUserName]);
end;
Bảo vệ các REST method
Cuối cùng, cần xác định quyền truy cập cho từng REST endpoint. Mở Unit2.pas (chứa REST service) và thêm thuộc tính kbmMW_Auth vào các method cần bảo vệ. Ví dụ, giới hạn GetSpecieByCategory chỉ dành cho vai trò Admin:
[kbmMW_Rest('method:get, path:"specieByCategory/{category}"')]
[kbmMW_Auth('role:[AdminRole], grant:true')]
function GetSpecieByCategory([kbmMW_Rest('value:"{category}"')] const ACategory: string): TBiolifeNoImage;
Đừng quên thêm kbmMWSecurity vào mệnh đề uses của Unit (file kbmMWSecurity.pas chứa định nghĩa kbmMW_Auth).
Sau khi đảm bảo cơ sở dữ liệu có ít nhất một người dùng với vai trò AdminRole, chạy server và thử gọi REST endpoint:
http://localhost:1111/biolife/specieByCategory/Butterflyfish
Trình duyệt sẽ yêu cầu đăng nhập. Nếu nhập đúng thông tin người dùng có vai trò AdminRole, kết quả sẽ hiển thị. Nếu không, server trả về lỗi. Các endpoint không được gắn kbmMW_Auth vẫn cho phép truy cập ẩn danh.
Bảo mật mật khẩu với Hashing
Như đã đề cập, không nên lưu hoặc truyền mật khẩu dạng plaintext. Giải pháp là dùng hashing (băm một chiều). kbmMW hỗ trợ nhiều thuật toán băm an toàn, phổ biến nhất là SHA256.
Trong trình duyệt (REST client), SSL (HTTPS) là cách tốt nhất để bảo vệ dữ liệu truyền tải, bao gồm tên người dùng và mật khẩu. Chi tiết xem bài viết REST easy with kbmMW #3 – SSL.
Đối với lưu trữ, ta dùng SHA256 kết hợp với "salt" để tăng độ an toàn. Thêm kbmMWHashSHA256 vào mệnh đề uses và sửa logic OnLogin:
var
hashed: string;
begin
hashed := TkbmMWHashSHA256.HashAsString(APassPhrase, 'somesaltvalue'); // Yêu cầu kbmMW 5.06+
// Gán hashed vào APassPhrase trước khi dùng để truy vấn
APassPhrase := hashed;
// Tiếp tục logic truy vấn như cũ...
end;
Giá trị 'somesaltvalue' nên là một chuỗi ngẫu nhiên dài, được giữ bí mật. "Salt" làm cho việc brute-force mật khẩu trở nên khó khăn hơn nhiều, ngay cả khi kẻ tấn công có được dữ liệu hash.
Bây giờ, hash của mật khẩu (kết hợp với salt) được lưu trong cơ sở dữ liệu. Mỗi lần đăng nhập, server sẽ hash mật khẩu đầu vào (với cùng salt) rồi so sánh với giá trị trong database.