Thực hiện điều khiển truy cập bằng Agavi

Hãy học để xây dựng các ứng dụng web có khả năng mở rộng với khung công tác Agavi

Agavi là khung công tác mã nguồn mở, linh hoạt và có khả năng mở rộng để phát triển ứng dụng. Một trong những điểm mạnh chính của nó là API đầy đủ tính năng chứng thực người dùng và điều khiển truy cập dựa trên vai trò. Bạn hãy nghiên cứu API này một cách cụ thể và xem cách bổ sung quản lý đặc quyền tinh vi ở cấp ứng dụng lí tưởng và thao tác với ứng dụng Web.

Vikram Vaswani, Tổng Giám đốc, Melonfire

Vikram Vaswani là nhà sáng lập và là tổng giám đốc điều hành (CEO) của công ty Melonfire, công ty dịch vụ tư vấn có chuyên môn đặc biệt về công nghệ và các công cụ mã nguồn mở. Ông cũng là tác giả của các cuốn sách: Giải pháp lập trình PHP và PHP: Hướng dẫn cho những người mới bắt đầu.



10 10 2010

Giới thiệu

Trong các bài trước, tôi đã giới thiệu với bạn khung công tác Agavi MVC, minh họa cách có thể sử dụng nó xây dựng nhanh chóng và hiệu quả ứng dụng web có khả năng mở rộng. Một trong những lý do chính mà tôi chọn Agavi làm khung công tác phát triển là hệ thống xác nhận và lọc nhập tinh vi của nó, hệ thống này đảm bảo rằng các đầu vào không hợp lệ hoặc không được lọc không bao giờ có thể qua được cửa trước của ứng dụng của bạn. Tác vụ này, là yếu tố quan trọng của việc xây dựng ứng dụng web an toàn, đã đơn giản hơn một cách đáng kể bởi các trình xác nhận Agavi có sẵn đối với các chuỗi, số, dấu thời gian, địa chỉ thư điện tử và các tệp, cũng như sự hỗ trợ của nó cho các trình xác nhận tùy chỉnh.

Sự tập trung của Agavi vào bảo mật ứng dụng không dừng ở xác nhận nhập. Khung công tác này cũng cho thấy khả năng xác thực người dùng mạnh mẽ và hệ thống con điều khiển truy cập mà bạn có thể tùy chỉnh nhằm đáp ứng các yêu cầu của hầu hết các ứng dụng Web. Hệ thống con này hỗ trợ cả chứng thực dựa trên đăng nhập đơn giản và điều khiển truy cập dựa trên vai trò (RBAC) phức tạp hơn và nó cung cấp nền tảng vững chắc cho quản lý đặc quyền cấp ứng dụng và thao tác. Tôi sẽ thảo luận chi tiết hơn trong bài này.


Hiểu các khái niệm cơ bản

Các từ viết tắt thường được sử dụng

  • API (Application program interface): Giao diện chương trình ứng dụng
  • MVC (Model-View-Controller): Mô hình-Khung nhìn-Điều khiển
  • XML (Extensible Markup Language): Ngôn ngữ đánh dấu mở rộng

Khi bạn ngồi xuống để xác định các quy tắc truy cập cho Hành động trong ứng dụng Agavi, trước tiên bạn phải nhận thức được rằng có những cấp độ bảo mật khác nhau đã có sẵn. Nói rộng ra, các cấp độ bảo mật này có thể được mô tả theo các khái niệm sau:

  • Mật khẩu Truy cập dựa trên mật khẩu là loại đơn giản nhất của điều khiển truy cập. Về cơ bản, nó cho phép nhà phát triển đánh dấu các hành động nào đó là an toàn, và yêu cầu người dùng nhập vào tập thông tin đăng nhập hợp lệ trước khi cấp quyền truy cập hành động đó. Loại điều khiển truy cập này thích hợp đối với các ứng dụng không đòi hỏi nhiều cấp độ đặc quyền, hoặc khi người sử dụng truy cập vào hệ thống có thể được phân loại một cách rộng rãi thành người sử dụngquản trị viên.

  • Đặc quyền Điều khiển truy cập dựa trên đặc quyền là hệ thống chi tiết hơn so với điều khiển truy cập dựa trên mật khẩu. Trong hệ thống này, nhà phát triển xác định những đặc quyền được yêu cầu thực hiện mỗi hành động, và hệ thống cho phép truy cập vào những hành động đó chỉ khi người sử dụng yêu cầu truy cập có những đặc quyền cần thiết. Cách tiếp cận này bao gồm cả xác thực và ủy quyền. Người sử dụng không những cần có tập hợp lệ của thông tin đăng nhập, mà còn phải có thêm tập các đặc quyền thực hiện các hành động cụ thể. Tuy nhiên, nó có thể nhanh chóng trở nên không quản lý được vì số loại người dùng và mức độ đặc quyền tăng lên.

  • Vai trò Điều khiển truy cập dựa trên vai trò là phiên bản phức tạp hơn và có thể bảo trì được của tiếp cận dựa trên đặc quyền được mô tả trước đó. Cách tiếp cận này định nghĩa tập các vai trò người dùng cho ứng dụng; mỗi vai trò gồm có tập các đặc quyền, và người sử dụng ứng dụng được gán cho một hoặc nhiều vai trò phụ thuộc vào chức năng mà họ cần thực hiện. Loại điều khiển truy cập này thích hợp đối với các ứng dụng với nhiều loại người dùng và nhiều cấp nhiều đặc quyền. Nó đủ linh hoạt để thích ứng nhiều loại nhu cầu chứng thực và ủy quyền đa dạng.


Thiết lập ứng dụng ví dụ

Trước khi bạn có thể bắt đầu thực hiện điều khiển truy cập, tôi có vài lưu ý. Trong suốt bài này, tôi sẽ giả định rằng bạn đã có môi trường phát triển Apache/PHP/MySQL đang làm việc, và bạn đã quen với những điều cơ bản của SQL và XML. Tôi cũng giả định rằng bạn:

  • Thông thạo các nguyên tắc cơ bản của phát triển ứng dụng với Agavi
  • Hiểu sự tương tác giữa hành động, khung nhìn, mô hình và định tuyến
  • Quen thuộc với việc sử dụng các mô hình Doctrine trong ứng dụng Agavi

Trong trường hợp bạn không quen các chủ đề này, bạn nên đọc loạt bài giới thiệu Agavi (xem phần Tài nguyên để lấy liên kết) trước khi đọc bài này.

Bước 1: Khởi tạo ứng dụng mới

Để bắt đầu, trước tiên bạn sẽ thiết lập ứng dụng Agavi đơn giản, hoạt động như nền thử nghiệm đối với các mục tiêu phát triển của bài này. Bạn hãy sử dụng kịch bản xây dựng Agavi khởi tạo dự án mới, chấp nhận giá trị mặc định trừ trường hợp dưới đây:

shell> agavi project-wizard
Project name [New Agavi Project]: ExampleApp
Project prefix (used, for example, in the project base action) []: ExampleApp
Should an Apache .htaccess file with rewrite rules be generated (y/n) [n]? y
...

Khi đã làm xong, bạn hãy xác định máy chủ ảo mới cho ứng dụng thử nghiệm của mình, chẳng hạn http://example.localhost/, trong cấu hình Apache của bạn, và sau đó trỏ trình duyệt của bạn đến máy chủ đó. Bạn sẽ thấy trang chào mặc định của Agavi như trong hình 1.

Hình 1. Trang chào mặc định của ứng dụng Agavi
Trang chào mặc định của ứng dụng Agavi

Bước 2: Thêm mô-đun mới và các hành động tương ứng

Để đơn giản, tôi giả định rằng tất cả các hành động mà bạn muốn bảo vệ được đặt trong mô-đun khác với các mô-đun mặc định. Trở lại dấu nhắc lệnh của bạn và tạo mô-đun Book mới, chứa sáu hành động bằng cách sử dụng kịch bản xây dựng của Agavi như sau:

shell> agavi module-wizard
Module name: Book
Space-separated list of actions to create for Book: 
  Create Index Delete Display Search Edit
...

Sáu hành động này - CreateAction, DeleteAction, DisplayAction, IndexAction, EditAction, và SearchAction- là những hành động mà bạn sẽ sớm bổ sung điều khiển truy cập. Vào thời điểm này, bạn hãy cập nhật khuôn mẫu *Success của mỗi hành động với thông điệp ngắn gọn chỉ rõ mục đích của nó (với * là tên của hành động). Dưới đây là ví dụ về những gì mà khuôn mẫu CreateSuccess có thể chứa:

If you can see this page, you are authorized to create and add new books to the database.

Tại thời điểm này bạn cũng nên loại bỏ mô-đun Welcome, theo như khuyến nghị của các tài liệu về Agavi.

shell> rm -rf app/modules/Welcome
shell> rm -rf app/pub/welcome

Bước 3: Cập nhật bảng định tuyến ứng dụng

Cuối cùng, bạn hãy cập nhật bảng định tuyến ứng dụng tại tệp $ROOT/app/config/routing.xml, với các đường bổ sung trỏ tới các hành động mới như trong liệt kê 1.

Liệt kê 1. Các đường của ứng dụng ví dụ
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
  <ae:configuration>
    <routes>
      <!-- default action for "/" -->
      <route name="index" pattern="^/$" module="Default" action="Index" />
      
      <!-- action for book pages "/book/*" -->
      <route name="book" pattern="^/book" module="Book">
        <route name=".index" pattern="^/index$" action="Index" />
        <route name=".create" pattern="^/create$" action="Create" />
        <route name=".display" pattern="^/display/(id:\d+)$" action="Display" />
        <route name=".edit" pattern="^/edit/(id:\d+)$" action="Edit" />
        <route name=".delete" pattern="^/delete/(id:\d+)$" action="Delete" />
        <route name=".search" pattern="^/search$" action="Search" />
      </route>
            
    </routes>
  </ae:configuration>
</ae:configurations>

Bây giờ bạn có thể truy cập vào các hành động mới được xây dựng bằng cách sử dụng các đường trong liệt kê 1. Để kiểm tra điều này, bạn hãy vào địa chỉ http://example.localhost/book/create và xem bạn đã có trang tương tự như hình 2 chưa.

Hình 2. Trang mặc định cho hành động CreateAction
Trang mặc định cho hành động CreateAction

Theo cách tương tự, hãy kiểm tra các đường khác trong liệt kê 1 trước khi bạn tiến hành phần kế tiếp. Trong trường hợp các hành động này không làm việc, hãy nhớ lại rằng các bước trên được mô tả cụ thể hơn trong phần 1 của loạt bài giới thiệu Agavi (xem phần Tài nguyên để lấy liên kết). Ngoài ra, bạn có thể tải tư liệu mã đầy đủ của ứng dụng ví dụ từ phần Tải về trong bài này.


Thiết lập các hành động đăng nhập và đăng xuất

Dù điều khiển truy cập của ứng dụng của bạn sẽ dựa trên mật khẩu hay trên vai trò, thì bạn vẫn sẽ cần hệ thống đăng nhập và đăng xuất để xử lý xác thực người dùng. Vậy, một khi ứng dụng cơ bản đã được tạo, bước tiếp theo là thực hiện các hành động đăng nhập và đăng xuất.

Bước 1: Khởi tạo cơ sở dữ liệu người dùng và mô hình

Vì thông tin người dùng cho các ứng dụng Web thường được lưu trong cơ sở dữ liệu, cũng như bạn đã làm ở đây. Để bắt đầu, bạn hãy tạo bảng MySQL mới để lưu thông tin người dùng như sau:

mysql> CREATE TABLE IF NOT EXISTS `user`
          (
    -> UserID int(4) NOT NULL AUTO_INCREMENT,
    -> Username varchar(50) CHARACTER SET utf8 NOT NULL,
    -> `Password` text CHARACTER SET utf8 NOT NULL,
    -> PRIMARY KEY (UserID),
    -> UNIQUE KEY Username (Username)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
Query OK, 0 rows affected (0.13 sec)

Hãy điền vài tài khoản vào bảng này để tiếp tục công việc:

mysql> INSERT INTO user (UserID, Username, Password)
          VALUES(1, 'james',
          PASSWORD('james'));
Query OK, 1 row affected (0.08 sec)

mysql> INSERT INTO user
          (UserID, Username, Password) VALUES(2, 'susan',
          PASSWORD('susan'));
Query OK, 1 row affected (0.08 sec)

mysql> INSERT INTO user
          (UserID, Username, Password) VALUES(3, 'marco',
          PASSWORD('marco'));
Query OK, 1 row affected (0.08 sec)

mysql> INSERT INTO user
          (UserID, Username, Password) VALUES(4, 'donald', PASSWORD('donald'));
Query OK, 1 row affected (0.08 sec)

Bạn cũng có thể tạo bảng chứa thông tin đặc quyền, mặc dù sau này bạn sẽ sử dụng chúng một chút:

mysql> CREATE TABLE IF NOT EXISTS `user_access`
          (
    -> RecordID int(4) NOT NULL AUTO_INCREMENT,
    -> UserID int(4) NOT NULL,
    -> UserAccess varchar(255) NOT NULL,
    -> PRIMARY KEY (RecordID),
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.1 sec)

Sau đó bạn tải các ORM của Doctrine (xem phần Tài nguyên để lấy liên kết) và thêm các thư viện của Doctrine vào $ROOT/libs/doctrine. Bạn cũng phải cập nhật các cài đặt ứng dụng của bạn trong tệp $ROOT/app/config/settings.xml nhằm kích hoạt trợ giúp cơ sở dữ liệu và rồi cập nhật tệp cấu hình cơ sở dữ liệu của bạn, thông thường ở trong tệp $ROOT/app/config/databases.xml, sử dụng bộ tương hợp Doctrine trong Agavi. Liệt kê 2 có ví dụ về tệp cấu hình này:

Liệt kê 2. Cấu hình của bộ tương hợp Doctrine của Agavi
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/databases/1.0">
  <ae:configuration>
    <databases default="doctrine">    
      <database name="doctrine" class="AgaviDoctrineDatabase">
        <ae:parameter name="dsn">
         mysql://user:pass@localhost/example
       </ae:parameter>
        <ae:parameter name="load_models">
         %core.lib_dir%/doctrine
       </ae:parameter>
      </database>      
    </databases>
  </ae:configuration>
</ae:configurations>

Tại thời điểm này, bạn có thể sử dụng Doctrine tạo các mô hình cho các bảng này. Nhớ sao chép thủ công các lớp mô hình sinh ra vào thư mục $ROOT/app/lib/doctrine/ của bạn.

shell> cp /tmp/models/User.php app/lib/doctrine/
shell> cp /tmp/models/generated/BaseUser.php app/lib/doctrine/
shell> cp /tmp/models/UserAccess.php app/lib/doctrine/
shell> cp /tmp/models/generated/BaseUserAccess.php app/lib/doctrine/

Quá trình tích hợp Doctrine với Agavi và sử dụng nó tạo các mô hình từ các bảng cơ sở dữ liệu đã được thảo luận chi tiết trong phần 3 của loạt bài giới thiệu Agavi (xem phần Tài nguyên của bài này để lấy liên kết).

Bước 2: Thêm các khung nhìn đăng nhập và đăng xuất khác nhau

Một khi bạn làm theo mô hình, bước tiếp là thêm hành động LoginAction và LogoutAction. Theo mặc định, tập lệnh xây dựng của Agavi sẽ có hành động LoginAction vào thời điểm tạo dự án, vậy tất cả những gì bạn cần là hành động LogoutAction.

shell> agavi action-wizard
Module name: Default
Action name: Logout
Space-separated list of views to create for Save [Success]: Success
...

Bạn cũng cần phải tạo các khuôn mẫu cho các khung nhìn LoginInputView, LoginSuccessView và LoginErrorView, như sau:

shell> agavi template-create
Module name: Default
Template name: LoginSuccess
...

shell> agavi template-create
Module name: Default
Template name: LoginInput
...

shell> agavi template-create
Module name: Default
Template name: LoginError
...

Trong những khung nhìn trên, LoginInputView có thể là quan trọng nhất. Nó chịu trách nhiệm tạo khuôn dạng đăng nhập mà người dùng sẽ thấy khi họ cố gắng truy cập các hành động bị hạn chế. Nó cũng chịu trách nhiệm lưu trữ các URL yêu cầu ban đầu và chuyển hướng người dùng đến URL đó sau khi đăng nhập thành công. Theo cuốn sách hướng dẫn sử dụng Agavi (xem phần Tài nguyên để lấy liên kết), cách dễ nhất làm điều này là lưu trữ các URL yêu cầu ban đầu trong bối cảnh thực hiện, như trong liệt kê 3:

Liệt kê 3. Định nghĩa khung nhìn LoginInputView
<?php

class Default_LoginInputView extends ExampleAppDefaultBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    // get referrer URL and save it
    if($this->getContainer()->hasAttributeNamespace(
      'org.agavi.controller.forwards.login')) {
      $this->getContext()->getUser()->setAttribute(
       'redirect', $this->getContext()->getRequest()->getUrl(), 
       'org.agavi.example.login');
    } else {
      $this->getContext()->getUser()->removeAttribute(
        'redirect', 'org.agavi.example.login');
    }     
    $this->setupHtml($rd);
    $this->setAttribute('_title', 'Login');
  }
}

?>

Liệt kê 4 có mã đối với khuôn mẫu LoginInput tương ứng.

Liệt kê 4. Khuôn mẫu của khung nhìn LoginInput
<form action="<?php echo $ro->gen('login'); ?>" method="post">
  <label for="username" class="required">Username:</label>
  <br/>
  <input id="username" type="text" name="username" style="width:150px" />
  <p/>
  <label for="password" class="required">Password:</label>
  <br/>
  <input id="password" type="password" name="password" style="width:150px" />
  <p/>
  <input type="submit" name="submit" class="submit" value="Log In" />
</form>

Như bạn mong đợi, khuôn mẫu này khá phù hợp với tiêu chuẩn: khuôn dạng với các trường cho tên người dùng và mật khẩu. Hình 3 minh họa khuôn dạng đó.

Hình 3. Trang đăng nhập của ứng dụng
Trang đăng nhập của ứng dụng

Liệt kê 5 có các quy tắc xác nhận nhập cho khuôn dạng trong liệt kê 4:

Liệt kê 5. Trình xác nhận của LoginAction
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
  xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
  xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
  parent="%core.module_dir%/Default/config/validators.xml"
>
  <ae:configuration>
    
    <validators method="write">
      <validator class="string">
        <arguments>
          <argument>username</argument>
        </arguments>
        <errors>
          <error for="required">ERROR: Username is missing</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>
      
      <validator class="string">
        <arguments>
          <argument>password</argument>
        </arguments>
        <errors>
          <error for="required">ERROR: Password is missing</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>
    </validators>
    
  </ae:configuration>
</ae:configurations>

Giả sử đăng nhập thành công, LoginSuccessView tìm yêu cầu URL ban đầu và chuyển hướng máy khách đến khung nhìn đó (liệt kê 6):

Liệt kê 6. Định nghĩa của khung nhìn LoginSuccessView
<?php

class Default_LoginSuccessView extends ExampleAppDefaultBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    if($this->getContext()->getUser()->hasAttribute(
      'redirect', 'org.agavi.example.login')) {
      $this->getResponse()->setRedirect($this->getContext()
        ->getUser()->removeAttribute(
        'redirect', 'org.agavi.example.login'));
      return true;
    }
    $this->setupHtml($rd);
    $this->setAttribute('_title', 'Login');
  }
}

?>

Nếu không có chuyển hướng, thì LoginSuccessView chỉ trả về khuôn mẫu LoginSuccess, khuôn mẫu này chứa thông báo xác nhận đăng nhập (liệt kê 7).

Liệt kê 7. Khuôn mẫu của LoginSuccess
You were successfully logged in.

Ngoài ra, nếu hoạt động đăng nhập thất bại, thì khuôn mẫu LoginError được trả về (liệt kê 8):

Liệt kê 8. Khuôn mẫu của LoginError
There was an error logging you in. Please try again.

Bước 3: Thực hiện các hành động đăng nhập và đăng xuất

Bây giờ bạn đã sẵn sàng thêm một vài mã vào hành động LoginAction. Liệt kê 9 minh họa hành động LoginAction đó, có trách nhiệm đọc thông tin người dùng được gửi thông qua khuôn dạng LoginInput trong liệt kê 4, và xác nhận chúng so với thông tin được lưu giữ trong cơ sở dữ liệu MySQL. Nếu hợp lệ, thì hành động LoginAction sử dụng phương thức setAuthenticated() thiết lập xác thực bằng cờ và trả về khung nhìnLoginSuccessView, nếu không hợp lệ, thì nó trả về khung nhìn LoginErrorView.

Liệt kê 9. Định nghĩa của hành động LoginAction
<?php

class Default_LoginAction extends ExampleAppDefaultBaseAction
{
  public function getDefaultViewName()
  {
    return 'Input';
  }
  
  public function executeWrite(AgaviRequestDataHolder $rd)
  {
    // get input parameters
    $u = $rd->getParameter('username');
    $p = $rd->getParameter('password');
    
    // check user credentials
    $q = Doctrine_Query::create()
          ->from('User u')
          ->where('u.Username = ? AND u.Password = PASSWORD(?)', array($u,$p));
    $result = $q->fetchArray();
      
    // set authentication flag if valid
    if (count($result) == 1) {
      $this->getContext()->getUser()->setAuthenticated(true);
      return 'Success';
    } else {
      return 'Error';
    }
  } 
}

?>

Hành động LogoutAction làm ngược lại: Nó xác lập lại cờ xác thực người dùng và kết thúc phiên của người dùng. Liệt kê 10 có mã cho hành động này:

Liệt kê 10. Định nghĩa của hành động LogoutAction
<?php

class Default_LogoutAction extends ExampleAppDefaultBaseAction
{
  public function getDefaultViewName()
  {
    return 'Success';
  }
  
  public function executeRead()
  {
    $this->getContext()->getUser()->setAuthenticated(false);
    return 'Success';
  }
}

?>

Bước 4: Cập nhật bảng định tuyến của ứng dụng

Bước cuối cùng là cập nhật bảng định tuyến của ứng dụng với các đường bổ sung cho các hành động đăng nhập và đăng xuất. Liệt kê 11 hiển thị các đường đã được cập nhật:

Liệt kê 11. Các tuyến đã được cập nhật của ứng dụng ví dụ
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
  <ae:configuration>
    <routes>
      <!-- default action for "/" -->
      <route name="index" pattern="^/$" module="Default" action="Index" />
      
      <!-- action for login page "/login" -->
      <route name="login" pattern="^/login$" module="Default" action="Login" />
      
      <!-- action for admin logout pages "/logout" -->
      <route name="logout" pattern="^/logout$" module="Default" action="Logout" />
		 		 		       
      <!-- action for book pages "/book/*" -->
      <route name="book" pattern="^/book" module="Book">
        <route name=".index" pattern="^/index$" action="Index" />
        <route name=".create" pattern="^/create$" action="Create" />
        <route name=".display" pattern="^/display/(id:\d+)$" action="Display" />
        <route name=".edit" pattern="^/edit/(id:\d+)$" action="Edit" />
        <route name=".delete" pattern="^/delete/(id:\d+)$" action="Delete" />
        <route name=".search" pattern="^/search$" action="Search" />
      </route>            
    </routes>
  </ae:configuration>
</ae:configurations>

Tại thời điểm này, bạn có hệ thống đăng nhập và đăng xuất đã hoạt động. Bạn hãy tự thử hoạt động đó bằng cách truy cập vào địa chỉ http://example.localhost/login và nhập thông tin phù hợp với những thông tin được thiết lập trước đó trong bảng người dùng. Nếu mọi thứ diễn ra tốt đẹp, bạn sẽ thấy thông báo đăng nhập thành công, như trong hình 4.

Hình 4. Kết quả của việc đăng nhập thành công
Kết quả của việc đăng nhập thành công' message

Nếu bạn nhập vào tên người dùng hoặc mật khẩu không đúng, bạn sẽ thấy thông báo lỗi đăng nhập, như trong hình 5.

Hình 5. Kết quả của đăng nhập không thành công
Screen capture with result of an unsuccessful login with 'There was an error logging in. Please try again.' message

Điều khiển truy cập bằng mật khẩu

Một khi bạn đã có khung công tác đăng nhập và đăng xuất đang hoạt động, rất dễ giới hạn truy cập cho những một vài hành động. Để minh họa, tôi giả định rằng tất cả các hành động trong mô-đun Book yêu cầu phải chứng thực. Để kích hoạt tính năng này, bạn chỉ cần biên tập từng lớp hành động và thêm phương thức isSecure() trả về giá trị true. Liệt kê 12 có ví dụ về hành động CreateAction đã được sửa đổi:

Liệt kê 12. Định nghĩa của hành động CreateAction
<?php

class Book_CreateAction extends ExampleAppBookBaseAction
{
  public function getDefaultViewName()
  {
    return 'Success';
  }
  
  public function isSecure()
  {
    return true;
  } 
}

?>

Và bây giờ, khi bạn cố gắng truy cập vào bất cứ hành động nào trong những hành động này, ví dụ : Hành động CreateAction tại địa chỉ http://example.localhost/book/create, đầu tiên Agavi sẽ chuyển hướng bạn đến hành động LoginAction và chỉ hiển thị URL được yêu cầu một khi bạn nhập tên người dùng và mật khẩu hợp lệ.

Điều quan trọng là phải cần lưu ý là sau khi đăng nhập truy cập vào bất kỳ một hành động nào, bạn cũng có thể truy cập các hành động còn lại mà không gặp phải bất kỳ yêu cầu đăng nhập thêm nào. Đây là điều quan trọng bởi vì nó minh họa trong những sai sót chính của cách tiếp cận đơn giản, chỉ dựa trên mật khẩu - tức là, một khi đã được chứng thực, người sử dụng có thể truy cập vào bất kỳ hoặc tất cả các hành động được bảo vệ nào. Nói cách khác, tiếp cận đơn giản này không phân biệt các loại hình khác nhau của người dùng, làm cho nó không phù hợp đối với các ứng dụng yêu cầu điều khiển truy cập chi tiết. Và đó là nơi mà tiếp cận dựa trên đặc quyền được sử dụng.


Điều khiển truy cập với các đặc quyền

Theo cách tiếp cận dựa trên đặc quyền, mỗi hành động yêu cầu các đặc quyền nhất định và mỗi người dùng được chứng thực có đặc quyền nhất định. Hành động chỉ có thể được thực hiện bởi người dùng có những đặc quyền cần thiết cho hành động đó. Điều này cho phép hành động được hạn chế trên cơ sở theo người sử dụng, cho phép kiểm soát gắt gao người dùng nào có thể truy cập hành động nào.

Bước 1: Thiết lập các đặc quyền hành động cần thiết

Để minh họa việc kiểm soát này hoạt động như thế nào, tôi giả định các hạn chế sau:

  • Các hành động CreateAction và DeleteAction chỉ có thể được gọi ra bởi người dùng có đặc quyền của 'book.create'.
  • Hành động EditAction chỉ có thể được gọi ra bởi người dùng có đặc quyền 'book.edit'.
  • Hành động IndexAction chỉ có thể được gọi ra bởi người dùng có đặc quyền 'book.index'.
  • Hành động DisplayAction chỉ có thể được gọi ra bởi người dùng có đặc quyền 'book.display'.
  • Hành động SearchAction chỉ có thể được gọi ra bởi người dùng có cả hai đặc quyền 'book.index' và 'book.display'.

Trong hành động, các đặc quyền này được thiết lập nhờ phương thức getCredentials() của hành động, trả về những đặc quyền cần thiết sử dụng hành động đó. Bạn hãy xem liệt kê 13, minh hoạ hành động CreateAction đã được sửa đổi:

Liệt kê 13. Định nghĩa của hành động CreateAction
<?php

class Book_CreateAction extends ExampleAppBookBaseAction
{
  public function getDefaultViewName()
  {
    return 'Success';
  }
  
  public function isSecure()
  {
    return true;
  }
  
  public function getCredentials()
  {
    return 'book.create';
  } 
}

?>

Tương tự như vậy, liệt kê 14 cập nhật hành động IndexAction để quy định rằng chỉ người dùng với đặc quyền 'book.index' mới có thể truy cập nó:

Liệt kê 14. Định nghĩa của hành động IndexAction
<?php

class Book_IndexAction extends ExampleAppBookBaseAction
{
  public function getDefaultViewName()
  {
    return 'Success';
  }
  
  public function isSecure()
  {
    return true;
  } 
  
  public function getCredentials()
  {
    return 'book.index';    
  }   
}

?>

Liệt kê 15 cập nhật hành động SearchAction minh họa rằng hai đặc quyền 'book.index' và 'book.display' được yêu cầu truy cập hành động đó, vì nó có phương thức getCredentials() trả về mảng:

Liệt kê 15. Định nghĩa của hành động SearchAction
<?php

class Book_SearchAction extends ExampleAppBookBaseAction
{
  public function getDefaultViewName()
  {
    return 'Success';
  }
  
  public function isSecure()
  {
    return true;
  } 
  
  public function getCredentials()
  {
    return array('book.index', 'book.display');    
  }   
  
}

?>

Bước 2: Thiết lập đặc quyền người dùng

Một khi bạn đã có hành động đã được cấu hình, bước tiếp theo là gán các đặc quyền cho người sử dụng. Giả sử rằng:

  • Người dùng 'james' có đặc quyền 'book.index'
  • Người dùng 'susan' có các đặc quyền 'book.index' và 'book.display'
  • Người dùng 'marco' có các đặc quyền 'book.edit ' và 'book.display'
  • Người dùng 'donald' có các đặc quyền 'book.index', 'book.display' và 'book.create'

Thêm các đặc quyền này vào cơ sở dữ liệu MySQL bằng cách sử dụng câu SQL sau đây:

mysql> INSERT INTO user_access (UserID, UserAccess) VALUES
    -> (1, 'book.index'),
    -> (2, 'book.index'),
    -> (2, 'book.display'),
    -> (3, 'book.display'),
    -> (3, 'book.edit'),
    -> (4, 'book.index'),
    -> (4, 'book.display'),
    -> (4, 'book.create');
Query OK, 8 rows affected (0.05 sec)
Records: 8  Duplicates: 0  Warnings: 0

Hình 6 minh hoạ mối quan hệ giữa người dùng và đặc quyền trong cơ sở dữ liệu MySQL. (Xem phần văn bản của hình 6.)

Hình 6. Ánh xạ đặc quyền-người dùng
Ánh xạ đặc quyền-người dùng

Bước 3: Tìm và gán đặc quyền người sử dụng theo thời gian chạy thực

Bước cuối cùng là cập nhật hành động LoginAction, lấy ra các đặc quyền của mỗi người dùng từ cơ sở dữ liệu tại thời điểm đăng nhập, và gán chúng cho các đối tượng người dùng. Liệt kê 16 có chứa mã đã được cập nhật:

Liệt kê 16. Định nghĩa của hành động LoginAction được cập nhật
<?php

class Default_LoginAction extends ExampleAppDefaultBaseAction
{
  public function getDefaultViewName()
  {
    return 'Input';
  }
  
  public function executeWrite(AgaviRequestDataHolder $rd)
  {
    // get input parameters
    $u = $rd->getParameter('username');
    $p = $rd->getParameter('password');
    
    // check user credentials
    $q = Doctrine_Query::create()
          ->from('User u')
          ->where('u.Username = ? AND u.Password = PASSWORD(?)', array($u,$p));
    $result = $q->fetchArray();
      
    // set authentication flag if valid
    if (count($result) == 1) {
      $this->getContext()->getUser()->setAuthenticated(true);

      // get credentials and attach to user object
      $this->getContext()->getUser()->clearCredentials();      
      $q = Doctrine_Query::create()
            ->from('UserAccess ua')
            ->where('ua.UserID = ?', array($result[0]['UserID']));
      $rs = $q->fetchArray();
      foreach ($rs as $r) {
        $this->getContext()->getUser()->addCredential(trim($r['UserAccess']));        
      }
      return 'Success';
    } else {
      return 'Error';
    }
  } 
}

?>

Vào thời điểm này, một khi thông tin đăng nhập của người dùng được xác minh, truy vấn bổ sung được thực hiện để lấy ra những đặc quyền của người dùng đó. Các đặc quyền này sau đó được gắn với đối tượng người dùng bằng phương thức addCredential() và các đối tượng LoginSuccessView được trả về. Bạn cũng lưu ý phương thức clearCredentials(), được sử dụng để xóa tất cả thông tin người dùng. Để tối đa hóa bảo mật, điều này nên được thực hiện trước mọi hoạt động liên quan đến cấp quyền, cũng như khi người sử dụng đăng xuất.

Để thấy các tính năng này hoạt động như thế nào, hãy thử đăng nhập với tên người dùng james. Khi đã đăng nhập, có thể truy cập hành động IndexAction. Tuy nhiên, mọi ý định truy cập vào các hành động khác sẽ nhận được câu trả lời Access Denied - Từ chối truy cập, như trong hình 7.

Hình 7. Kết quả của việc cố gắng truy cập hành động được cấp đặc quyền
Kết quả của việc cố gắng truy cập hành động được cấp đặc quyền

Mặt khác, nếu bạn đăng nhập với tên là marco, bạn sẽ có thể truy cập vào các hành động DisplayAction và EditAction, nhưng không truy cập được vào các hành động khác. Nếu bạn đăng nhập với tên là susan, bạn có quyền truy cập vào các hành động IndexAction, DisplayAction và SearchAction. Và nếu bạn đăng nhập với tên là donald, bạn có quyền truy cập vào các hành động IndexAction, DisplayAction, CreateAction, DeleteAction và SearchAction.

Cách tiếp cận này rõ ràng cung cấp sự điều khiển chính xác hơn so với việc chứng thực dựa trên mật khẩu đơn giản, như ở phần trước, và nó được khuyến khích cho các ứng dụng đòi hỏi nhiều cấp điều khiển truy cập. Tuy nhiên, vì số các cấp đặc quyền và người sử dụng tăng lên, nên việc duy trì ánh xạ đặc quyền người sử dụng trở nên ngày càng trở nên phức tạp và tốn thời gian hơn. Và như vậy ta phải cần đến điều khiển truy cập dựa trên vai trò.


Thực hiện điều khiển truy cập dựa trên vai trò

RBAC là kỹ thuật phổ biến được dùng để xử lý các tác vụ ủy quyền trong ứng dụng đa người dùng, đa quyền. Về cơ bản, nó cho phép bạn xác định các vai trò của người dùng, mỗi vai trò bao gồm tập các đặc quyền được xác định từ trước, và gán các vai trò này cho người sử dụng ứng dụng. Khi người dùng đăng nhập, người đó đó được liên kết với một hoặc nhiều vai trò, và họ tự động có được tất cả những đặc quyền đi theo các vai trò đó.

Bước 1: Xác định vai trò và đặc quyền

Giống như hầu hết những thứ khác trong Agavi, vai trò và đặc quyền được quy định trong tệp cấu hình XML, có vị trí mặc định tại địa chỉ $ROOT/app/config/rbac_definitions.xml. Các tệp định nghĩa được tự động đọc bởi đối tượng AgaviRbacSecurityUser. Liệt kê 17 có ví dụ về tệp này:

Liệt kê 17. Đinh nghĩa vai trò trong ứng dụng ví dụ
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
  xmlns="http://agavi.org/agavi/config/parts/rbac_definitions/1.0">
  <ae:configuration>
    <roles>
      <role name="visitor">
        <permissions>
          <permission>book.index</permission>
        </permissions>
        <roles>
          <role name="student">
            <permissions>
              <permission>book.display</permission>
            </permissions>
            <roles>
              <role name="manager">
                <permissions>
                  <permission>book.create</permission>
                </permissions>
              </role>
            </roles>
          </role>
        </roles>
      </role>
      <role name="librarian">
        <permissions>
          <permission>book.display</permission>
          <permission>book.edit</permission>
        </permissions>
      </role>           
    </roles>
  </ae:configuration>
</ae:configurations>

Tệp này định nghĩa bốn vai trò: nhà quản lý, thủ thư, sinh viên và khách truy cập. Mỗi vai trò được liên kết tới các đặc quyền khác nhau. Theo như cấu trúc của tệp XML minh họa, thì các vai có thể được lồng vào nhau: Vai trò con thừa hưởng những đặc quyền của vai trò cha của nó. Do đó, nhà quản lý có cả hai đặc quyền khách riêng của mình, cũng như những đặc quyền của sinh viên và khách truy cập.

Bước 2: Gán vai trò cho người dùng

Bước tiếp theo là phân công vai trò cho người dùng. Giả sử rằng:

  • Người dùng james là khách truy cập.
  • Người dùng susan là sinh viên.
  • Người dùng marco là thủ thư.
  • Người dùng donald là nhà quản lý.

Bạn có thể sử dụng lại bảng MySQL hiện có để gán vai trò cho mỗi người dùng, như hình dưới đây:

mysql> TRUNCATE TABLE user_access;
Query OK, 0 rows affected (0.06 sec)

mysql> INSERT INTO user_access (UserID, UserAccess) VALUES
    -> (1, 'visitor'),
    -> (2, 'student'),
    -> (3, 'librarian'),
    -> (4, 'manager');
Query OK, 4 rows affected (0.05 sec)
Records: 4  Duplicates: 0  Warnings: 0

Hình 8 minh họa mối quan hệ giữa người dùng và vai trò trong cơ sở dữ liệu MySQL. (Xem phiên bản chỉ là văn bản của hình 8.)

Hình 8. Ánh xạ vai trò-người sử dụng
Ánh xạ vai trò-người sử dụng

Bước 3: Xử lý đối tượng AgaviRbacSecurityUser

Theo mặc định, Agavi thể hiện người sử dụng ứng dụng bằng đối tượng AgaviSecurityUser. Tuy nhiên, đối tượng này thiếu những phương thức cần thiết gán vai trò thời gian chạy thực và hủy bỏ các vai trò đó. Vì thế, bạn phải lệnh cho Agavi xử lý đối tượng AgaviRbacSecurityUser thay vì đối tượng AgaviSecurityUser, bằng cách biên tập tệp $ROOT/app/config/factories.xml và cập nhật liệt kê các nơi sản xuất lớp như trong liệt kê 18:

Liệt kê 18. Cấu hình nơi sản xuất lớp của Agavi
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
  xmlns="http://agavi.org/agavi/config/parts/factories/1.0">
  
  <ae:configuration>    
    ...
    <user class="AgaviRbacSecurityUser" />
    ...
  </ae:configuration>
  
</ae:configurations>

Bước 4: Lấy ra và gán vai trò người sử dụng theo thời gian chạy thực

Bước cuối cùng là cập nhật hành động LoginAction để tìm các vai trò cho mỗi người dùng từ cơ sở dữ liệu một khi chứng thực thành công và gán các vai trò này cho đối tượng người dùng. Liệt kê 19 có đoạn mã đã cập nhật cho hành động LoginAction:

Liệt kê 19. Định nghĩa của hành động LoginAction được cập nhật
<?php

class Default_LoginAction extends ExampleAppDefaultBaseAction
{
  public function getDefaultViewName()
  {
    return 'Input';
  }
  
  public function executeWrite(AgaviRequestDataHolder $rd)
  {
    // get input parameters
    $u = $rd->getParameter('username');
    $p = $rd->getParameter('password');
    
    // check user credentials
    $q = Doctrine_Query::create()
          ->from('User u')
          ->where('u.Username = ? AND u.Password = PASSWORD(?)', array($u,$p));
    $result = $q->fetchArray();
      
    // set authentication flag if valid
    if (count($result) == 1) {
      $this->getContext()->getUser()->setAuthenticated(true);
      // get and grant roles
      $this->getContext()->getUser()->revokeAllRoles();        
      $q = Doctrine_Query::create()
            ->from('UserAccess ua')
            ->where('ua.UserID = ?', array($result[0]['UserID']));
      $rs = $q->fetchArray();
      foreach ($rs as $r) {
        $this->getContext()->getUser()->grantRole(trim($r['UserAccess']));        
      }
      return 'Success';
    } else {
      return 'Error';
    }
  } 
}

?>

Một khi thông tin đăng nhập của người dùng được xác minh, truy vấn bổ sung được thực hiện để tìm vai trò người dùng đó. Lúc đó các vai trò này được gắn với đối tượng AgaviRbacSecurityUser bằng phương thức grantRole() và đối tượng LoginSuccessView được trả về. Bạn lưu ý phương thức revokeAllRoles(), được dùng để xóa đi tất cả vai trò hiện có của người dùng trước khi gán các vai trò mới, cũng như khi người sử dụng đăng xuất.

Nếu bạn đăng nhập với tên là james, bạn chỉ có thể truy cập vào hành động IndexAction. Mặt khác, nếu bạn đăng nhập với tên là marco, bạn có thể truy cập vào hành động DisplayAction và EditAction, nhưng không truy cập được vào các hành động khác. Nếu bạn đăng nhập với tên là susan, bạn có quyền truy cập vào các hành động IndexAction, DisplayAction và SearchAction. Và nếu bạn đăng nhập với tên là donald, thì bạn có quyền truy cập vào các hành động IndexAction, DisplayAction, CreateAction, DeleteAction và SearchAction.

Vì vai trò đơn có thể có nhiều đặc quyền, và người sử dụng đơn có thể có nhiều vai trò, nên việc thực hiện kỹ thuật RBAC của Agavi làm cho nó trở nên dễ dàng tạo hệ thống đặc quyền đa cấp. Công tác bảo trì cũng đơn giản một cách đáng kể, vì bạn có thể thay đổi đặc quyền áp dụng đối với từng vai trò, và có các thay đổi được tự động xếp theo lớp đối với tất cả người dùng trong các vai trò đó, chỉ bằng cách biên tập tệp XML định nghĩa vai trò. Điều này làm cho Agavi trở nên lý tưởng đối với các ứng dụng web có độ phức tạp cao và yêu cầu có sự kiểm soát cao đối với việc người dùng nào có quyền truy cập vào chức năng nào. Và, rõ ràng là bạn có thể (và chắc chắn) mở rộng đối tượng AgaviRbacSecurityUser cơ sở bằng các phương thức bổ sung liên quan đến các yêu cầu quản lý người dùng của riêng bạn.


Kết luận

Từ các cuộc thảo luận trước ta thấy rõ ràng là cơ chế điều khiển truy cập của Agavi có gì đó cho tất cả mọi người. Bất kể bạn đang tìm kiếm hệ thống đặc quyền phân cấp hoặc không có đặc quyền, thì các đối tượng có sẵn của Agavi làm cho nó trở nên dễ dàng triển khai thực hiện kiến trúc an toàn, mạnh mẽ và bảo vệ các truy cập vào các chức năng ứng dụng một cách đơn giản và tao nhã. Bản chất mô-đun của điều khiển truy cập của Agavi cũng có nghĩa là bạn có thể thêm điều khiển truy cập vào ứng dụng của bạn vào bất cứ lúc nào trong giai đoạn thực hiện, hoặc thậm chí sau khi ứng dụng đã được triển khai, với tác động tối thiểu đến logic nghiệp vụ hiện tại.

Phần Tải về có tất cả các mã thực hiện trong bài này. Tôi khuyên bạn nên tải nó, bắt đầu thực tập nó và có thể thử bổ sung các ứng dụng mới. Tôi đảm bảo bạn sẽ không va vấp điều gì, và nó chắc chắn sẽ bổ sung việc học tập của bạn. Chúc bạn vui!


Tải về

Mô tảTênKích thước
Archive of the example application in this articleexample-app-rbac.zip3784KB

Tài nguyên

Học tập

Lấy sản phẩm và công nghệ

Thảo luận

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Ở lần bạn đăng nhập đầu tiên vào trang developerWorks, một hồ sơ cá nhân của bạn được tạo ra. Thông tin trong bản hồ sơ này (tên bạn, nước/vùng lãnh thổ, và tên cơ quan) sẽ được trưng ra cho mọi người và sẽ đi cùng các nội dung mà bạn đăng, trừ khi bạn chọn việc ẩn tên cơ quan của bạn. Bạn có thể cập nhật tài khoản trên trang IBM bất cứ khi nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Nguồn mở
ArticleID=549714
ArticleTitle=Thực hiện điều khiển truy cập bằng Agavi
publish-date=10102010