Quản lý đăng nhập qua cơ sở dữ liệu với kbmMW

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.

Thẻ: kbmMW Delphi SQLite orm REST

Đăng vào ngày 1 tháng 6 lúc 13:52