Yii Framework

Yii Framework

Xác thực và phân quyền cơ bản trong Yii Framework

Trong Yii đã xây dựng sẵn chức năng AuthManager mạnh mẽ, nhưng có thể sẽ hơi phức tạp với người mới bắt đầu. Chức năng cơ bản nhất phải làm khi học bất kỳ ngôn ngữ nào; đó là xác thực (authentication) và phân quyền người dùng (authorization). Trong phạm vi bài viết này tôi chỉ hướng dẫn cơ bản về chức năng đăng nhập và phân quyền dựa trên nhóm người dùng (role).

 

Bài viết này sẽ truy cập database thông qua ActiveRecord. Bạn có thể dùng Query Builder hoặc DAO tùy thích.

 

Để bắt đầu tôi cần 4 table như sau:
tc_user: lưu thông tin cơ bản của người dùng (bạn có thể thêm các trường khác theo yêu cầu dự án của bạn)
tc_user_auth: chứa danh sách các nhóm quyền (admin, member)
tc_user_auth_assignment: chứa thông tin khi người dùng đăng nhập, có quyền gì sẽ lưu hết vào đây (tôi chọn lưu trong database, không lưu trên file)
tc_user_auth_item_child: chứa thông tin phân quyền dạng parent/child

xac-thuc-phan-quyen-h1.jpg

xac-thuc-phan-quyen-h2.jpg

 

Trong các hướng dẫn viết trong Yii Blog, Agile đã có viết khá rõ về chức năng đăng nhập và phân quyền. Tuy nhiên tôi chắc chắn một điều nó không phải quá đơn giản với các bạn mới làm quen với Yii, nhất là phần phân quyền. Tôi cũng từng mất khá nhiều thời gian, làm đi làm lại mà không được. Tức quá định viết ra chức năng phân quyền của riêng mình dựa trên các hướng dẫn của Sitepoint về RBAC. Nhưng cuối cùng cũng ngộ ra được cách làm việc của Yii Framework, nó thật sự rất mạnh. Tôi thích nó, sử dụng nó mà không cần quan tâm code bên dưới xử lý thế nào. Trình độ có hạn nên nếu xem chưa chắc có thể hiểu được.

 

Thôi không chém gió nữa làm mất thời gian của bạn.
Trước tiên, bạn dùng Gii tạo model User và CRUD cho model này để tiện thêm bớt user. Thích thằng Yii về cái Gii, rất nhanh, hạn chế sai sót do gõ nhầm tên trường của table

 

Bạn cần tạo ra một class UserIdentity đặt trong protected/components để lấy thông tin của người dùng từ table tc_user, trả về ID lưu trong class UserIdentity thông qua phương pháp xác thực và phân quyền cơ bản.
Bạn có thể tham khảo class UserIdentity của tôi như sau:

//protected/compoments/UserIdentity.php
 class UserIdentity extends CUserIdentity {
 
     public $autoLogin;
     private $_id;
 
     public function __construct($username, $password, $autoLogin) {
         $this->username = $username;
         $this->password = $password;
         $this->autoLogin = $autoLogin;
     }
 
     public function authenticate() {
         $user = User::model()->findByAttributes(array(
             'username' => $this->username
         ));
 
         if ($user === null) {
             $this->errorCode = self::ERROR_USERNAME_INVALID;
         } else {
             // check Auto or Not
             $password = ($this->autoLogin == false)
                 ? MSecure::password($this->username . $this->password . $user->registered)
                 : $this->password;
 
             if ($user->password !== $password) {
                 $this->errorCode = self::ERROR_PASSWORD_INVALID;
             } else {
                 $this->_id = $user->id;
                 if ($user->lastvisited === NULL) {
                     $lastLogin = new CDbExpression('NOW()');
                 } else {
                     $lastLogin = $user->lastvisited;
                 }
 
                 // RBAC
                 $roles = CJSON::decode($user->role);
                 $auth = Yii::app()->authManager;
                 foreach ($roles as $role) {
                     if (!$auth->isAssigned($role, $this->_id)) {
                         if ($auth->assign($role, $this->_id)) {
                             Yii::app()->authManager->save();
                         }
                     }
                 }
 
                 $this->setState('email', $user->email);
                 $this->setState('lastvisited', $lastLogin);
                 $this->errorCode = self::ERROR_NONE;
             }
         }
         return !$this->errorCode;
     }
 
     public function getId() {
         return $this->_id;
     }        
 }

 

Có một chút khác biệt so với class UserIdentity sinh ra mặc định bởi yiic. Viết lâu quá rồi tôi không nhớ là đã thêm/bỏ những gì. Nhớ chỗ nào ghi ra chỗ đó vậy. Nhìn sơ qua code cũng dễ hiểu, chú ý cái chỗ // RBAC thực hiện chức năng phân quyền
Dòng 4: thêm thuộc tính $autoLogin cho phép đăng nhập tự động theo một đường link định dạng trước mà không thông qua form đăng nhập.
Dòng 14-16: kiểm tra thông tin người dùng trong database thông qua trường username
Dòng 23: có method MSecure::password() chủ yếu để mã hóa pass nhập vào từ form so với password lưu trong database (có thể là MD5 hay SHA +salt gì gì đó). Hoặc bạn chỉ cần md5 chỗ này cũng không sao.
Dòng 36-45: phân quyền trong Yii rất đơn giản, chỉ cần vài dòng này là xong. Vậy mà lúc trước gà quá không hiểu. Để giải thích tí cách xử lý của tôi:
  - Cho đến thời điểm hiện tại tôi chỉ phân quyền dựa trên role (nhóm người dùng – không bàn về task và operation)
  - Tôi thêm 1 trường role (VARCHAR(255)) trong table tc_user để lưu các role của người dùng này với định dạng JSON giống thế này ["admin","member"]. Do đó trước khi lưu bạn hãy CJSON::encode($model->role) lại vì trên form phân quyền tôi dùng dạng checkboxList, 1 user thuộc về nhiều nhóm. Nếu bạn nào mới bắt đầu làm, user chỉ thuộc về 1 nhóm duy nhất có thể dùng dropdownList với các value ["admin"] và ["member"]; và cập nhật lại code trong class UserIdentity chỗ comment là // RBAC cho hợp lý.
    + Tại sao tôi dùng trường role này mà không tạo ra một table khác để lưu. Lý do: yêu cầu phân quyền đơn giản, chạy nhanh, giảm bớt số lượng table không cần thiết (đối với tôi)
    + Nhược điểm: khi thêm mới role không có vấn đề gì; nhưng khi cập nhật hay xóa role bạn sẽ phải cập nhật lại các trường này. Phân tích ban đầu của tôi là role chỉ cho superadmin thay đỗi nên đây ko phải là vấn đề
    + Giá trị lưu trữ là một chuỗi JSON dạng string không phải int. Dùng string nhìn trực quan hơn khi code, dùng 1, 2, 3... khi check quyền phải mò lại nếu nhiều người cùng code (đây chỉ là quan điểm riêng của tôi – hy sinh dung lượng lưu trữ để nhìn code thân thiện hơn)
Dòng 37: vì lưu dạng JSON nên cần phải decode cho nó thành Array để có thể Foreach
Dòng 39: Gọi component AuthManager. Nhanh, gọn, lẹ
Dòng 40: kiểm ra role này đã được gán cho User hay chưa
Dòng 41-42: nếu chưa sẽ gán role cho user này và lưu vào database
Dòng 47-49: bạn muốn lưu vào State hay không thì tùy

 

Xong class UserIdentity. Bạn có thể vào file protected/configs/main.php để thay đỗi quyền mặc định. Đây là quyền mặc định được gán khi bất kỳ người nào truy cập vào site của bạn (quyền guest). Tên table bạn có thể thay đỗi tại đây

'components' => array(
     .....
     'authManager'=>array(
         'class' => 'CDbAuthManager',
         'connectionID' => 'db',
         'itemTable' => 'tc_user_auth', //tc_user_auth_item
         'itemChildTable' => 'tc_user_auth_item_child',
         'assignmentTable' => 'tc_user_auth_assignment',
         'defaultRoles' => array('guest'),
     ),
     .....
 )

 

Trước khi làm chức năng đăng nhập bạn chép code model LoginForm của mình về tham khảo, nó cũng đơn giản, và có 1 vài thay đỗi với bản gốc của yiic. Nên không giải thích gì thêm

class LoginForm extends CFormModel {
 
     public $username;
     public $password;
     public $recovery;
     public $rememberMe;
     public $autoLogin = false;
     private $_identity;
 
     /**
      * Declares the validation rules.
      * The rules state that username and password are required,
      * and password needs to be authenticated.
      */
     public function rules() {
         return array(
             array('username, password', 'required', 'on' => 'login'),
             array('recovery', 'required', 'on' => 'recovery'),
             array('recovery', 'length', 'max' => 100),
             array('rememberMe', 'boolean'),
             array('password', 'authenticate'),
         );
     }
 
     /**
      * Declares attribute labels.
      */
     public function attributeLabels() {
         return array(
             'rememberMe' => Yii::t('default', 'Remember me next time'),
             'username' => Yii::t('default', 'Username'),
             'password' => Yii::t('default', 'Password'),
             'recovery' => Yii::t('default', 'Recovery'),
         );
     }
 
     /**
      * Authenticates the password.
      * This is the 'authenticate' validator as declared in rules().
      */
     public function authenticate($attribute, $params) {
         if (!$this->hasErrors()) {
             $this->_identity = new UserIdentity($this->username, $this->password, $this->autoLogin);
             if (!$this->_identity->authenticate())
                 $this->addError('password', Yii::t('default', 'Incorrect username or password'));
         }
     }
 
     /**
      * Logs in the user using the given username and password in the model.
      * @return boolean whether login is successful
      */
     public function login() {
         if ($this->_identity === null) {
             $this->_identity = new UserIdentity($this->username, $this->password, $this->autoLogin);
             $this->_identity->authenticate();
         }
 
         if ($this->_identity->errorCode === UserIdentity::ERROR_NONE) {
             $duration = $this->rememberMe ? 3600 * 24 * 30 : 0; // 30 days
             Yii::app()->user->login($this->_identity, $duration);
             User::model()->updateByPk($this->_identity->id, array('lastvisited' => new CDbExpression('NOW()')));
             return true;
         }
         else
             return false;
     }
 
 }

 

Trên chức năng đăng nhập trên controller bạn viết thế này. File view của form login chứa gì thì bạn biết rồi

public function actionLogin() {
     if (!Yii::app()->user->isGuest)
         $this->redirect(Yii::app()->homeUrl);
 
     $model = new LoginForm('login');
 
     // if it is ajax validation request
     if (isset($_POST['ajax']) && $_POST['ajax'] === 'login-form') {
         echo CActiveForm::validate($model);
         Yii::app()->end();
     }
 
     // collect user input data
     if (isset($_POST['LoginForm'])) {
         $model->attributes = $_POST['LoginForm'];
 
         if ($model->validate('login') && $model->login()) {
             $this->redirect(Yii::app()->user->returnUrl);
         }
     }
 
     $this->render('login', array('model' => $model));
 }

 

Khi logout thì thế này. Nó gỡ bỏ các quyền được lưu trong phiên đăng nhập trước đó. Có comment khá rõ rồi, tôi không giải thích thêm

public function actionLogout() {
     //obtains all assigned roles for this user id
     $roleAssigned = Yii::app()->authManager->getRoles(Yii::app()->user->id);
 
     if (!empty($roleAssigned)) { //checks that there are assigned roles
         $auth = Yii::app()->authManager; //initializes the authManager
         foreach ($roleAssigned as $n => $role) {
             if ($auth->revoke($n, Yii::app()->user->id)) //remove each assigned role for this user
                 Yii::app()->authManager->save(); //again always save the result
         }
     }
 
     Yii::app()->user->logout();
     $this->redirect(Yii::app()->homeUrl);
 }

 

Quá dài dòng, tới đây mới chạy phân quyền

Bạn có thể yêu cầu xác thực và phân quyền với sự hỗ trợ tự động của Yii Framework trong bất kỳ Controller nào thông qua accessRules()

public function accessRules() {
     return array(
         array('allow',
             'actions' => array('index'),
             'users' => array('*'),
         ),
         array('allow',
             'actions' => array('view'),
             'roles' => array('member')
         ),
         array('allow',
             'actions' => array('create', 'update', 'delete'),
             'roles' => array('admin'),
         ),
         array('deny', // deny anything else
             'users' => array('*'),
         ),
     );
 }

Nhìn vào những dòng trên bạn sẽ thấy:
- Tất cả mọi người đều có thể vào index
- Member mới được quyền view
- Admin thì được create, update, delete. Ở đây không ghi view, index cho admin; nó đã được hiểu ngầm bao gồm luôn 2 quyền này. Vì mối quan hệ admin/member được quy định trong bảng tc_user_auth_item_child. Yii nó giúp bạn tự xử phần này.
 
Tuy nhiên nếu bạn không muốn check quyền trên accessRules, có thể check tại bất kỳ đâu như sau:

if (Yii::app()->user->checkAccess(‘roleName’)) {
     // muốn làm gì thì làm
 }

Như vậy chỗ $this->redirect(Yii::app()->user->returnUrl) của method login trên controller bạn có thể kiểm tra quyền và redirect tới đâu tùy thích :D

 

Viết dài dòng nhưng gom lại như sau:

- Bạn cần model User với các trường: id, username, password để lưu thông tin người dùng

- Một form chứa thông tin đăng nhập, được POST lên actionLogin trong controller

- Action này sẽ gọi đến model LoginForm để kiểm tra thông tin người dùng. Đồng thời gọi đến method login() của model này

- Method login() khởi tạo object UserIdentity nếu nó chưa tồn tại và gọi tiếp method authenticate() trong class UserIdentity

- Class UserIdentity chính là nơi kiểm tra username tồn tại, password trùng khớp hay không. Phân quyền cũng được thực hiện tại đây.

- Kết quả trả về nếu ko có lỗi, bạn có thể redirect hay làm gì tùy yêu cầu dự án của bạn.

 Tại sao phải lòng vòng như vậy cho lu bu? Lý do: theo mô hình MVC, tận dụng các chức năng hỗ trợ sẵn bởi Yii mà không phải viết thêm

 

Kết thúc phần phân quyền tại đây.
Bạn có thể mở rông thêm, như dùng bizrule, phân quyền sâu hơn với task, operation

Tham khảo theo link sau: http://www.yiiframework.com/doc/guide/1.1/en/topics.auth

Bài đăng khác

HỖ TRỢ TRỰC TUYẾN

Mr. Lĩnh

0939.898.458

linhnp@panda.com.vn

Mr. Lai

0939.38.77.39

lainp@panda.com.vn

Processing...