Yii修行之路 - Security 安全

簡述

在程序開發過程當中,每每都不能忽視安全問題,不管你的框架有多麼完美,都會有破綻,因此完善本身的系統,從程序開發的安全角度去思考問題,把一切潛在的危機扼殺在搖籃中。php

認證(Authentication)

認證是鑑定用戶身份的過程。web

它一般使用一個標識符 (如用戶名或電子郵件地址)和一個加密令牌(好比密碼或者存取令牌)來 鑑別用戶身份。算法

認證是登陸功能的基礎。
Yii提供了一個認證框架,它鏈接了不一樣的組件以支持登陸。欲使用這個框架, 你主要須要作如下工做:sql

  • 設置用戶組件 yiiwebUser ;數據庫

  • 建立一個類實現 yiiwebIdentityInterface 接口。數組

配置 yiiwebUser

用戶組件 yiiwebUser 用來管理用戶的認證狀態。瀏覽器

這須要你 指定一個含有實際認證邏輯的認證類 yiiwebUser::identityClass。緩存

在如下web應用的配置項中,將用戶用戶組件 yiiwebUser 的 認證類 yiiwebUser::identityClass 配置成 模型類 appmodelsUser。安全

return [
    'components' => [
        'user' => [
            'identityClass' => 'app\models\User',
        ],
    ],
];

認證接口 yiiwebIdentityInterface 的實現

認證類 yiiwebUser::identityClass 必須實現包含如下方法的 認證接口 yiiwebIdentityInterface:服務器

  • yiiwebIdentityInterface::findIdentity():根據指定的用戶ID查找 認證模型類的實例,當你須要使用session來維持登陸狀態的時候會用到這個方法。

  • yiiwebIdentityInterface::findIdentityByAccessToken():根據指定的存取令牌查找 認證模型類的實例,該方法用於 經過單個加密令牌認證用戶的時候(好比無狀態的RESTful應用)。

  • yiiwebIdentityInterface::getId():獲取該認證明例表示的用戶的ID。

  • yiiwebIdentityInterface::getAuthKey():獲取基於 cookie 登陸時使用的認證密鑰。 認證密鑰儲存在 cookie 裏而且未來會與服務端的版本進行比較以確保 cookie的有效性。

  • yiiwebIdentityInterface::validateAuthKey() :是基於 cookie 登陸密鑰的 驗證的邏輯的實現。

用不到的方法能夠空着,例如,你的項目只是一個 無狀態的 RESTful 應用,只需實現 yiiwebIdentityInterface::findIdentityByAccessToken() 和 yiiwebIdentityInterface::getId() ,其餘的方法的函數體留空便可。

下面的例子是一個經過結合了 user 數據表的 AR 模型 Active Record 實現的一個認證類 yiiwebUser::identityClass。

<?php

use yii\db\ActiveRecord;
use yii\web\IdentityInterface;

class User extends ActiveRecord implements IdentityInterface{
    public static function tableName()
    {
        return 'user';
    }

    /**
     * 根據給到的ID查詢身份。
     *
     * @param string|integer $id 被查詢的ID
     * @return IdentityInterface|null 經過ID匹配到的身份對象
     */
    public static function findIdentity($id)
    {
        return static::findOne($id);
    }

    /**
     * 根據 token 查詢身份。
     *
     * @param string $token 被查詢的 token
     * @return IdentityInterface|null 經過 token 獲得的身份對象
     */
    public static function findIdentityByAccessToken($token, $type = null)
    {
        return static::findOne(['access_token' => $token]);
    }

    /**
     * @return int|string 當前用戶ID
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return string 當前用戶的(cookie)認證密鑰
     */
    public function getAuthKey()
    {
        return $this->auth_key;
    }

    /**
     * @param string $authKey
     * @return boolean if auth key is valid for current user
     */
    public function validateAuthKey($authKey)
    {
        return $this->getAuthKey() === $authKey;
    }
}

如上所述,若是你的應用利用 cookie 登陸, 你只須要實現 getAuthKey() 和 validateAuthKey() 方法。這樣的話,你可使用下面的代碼在 user 表中生成和存儲每一個用戶的認證密鑰。

class User extends ActiveRecord implements IdentityInterface{
    ......

    public function beforeSave($insert)
    {
        if (parent::beforeSave($insert)) {
            if ($this->isNewRecord) {
                $this->auth_key = \Yii::$app->security->generateRandomString();
            }
            return true;
        }
        return false;
    }
}

注意:不要混淆 user 認證類和用戶組件 yiiwebUser。前者是實現 認證邏輯的類,一般用關聯了 持久性存儲的用戶信息的AR模型 Active Record 實現。後者是負責管理用戶認證狀態的 應用組件。

使用用戶組件 yiiwebUser

在 user 應用組件方面,你主要用到 yiiwebUser 。

你可使用表達式 Yii::$app->user->identity 檢測當前用戶身份。它返回 一個表示當前登陸用戶的認證類 yiiwebUser::identityClass 的實例, 未認證用戶(遊客)則返回 null。

下面的代碼展現瞭如何從 yiiwebUser 獲取其餘認證相關信息:

// 當前用戶的身份實例。未認證用戶則爲 Null 。
$identity = Yii::$app->user->identity;

// 當前用戶的ID。 未認證用戶則爲 Null 。
$id = Yii::$app->user->id;

// 判斷當前用戶是不是遊客(未認證的)
$isGuest = Yii::$app->user->isGuest;

你可使用下面的代碼登陸用戶:

// 使用指定用戶名獲取用戶身份實例。
// 請注意,若是須要的話您可能要檢驗密碼
$identity = User::findOne(['username' => $username]);

// 登陸用戶
Yii::$app->user->login($identity);
yii\web\User::login() 方法將當前用戶的身份登記到 yii\web\User。

若是 session 設置爲 yiiwebUser::enableSession,則使用 session 記錄用戶身份,用戶的 認證狀態將在整個會話中得以維持。

若是開啓自動登陸 yiiwebUser::enableAutoLogin 則基於 cookie 登陸(如:記住登陸狀態),它將使用 cookie 保存用戶身份,這樣 只要 cookie 有效就能夠恢復登陸狀態。

爲了使用 cookie 登陸,你須要在應用配置文件中將 yii\web\User::enableAutoLogin 設爲 true。
你還須要在 yii\web\User::login() 方法中 傳遞有效期(記住登陸狀態的時長)參數。

註銷用戶:

Yii::$app->user->logout();

請注意,啓用 session 時註銷用戶纔有意義。該方法將從內存和 session 中 同時清理用戶認證狀態。

默認狀況下,它還會註銷全部的 用戶會話數據。

若是你但願保留這些會話數據,能夠換成 Yii::$app->user->logout(false) 。

認證事件

yiiwebUser 類在登陸和註銷流程引起一些事件。

一、yii\web\User::EVENT_BEFORE_LOGIN:在登陸 yii\web\User::login() 時引起。 
    若是事件句柄將事件對象的 yii\web\UserEvent::isValid 屬性設爲 false, 登陸流程將會被取消。
    
    二、yii\web\User::EVENT_AFTER_LOGIN:登陸成功後引起。
    
    三、yii\web\User::EVENT_BEFORE_LOGOUT:註銷 yii\web\User::logout() 前引起。 
    若是事件句柄將事件對象的 yii\web\UserEvent::isValid 屬性設爲 false, 註銷流程將會被取消。
    
    四、yii\web\User::EVENT_AFTER_LOGOUT:成功註銷後引起。

你能夠經過響應這些事件來實現一些相似登陸統計、在線人數統計的功能。例如, 在登陸後 yiiwebUser::EVENT_AFTER_LOGIN 的處理程序,你能夠將用戶的登陸時間和IP記錄到 user 表中。

受權(Authorization)

受權是指驗證用戶是否容許作某件事的過程。

Yii提供兩種受權方法: 存取控制過濾器(ACF)和基於角色的存取控制(RBAC)。

存取控制過濾器

存取控制過濾器(ACF)是一種經過 yiifiltersAccessControl 類來實現的簡單受權方法, 很是適用於僅須要簡單的存取控制的應用。

正如其名稱所指,ACF 是一個種行動(action)過濾器 filter,可在控制器或者模塊中使用。當一個用戶請求一個 action 時, ACF會檢查 yiifiltersAccessControl::rules 列表,判斷該用戶是否容許執 行所請求的action。(譯者注: action 在本文中視狀況翻譯爲行動、操做、方法等)

下述代碼展現如何在 site 控制器中使用 ACF:

use yii\web\Controller;
use yii\filters\AccessControl;

class SiteController extends Controller{
    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::className(),
                'only' => ['login', 'logout', 'signup'],
                'rules' => [
                    [
                        'allow' => true,
                        'actions' => ['login', 'signup'],
                        'roles' => ['?'],
                    ],
                    [
                        'allow' => true,
                        'actions' => ['logout'],
                        'roles' => ['@'],
                    ],
                ],
            ],
        ];
    }
    // ...
}

上面的代碼中 ACF 以行爲 (behavior) 的形式附加到 site 控制器。 這就是很典型的使用行動過濾器的方法。

only 選項指明 ACF 應當只 對 login, logout 和 signup 方法起做用。全部其它的 site 控制器中的方法不受存取控制的限制。

rules 選項列出了 yiifiltersAccessRule,解讀以下:

  • 容許全部訪客(還未經認證的用戶)執行 login 和 signup 操做。 roles 選項包含的問號 ? 是一個特殊的標識,表明」訪客用戶」。

  • 容許已認證用戶執行 logout 操做。@是另外一個特殊標識, 表明」已認證用戶」。

ACF 自上向下逐一檢查存取規則,直到找到一個與當前 欲執行的操做相符的規則。 而後該匹配規則中的 allow 選項的值用於斷定該用戶是否得到受權。若是沒有找到匹配的規則, 意味着該用戶沒有得到受權。(譯者注: only 中沒有列出的操做,將無條件得到受權)

當 ACF 斷定一個用戶沒有得到執行當前操做的受權時,它的默認處理是:

  • 若是該用戶是訪客,將調用 yiiwebUser::loginRequired() 將用戶的瀏覽器重定向到登陸頁面。

  • 若是該用戶是已認證用戶,將拋出一個 yiiwebForbiddenHttpException 異常。

你能夠經過配置 yiifiltersAccessControl::denyCallback 屬性定製該行爲:

[
    'class' => AccessControl::className(),
    ...
    'denyCallback' => function ($rule, $action) {
        throw new \Exception('You are not allowed to access this page');
    }
]

yiifiltersAccessRule 支持不少的選項。

下列是所支持選項的總覽:你能夠派生 yiifiltersAccessRule 來建立自定義的存取規則類。

* yii\filters\AccessRule::allow: 指定該規則是 "容許" 仍是 "拒絕" 。(譯者注:true是容許,false是拒絕)

    * yii\filters\AccessRule::actions:指定該規則用於匹配哪些操做。 它的值應該是操做方法的ID數組。匹配比較是大小寫敏感的。若是該選項爲空,或者不使用該選項, 意味着當前規則適用於全部的操做。

    一、yii\filters\AccessRule::controllers:指定該規則用於匹配哪些控制器。 它的值應爲控制器ID數組。匹配比較是大小寫敏感的。若是該選項爲空,或者不使用該選項, 則意味着當前規則適用於全部的操做。(譯者注:這個選項通常是在控制器的自定義父類中使用纔有意義)

    二、yii\filters\AccessRule::roles:指定該規則用於匹配哪些用戶角色。 系統自帶兩個特殊的角色,經過 yii\web\User::isGuest 來判斷:

        * ?: 用於匹配訪客用戶 (未經認證)
        * @: 用於匹配已認證用戶

    三、使用其餘角色名時,將觸發調用 yii\web\User::can(),這時要求 RBAC 的支持 (在下一節中闡述)。 若是該選項爲空或者不使用該選項,意味着該規則適用於全部角色。

    四、yii\filters\AccessRule::ips:指定該規則用於匹配哪些 yii\web\Request::userIP 。 IP 地址可在其末尾包含通配符 * 以匹配一批前綴相同的IP地址。 例如,192.168.* 匹配全部 192.168. 段的IP地址。 若是該選項爲空或者不使用該選項,意味着該規則適用於全部角色。

    五、yii\filters\AccessRule::verbs:指定該規則用於匹配哪一種請求方法(例如GET,POST)。 這裏的匹配大小寫不敏感。

    六、yii\filters\AccessRule::matchCallback:指定一個PHP回調函數用於 斷定該規則是否知足條件。(譯者注:此處的回調函數是匿名函數)

    七、yii\filters\AccessRule::denyCallback: 指定一個PHP回調函數, 當這個規則不知足條件時該函數會被調用。(譯者注:此處的回調函數是匿名函數)

如下例子展現瞭如何使用 matchCallback 選項, 可以使你設計任意的訪問權限檢查邏輯:

use yii\filters\AccessControl;

class SiteController extends Controller{
    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::className(),
                'only' => ['special-callback'],
                'rules' => [
                    [
                        'actions' => ['special-callback'],
                        'allow' => true,
                        'matchCallback' => function ($rule, $action) {
                            return date('d-m') === '31-10';
                        }
                    ],
                ],
            ],
        ];
    }

    // 匹配的回調函數被調用了!這個頁面只有每一年的10月31號能訪問(譯者注:原文在這裏說該方法是回調函數不確切,讀者不要和 `matchCallback` 的值即匿名的回調函數混淆理解)。
    public function actionSpecialCallback()
    {
        return $this->render('happy-halloween');
    }
}

基於角色的存取控制 (RBAC)

基於角色的存取控制 (RBAC) 提供了一個簡單而強大的集中式存取控制機制。

Yii 實現了通用的分層的 RBAC,遵循的模型是 NIST RBAC model. 它經過 yiirbacManagerInterface application component 提供 RBAC 功能。
使用 RBAC 涉及到兩部分工做。第一部分是創建受權數據, 而第二部分是使用這些受權數據在須要的地方執行檢查。
爲方便後面的講述,這裏先介紹一些 RBAC 的基本概念。

基本概念

角色是 權限 的集合 (例如:建貼、改貼)。一個角色 能夠指派給一個或者多個用戶。要檢查某用戶是否有一個特定的權限, 系統會檢查該包含該權限的角色是否指派給了該用戶。

能夠用一個規則 rule 與一個角色或者權限關聯。一個規則用一段代碼表明, 規則的執行是在檢查一個用戶是否知足這個角色或者權限時進行的。

例如,"改帖" 的權限 可使用一個檢查該用戶是不是帖子的建立者的規則。權限檢查中,若是該用戶 不是帖子建立者,那麼他(她)將被認爲不具備 "改帖"的權限。

角色和權限均可以按層次組織。特定狀況下,一個角色可能由其餘角色或權限構成, 而權限又由其餘的權限構成。

Yii 實現了所謂的 局部順序 的層次結構,包含更多的特定的 樹 的層次。 一個角色能夠包含一個權限,反之則不行。(譯者注:可理解爲角色在上方,權限在下方,從上到下若是碰到權限那麼再往下不能出現角色)

配置 RBAC

在開始定義受權數據和執行存取檢查以前,須要先配置應用組件 yiibaseApplication::authManager 。

Yii 提供了兩套受權管理器: yiirbacPhpManager 和 yiirbacDbManager。前者使用 PHP 腳本存放受權數據, 然後者使用數據庫存放受權數據。 若是你的應用不要求大量的動態角色和權限管理, 你能夠考慮使用前者。

使用 PhpManager
如下代碼展現使用 yiirbacPhpManager 時如何在應用配置文件中配置 authManager:

return [
    // ...
    'components' => [
        'authManager' => [
            'class' => 'yii\rbac\PhpManager',
        ],
        // ...
    ],
];

如今能夠經過 Yii::$app->authManager 訪問 authManager 。
yiirbacPhpManager 默認將 RBAC 數據保存在 @app/rbac 目錄下的文件中。 若是權限層次數據在運行時會被修改,需確保WEB服務器進程對該目錄和其中的文件有寫權限。

使用 DbManager

如下代碼展現使用 yiirbacDbManager 時如何在應用配置文件中配置 authManager:
return [

// ...
'components' => [
    'authManager' => [
        'class' => 'yii\rbac\DbManager',
    ],
    // ...
],

];
DbManager 使用4個數據庫表存放它的數據:

  • yiirbacDbManager::$itemTable: 該表存放受權條目(譯者注:即角色和權限)。默認表名爲 "auth_item" 。

  • yiirbacDbManager::$itemChildTable: 該表存放受權條目的層次關係。默認表名爲 "auth_item_child"。

  • yiirbacDbManager::$assignmentTable: 該表存放受權條目對用戶的指派狀況。默認表名爲 "auth_assignment"。

  • yiirbacDbManager::$ruleTable: 該表存放規則。默認表名爲 "auth_rule"。

繼續以前,你須要在數據庫中建立這些表。

你可使用存放在 @yii/rbac/migrations 目錄中的數據庫遷移文件來作這件事(譯者注:根據本人經驗,最好是將受權數據初始化命令也寫到這個 RBAC 數據庫遷移文件中):

yii migrate --migrationPath=@yii/rbac/migrations

如今能夠經過 Yii::$app->authManager 訪問 authManager 。

創建受權數據
全部受權數據相關的任務以下:

  • 定義角色和權限;

  • 創建角色和權限的關係;

  • 定義規則;

  • 將規則與角色和權限做關聯;

  • 指派角色給用戶。

根據受權的彈性需求,上述任務可用不一樣的方法完成。

若是你的權限層次結構不會發生改變,並且你的用戶數是恆定的,你能夠經過 authManager 提供的 API 建立一個 控制檯命令 一次性初始化受權數據:

<?php
namespace app\commands;

use Yii;
use yii\console\Controller;

class RbacController extends Controller{
    public function actionInit()
    {
        $auth = Yii::$app->authManager;

        // 添加 "createPost" 權限
        $createPost = $auth->createPermission('createPost');
        $createPost->description = 'Create a post';
        $auth->add($createPost);

        // 添加 "updatePost" 權限
        $updatePost = $auth->createPermission('updatePost');
        $updatePost->description = 'Update post';
        $auth->add($updatePost);

        // 添加 "author" 角色並賦予 "createPost" 權限
        $author = $auth->createRole('author');
        $auth->add($author);
        $auth->addChild($author, $createPost);

        // 添加 "admin" 角色並賦予 "updatePost" 
        // 和 "author" 權限
        $admin = $auth->createRole('admin');
        $auth->add($admin);
        $auth->addChild($admin, $updatePost);
        $auth->addChild($admin, $author);

        // 爲用戶指派角色。其中 1 和 2 是由 IdentityInterface::getId() 返回的id (譯者注:user表的id)
        // 一般在你的 User 模型中實現這個函數。
        $auth->assign($author, 2);
        $auth->assign($admin, 1);
    }
}

在用 yii rbac/init 執行了這個命令後,咱們將獲得下圖所示的層次結構:

做者可建立新貼,管理員可編輯帖子以及全部做者可作的事情。

若是你的應用容許用戶註冊,你須要在註冊時給新用戶指派一次角色。

例如, 在高級項目模板中,要讓全部註冊用戶成爲做者,你須要以下例所示修改 frontendmodelsSignupForm::signup() 方法:

public function signup(){
    if ($this->validate()) {
        $user = new User();
        $user->username = $this->username;
        $user->email = $this->email;
        $user->setPassword($this->password);
        $user->generateAuthKey();
        $user->save(false);

        // 要添加如下三行代碼:
        $auth = Yii::$app->authManager;
        $authorRole = $auth->getRole('author');
        $auth->assign($authorRole, $user->getId());

        return $user;
    }

    return null;
}

對於有動態更改受權數據的複雜存取控制需求的,你可能須要使用 authManager 提供的 API 的開發用戶界面(例如:管理面板)。

使用規則 (Rules)

如前所述,規則給角色和權限增長額外的約束條件。規則是 yiirbacRule 的派生類。 它須要實現 yiirbacRule::execute() 方法。

在以前咱們建立的層次結構中,做者不能編輯本身的帖子,咱們來修正這個問題。 首先咱們須要一個規則來認證當前用戶是帖子的做者:

namespace app\rbac;

use yii\rbac\Rule;

/**
 * 檢查 authorID 是否和經過參數傳進來的 user 參數相符
 */class AuthorRule extends Rule{
    public $name = 'isAuthor';

    /**
     * @param string|integer $user 用戶 ID.
     * @param Item $item 該規則相關的角色或者權限
     * @param array $params 傳給 ManagerInterface::checkAccess() 的參數
     * @return boolean 表明該規則相關的角色或者權限是否被容許
     */
    public function execute($user, $item, $params)
    {
        return isset($params['post']) ? $params['post']->createdBy == $user : false;
    }
}

上述規則檢查 post 是不是 $user 建立的。咱們還要在以前的命令中 建立一個特別的權限 updateOwnPost :

$auth = Yii::$app->authManager;

// 添加規則
$rule = new \app\rbac\AuthorRule;$auth->add($rule);

// 添加 "updateOwnPost" 權限並與規則關聯
$updateOwnPost = $auth->createPermission('updateOwnPost');
$updateOwnPost->description = 'Update own post';
$updateOwnPost->ruleName = $rule->name;
$auth->add($updateOwnPost);

// "updateOwnPost" 權限將由 "updatePost" 權限使用
$auth->addChild($updateOwnPost, $updatePost);

// 容許 "author" 更新本身的帖子
$auth->addChild($author, $updateOwnPost);

使用默認角色

所謂默認角色就是 隱式 地指派給 全部 用戶的角色。不須要調用 yiirbacManagerInterface::assign() 方法作顯示指派,而且受權數據中不包含指派信息。

默認角色一般與一個規則關聯,用以檢查該角色是否符合被檢查的用戶。

默認角色經常用於已經確立了一些角色的指派關係的應用(譯者注:指派關係指的是應用業務邏輯層面, 並不是指受權數據的結構)。好比,一個應用的 user 表中有一個 group 字段,表明用戶屬於哪一個特權組。 若是每一個特權組能夠映射到 RBAC 的角色,你就能夠採用默認角色自動地爲每一個用戶指派一個 RBAC 角色。 讓咱們用一個例子展現如何作到這一點。

假設在 user 表中,你有一個 group 字段,用 1 表明管理員組,用 2 表示做者組。 你規劃兩個 RBAC 角色 admin 和 author 分別對應這兩個組的權限。 你能夠這樣設置 RBAC 數據,

namespace app\rbac;

use Yii;
use yii\rbac\Rule;

/**
 * 檢查是否匹配用戶的組
 */class UserGroupRule extends Rule{
    public $name = 'userGroup';

    public function execute($user, $item, $params)
    {
        if (!Yii::$app->user->isGuest) {
            $group = Yii::$app->user->identity->group;
            if ($item->name === 'admin') {
                return $group == 1;
            } elseif ($item->name === 'author') {
                return $group == 1 || $group == 2;
            }
        }
        return false;
    }
}

$auth = Yii::$app->authManager;

$rule = new \app\rbac\UserGroupRule;
$auth->add($rule);

$author = $auth->createRole('author');
$author->ruleName = $rule->name;
$auth->add($author);
// ... 添加$author角色的子項部分代碼 ... (譯者注:省略部分參照以前的控制檯命令)

$admin = $auth->createRole('admin');
$admin->ruleName = $rule->name;
$auth->add($admin);
$auth->addChild($admin, $author);
// ... 添加$admin角色的子項部分代碼 ... (譯者注:省略部分參照以前的控制檯命令)

注意,在上述代碼中,由於 "author" 做爲 "admin" 的子角色,當你實現這個規則的 execute() 方法時, 你也須要聽從這個層次結構。

這就是爲什麼當角色名爲 "author" 的狀況下(譯者注:$item->name就是角色名), execute() 方法在組爲 1 或者 2 時均要返回 true (意思是用戶屬於 "admin" 或者 "author" 組 )。

接下來,在配置 authManager 時指定 yiirbacBaseManager::$defaultRoles 選項(譯者注:在應用配置文件中的組件部分配置):

return [
    // ...
    'components' => [
        'authManager' => [
            'class' => 'yii\rbac\PhpManager',
            'defaultRoles' => ['admin', 'author'],
        ],
        // ...
    ],
];
如今若是你執行一個存取權限檢查, 斷定該規則時, admin 和 author 兩個角色都將會檢查。若是規則返回 true ,意思是角色符合當前用戶。基於上述規則 的實現,意味着若是某用戶的 group 值爲 1 , admin 角色將賦予該用戶, 若是 group 值是 2 則將賦予author 角色。

處理密碼(Passwords)

好的安全策略對任何應用的健康和成功極其重要。

不幸的是,許多開發者在遇到安全問題時,由於認識不夠或者實現起來比較麻煩,都不是很注意細節。

爲了讓你的 Yii 應用程序儘量的安全, Yii 囊括了一些卓越並簡單易用的安全特性。

密碼的哈希與驗證

大部分開發者知道密碼不能以明文形式存儲,可是許多開發者仍認爲使用 md5 或者 sha1 來哈希化密碼是安全的。

一度,使用上述的哈希算法是足夠安全的,可是,現代硬件的發展使得短期內暴力破解上述算法生成的哈希串成爲可能。

爲了即便在最糟糕的狀況下(你的應用程序被破解了)也能給用戶密碼提供加強的安全性,你須要使用一個可以對抗暴力破解攻擊的哈希算法,目前最好的選擇是 bcrypt。

在 PHP 中,你能夠經過 crypt 函數 生成 bcrypt 哈希。Yii 提供了兩個幫助函數以讓使用crypt 來進行安全的哈希密碼生成和驗證更加容易。

當一個用戶爲第一次使用,提供了一個密碼時(好比:註冊時),密碼就須要被哈希化。
$hash = Yii::$app->getSecurity()->generatePasswordHash($password);

哈希串能夠被關聯到對應的模型屬性,這樣,它能夠被存儲到數據庫中以備未來使用。

當一個用戶嘗試登陸時,表單提交的密碼須要使用以前的存儲的哈希串來驗證:

if (Yii::$app->getSecurity()->validatePassword($password, $hash)) {
    // all good, logging user in
} else {
    // wrong password
}

生成僞隨機數

僞隨機數據在許多場景下都很是有用。

好比當經過郵件重置密碼時,你須要生成一個令牌,將其保存到數據庫中,並經過郵件發送到終端用戶那裏以讓其證實其對某個帳號的全部權。

這個令牌的惟一性和難猜解性很是重要,不然,就存在攻擊者猜解令牌,並重置用戶的密碼的可能性。

Yii 安全助手使得生成僞隨機數據很是簡單:

$key = Yii::$app->getSecurity()->generateRandomString();

注意,你須要安裝有 openssl 擴展,以生成密碼的安全隨機數據。

加密與解密

Yii 提供了方便的幫助函數來讓你用一個安全祕鑰來加密解密數據。數據經過加密函數進行傳輸,這樣只有擁有安全祕鑰的人才能解密。

好比,咱們須要存儲一些信息到咱們的數據庫中,可是,咱們須要保證只有擁有安全祕鑰的人才能看到它(即便應用的數據庫泄露)

// $data and $secretKey are obtained from the form
$encryptedData = Yii::$app->getSecurity()->encryptByPassword($data, $secretKey);

// store $encryptedData to database

隨後,當用戶須要讀取數據時:

// $secretKey is obtained from user input, $encryptedData is from the database
$data = Yii::$app->getSecurity()->decryptByPassword($encryptedData, $secretKey);

校驗數據完整性

有時,你須要驗證你的數據沒有第三方篡改或者使用某種方式破壞了。Yii 經過兩個幫助函數,提供了一個簡單的方式來進行數據的完整性校驗。

首先,將由安全祕鑰和數據生成的哈希串前綴到數據上。

// $secretKey our application or user secret, $genuineData obtained from a reliable source
$data = Yii::$app->getSecurity()->hashData($genuineData, $secretKey);

驗證數據完整性是否被破壞了。

// $secretKey our application or user secret, $data obtained from an unreliable source
$data = Yii::$app->getSecurity()->validateData($data, $secretKey);

你一樣能夠給控制器或者 action 設置它的 enableCsrfValidation 屬性來單獨禁用 CSRF 驗證。

namespace app\controllers;

use yii\web\Controller;

class SiteController extends Controller{
    public $enableCsrfValidation = false;

    public function actionIndex()
    {
        // CSRF validation will not be applied to this and other actions
    }

}

爲了給某個定製的 action 關閉 CSRF 驗證,你能夠:

namespace app\controllers;

use yii\web\Controller;

class SiteController extends Controller{
    public function beforeAction($action)
    {
        // ...set `$this->enableCsrfValidation` here based on some conditions...
        // call parent method that will check CSRF if such property is true.
        return parent::beforeAction($action);
    }
}

一些最佳安全實踐(Best Practices)

基本準則不管是開發何種應用程序,咱們都有兩條基本的安全準則:

  1. 過濾輸入

  2. 轉義輸出

過濾輸入

過濾輸入的意思是,用戶輸入不該該認爲是安全的,你須要老是驗證你得到的輸入值是在容許範圍內。

好比,咱們假設 sorting 只能指定爲 title, created_at 和 status 三個值,而後,這個值是由用戶輸入提供的,那麼,最好在咱們接收參數的時候,檢查一下這個值是不是指定的範圍。

對於基本的 PHP 而言,上述作法相似以下:

$sortBy = $_GET['sort'];
if (!in_array($sortBy, ['title', 'created_at', 'status'])) {
    throw new Exception('Invalid sort value.');
}

在 Yii 中,很大可能性,你會使用 表單校驗器 來執行相似的檢查。

轉義輸出

轉義輸出的意思是,根據咱們使用數據的上下文環境,數據須要被轉義。

好比:在 HTML 上下文,你須要轉義 <,> 之類的特殊字符。在 JavaScript 或者 SQL 中,也有其餘的特殊含義的字符串須要被轉義。

因爲手動的給所用的輸出轉義容易出錯,Yii 提供了大量的工具來在不一樣的上下文執行轉義。

避免 SQL 注入

SQL 注入發生在查詢語句是由鏈接未轉義的字符串生成的場景,好比:

$username = $_GET['username'];
$sql = "SELECT * FROM user WHERE username = '$username'";

除了提供正確的用戶名外,攻擊者能夠給你的應用程序輸入相似 '; DROP TABLE user; --` 的語句。 這將會致使生成以下的 SQL :

SELECT * FROM user WHERE username = ''; DROP TABLE user; --'

這是一個合法的查詢語句,並將會執行以空的用戶名搜索用戶操做,而後,刪除 user 表。這極有可能致使網站出差,數據丟失。(你是否進行了規律的數據備份?)

在 Yii 中,大部分的數據查詢是經過 Active Record 進行的,而其是徹底使用 PDO 預處理語句執行 SQL 查詢的。在預處理語句中,上述示例中,構造 SQL 查詢的場景是不可能發生的。

有時,你仍須要使用 raw queries 或者 query builder。在這種狀況下,你應該使用安全的方式傳遞參數。若是數據是提供給表列的值,最好使用預處理語句:

// query builder
$userIDs = (new Query())
    ->select('id')
    ->from('user')
    ->where('status=:status', [':status' => $status])
    ->all();

// DAO
$userIDs = $connection
    ->createCommand('SELECT id FROM user where status=:status')
    ->bindValues([':status' => $status])
    ->queryColumn();

若是數據是用於指定列的名字,或者表的名字,最好的方式是隻容許預約義的枚舉值。

function actionList($orderBy = null){
    if (!in_array($orderBy, ['name', 'status'])) {
        throw new BadRequestHttpException('Only name and status are allowed to order by.')
    }

    // ...
}

若是上述方法不行,表名或者列名應該被轉義。 Yii 針對這種轉義提供了一個特殊的語法,這樣能夠在全部支持的數據庫都使用一套方案。

$sql = "SELECT COUNT($column) FROM {{table}}";
$rowCount = $connection->createCommand($sql)->queryScalar();

防止 XSS 攻擊

XSS 或者跨站腳本發生在輸出 HTML 到瀏覽器時,輸出內容沒有正確的轉義。

例如,若是用戶能夠輸入其名稱,那麼他輸入<script>alert('Hello!');</script> 而非其名字 Alexander,全部輸出沒有轉義直接輸出用戶名的頁面都會執行 JavaScript 代碼 alert('Hello!');,這會致使瀏覽器頁面上出現一個警告彈出框。

就具體的站點而言,除了這種無心義的警告輸出外,這樣的腳本能夠以你的名義發送一些消息到後臺,甚至執行一些銀行交易行爲。

避免 XSS 攻擊在 Yii 中很是簡單,有以下兩種通常狀況:

  1. 你但願數據以純文本輸出。

  2. 你但願數據以 HTML 形式輸出。

若是你須要的是純文本,你能夠以下簡單的轉義:

<?= \yii\helpers\Html::encode($username) ?>

若是是 HTML ,咱們能夠用 HtmlPurifier 幫助類來執行:

<?= \yii\helpers\HtmlPurifier::process($description) ?>

注意: HtmlPurifier 幫助類的處理過程較爲費時,建議增長緩存:

防止 CSRF 攻擊
CSRF 是跨站請求僞造的縮寫。這個攻擊思想源自許多應用程序假設來自用戶的瀏覽器請求是由用戶本身產生的,而事實並不是如此。

好比說:an.example.com 站點有一個 /logout URL,當以 GET 請求訪問時,登出用戶。若是它是由用戶本身操做的,那麼一切都沒有問題。可是,有一天壞人在一個用戶常常訪問的論壇發了一個

<img src="/docs/guide/2.0/http://an.example.com/logout">

內容的帖子。瀏覽器沒法辨別請求一個圖片仍是一個頁面,因此,當用戶打開含有上述標籤的頁面時,他將會從 an.example.com 登出。

上面就是最原始的思想。有人可能會說,登出用戶也不是什麼嚴重問題,然而,咱們發送一些 POST 數據其實也不是很麻煩的事情。

爲了不 CSRF 攻擊,你老是須要:

  1. 遵循 HTTP 準則,好比 GET 不該該改變應用的狀態。

  2. 保證 Yii CSRF 保護開啓。

防止文件暴露

默認的服務器 webroot 目錄指向包含有 index.php 的 web 目錄。

在共享託管環境下,這樣是不可能的,這樣致使了全部的代碼,配置,日誌都在webroot目錄。

若是是這樣,別忘了拒絕除了 web 目錄之外的目錄的訪問權限。若是無法這樣作,考慮將你的應用程序託管在其餘地方。

在生產環境關閉調試信息和工具

在調試模式下, Yii 展現了大量的錯誤信息,這樣是對開發有用的。一樣,這些調試信息對於攻擊者而言也是方便其用於破解數據結構,配置值,以及你的部分代碼。

永遠不要在生產模式下將你的 index.php 中的 YII_DEBUG 設置爲 true。

你一樣也不該該在生產模式下開啓 Gii。

它能夠被用於獲取數據結構信息,代碼,以及簡單的用 Gii 生成的代碼覆蓋你的代碼。

調試工具欄一樣也應該避免在生產環境出現,除非很是有必要。它將會暴露全部的應用和配置的詳情信息。若是你肯定須要,反覆確認其訪問權限限定在你本身的 IP。

相關文章
相關標籤/搜索