6

简述

在程序开发过程中,往往都不能忽视安全问题,无论你的框架有多么完美,都会有破绽,所以完善自己的系统,从程序开发的安全角度去思考问题,把一切潜在的危机扼杀在摇篮中。

认证(Authentication)

认证是鉴定用户身份的过程。

它通常使用一个标识符 (如用户名或电子邮件地址)和一个加密令牌(比如密码或者存取令牌)来 鉴别用户身份。

认证是登录功能的基础。
Yii提供了一个认证框架,它连接了不同的组件以支持登录。欲使用这个框架, 你主要需要做以下工作:

  • 设置用户组件 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 类在登录和注销流程引发一些事件。

    1、yii\web\User::EVENT_BEFORE_LOGIN:在登录 yii\web\User::login() 时引发。 
    如果事件句柄将事件对象的 yii\web\UserEvent::isValid 属性设为 false, 登录流程将会被取消。
    
    2、yii\web\User::EVENT_AFTER_LOGIN:登录成功后引发。
    
    3、yii\web\User::EVENT_BEFORE_LOGOUT:注销 yii\web\User::logout() 前引发。 
    如果事件句柄将事件对象的 yii\web\UserEvent::isValid 属性设为 false, 注销流程将会被取消。
    
    4、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数组。匹配比较是大小写敏感的。如果该选项为空,或者不使用该选项, 意味着当前规则适用于所有的操作。

    1、yii\filters\AccessRule::controllers:指定该规则用于匹配哪些控制器。 它的值应为控制器ID数组。匹配比较是大小写敏感的。如果该选项为空,或者不使用该选项, 则意味着当前规则适用于所有的操作。(译者注:这个选项一般是在控制器的自定义父类中使用才有意义)

    2、yii\filters\AccessRule::roles:指定该规则用于匹配哪些用户角色。 系统自带两个特殊的角色,通过 yii\web\User::isGuest 来判断:

        * ?: 用于匹配访客用户 (未经认证)
        * @: 用于匹配已认证用户

    3、使用其他角色名时,将触发调用 yii\web\User::can(),这时要求 RBAC 的支持 (在下一节中阐述)。 如果该选项为空或者不使用该选项,意味着该规则适用于所有角色。

    4、yii\filters\AccessRule::ips:指定该规则用于匹配哪些 yii\web\Request::userIP 。 IP 地址可在其末尾包含通配符 * 以匹配一批前缀相同的IP地址。 例如,192.168.* 匹配所有 192.168. 段的IP地址。 如果该选项为空或者不使用该选项,意味着该规则适用于所有角色。

    5、yii\filters\AccessRule::verbs:指定该规则用于匹配哪种请求方法(例如GET,POST)。 这里的匹配大小写不敏感。

    6、yii\filters\AccessRule::matchCallback:指定一个PHP回调函数用于 判定该规则是否满足条件。(译者注:此处的回调函数是匿名函数)

    7、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。


AlphaGooo
1.1k 声望135 粉丝

你想要什么,就去追求什么