项目地址:https://gitee.com/templi/yiicms 感谢七觞酒大神的付出,和免费分享。当然也感谢yii2的开发团队们。
项目已经安全完毕,不知道后台密码,这种背景下,后台无法进去。绕不开的话题就是:
1.后台密码账号如何产生。类似下图:
加密过的字符串自然不能用于登陆。
我尝试通过前台注册一个账户,然后登陆,因为权限不够,尽管等登陆到后台首页,同样无权访问其他任何页面。配置了下modules,
意图通过给予权限系统权限,然后更改刚加入的用户“zhuangli”以管理员角色以及权限分配等。不成功。
我们先来梳理第一个问题,yii2的密码到底是如何产生的?
我们从登陆这里开始,看它一步步都做了什么操作。
只要用户访问,必须经过yii2入口文件index.php,也就是在这个时候新创建了一个application实例,使用了配置文件中的各种配置项以及系统默认给定的几个包括响应组件,请求组件,错误组件等核心组件的实例,附着在该应用上,方便随时供访问。其中这里配置了一个user,当然不配置使用系统默认也是可以的。
'user' => [ 'identityClass' => 'commonmodelsUser', 'enableAutoLogin' => true, 'identityCookie' => ['name' => '_identity-backend', 'httpOnly' => true], ],
这个组件规定了权限类,是否自动登陆以及权限验证cookie
1 User is the class for the `user` application component that manages the user authentication status. 2 * 3 * You may use [[isGuest]] to determine whether the current user is a guest or not. 4 * If the user is a guest, the [[identity]] property would return `null`. Otherwise, it would 5 * be an instance of [[IdentityInterface]]. 6 * 7 * You may call various methods to change the user authentication status: 8 * 9 * - [[login()]]: sets the specified identity and remembers the authentication status in session and cookie; 10 * - [[logout()]]: marks the user as a guest and clears the relevant information from session and cookie; 11 * - [[setIdentity()]]: changes the user identity without touching session or cookie 12 * (this is best used in stateless RESTful API implementation). 13 * 14 * Note that User only maintains the user authentication status. It does NOT handle how to authenticate 15 * a user. The logic of how to authenticate a user should be done in the class implementing [[IdentityInterface]]. 16 * You are also required to set [[identityClass]] with the name of this class. 17 * 18 * User is configured as an application component in [[yiiwebApplication]] by default. 19 * You can access that instance via `Yii::$app->user`. 20 * 21 * You can modify its configuration by adding an array to your application config under `components` 22 * as it is shown in the following example: 23 * 24 * ```php 25 * 'user' => [ 26 * 'identityClass' => 'appmodelsUser', // User must implement the IdentityInterface 27 * 'enableAutoLogin' => true, 28 * // 'loginUrl' => ['user/login'], 29 * // ... 30 * ] 31 * ``` 32 * 33 * @property string|int $id The unique identifier for the user. If `null`, it means the user is a guest. This 34 * property is read-only. 35 * @property IdentityInterface|null $identity The identity object associated with the currently logged-in 36 * user. `null` is returned if the user is not logged in (not authenticated). 37 * @property bool $isGuest Whether the current user is a guest. This property is read-only. 38 * @property string $returnUrl The URL that the user should be redirected to after login. Note that the type 39 * of this property differs in getter and setter. See [[getReturnUrl()]] and [[setReturnUrl()]] for details. 40 *
翻译一下这段对user这个组件的说明:
这是一个user应用组件的类,管理用户的认证状态。可以使用isGuest(实际是getIsGuest)来辨别当前用户是否用游客。如果为游客,identity属性返回null,否则就是IdentityInterface接口的实例。有很多方法可以改变认证状态。
login,设置特定的身份,并将认证状态存在session或cookie中。
loginout,标示为游客,并将有关的信息从session或cookie中清除。
setIndentiy,只改变用户身份,不涉及session和cookie。这最好用于无状态的REST API实现。
User 仅仅用于维护用户认证状态,它并不负责如何去认证一个用户。如何去认证一个用户的逻辑应在继承至IdentityInterface类中去完成,User这个类也被要求设置为identityClass属性的值。正如这里做的:
它是被默认配置为yiiwebapplication的一个应用组件,正如我上面所说。可以直接通过Yii::$app->user访问到该组件。当然可以改变配置,通过在配置文件中的components底下加入数组,类似:
'user' => [
'identityClass' => 'appmodelsUser', // User must implement the IdentityInterface
'enableAutoLogin' => true,
'loginUrl' => ['user/login'],
// ...
]
$id唯一身份标示,为null,代表是游客,只读。$identity关联到当前登入用户的认证对象,null代表未登入(未认证)。$isGuest,当前用户是否游客,只读。$returnUrl,登陆后重定向的url。
继续看登陆的过程。
1 /** 2 * Login action. 3 * 4 * @return string 5 */ 6 public function actionLogin() 7 { 8 $this->layout = false; 9 var_dump(Yii::$app->user); 10 11 if (!Yii::$app->user->isGuest) { 12 return $this->goHome(); 13 } 14 15 $model = new LoginForm(); 16 if ($model->load(Yii::$app->request->post()) && $model->login()) { 17 return $this->goBack(); 18 } else { 19 return $this->render('login', [ 20 'model' => $model, 21 ]); 22 } 23 }
首先是判断用户是否游客,不是,重定向到主页去,新建一个loginForm模型实例,这是个登陆表单的虚拟数据模型,将username和password加载到这个model中,验证执行登陆login操作。
1 /** 2 * Logs in a user using the provided username and password. 3 * 4 * @return bool whether the user is logged in successfully 5 */ 6 public function login() 7 { 8 if ($this->validate()) { 9 return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0); 10 } else { 11 return false; 12 } 13 }
开步就是验证,这个validate方法是该类的父类model上的方法。
validate点进去是以下这样一个方法: 1 /** 2 * Performs the data validation.
3 * 4 * This method executes the validation rules applicable to the current [[scenario]]. 5 * The following criteria are used to determine whether a rule is currently applicable: 6 * 7 * - the rule must be associated with the attributes relevant to the current scenario; 8 * - the rules must be effective for the current scenario. 9 * 10 * This method will call [[beforeValidate()]] and [[afterValidate()]] before and 11 * after the actual validation, respectively. If [[beforeValidate()]] returns false, 12 * the validation will be cancelled and [[afterValidate()]] will not be called. 13 * 14 * Errors found during the validation can be retrieved via [[getErrors()]], 15 * [[getFirstErrors()]] and [[getFirstError()]]. 16 * 17 * @param array $attributeNames list of attribute names that should be validated. 18 * If this parameter is empty, it means any attribute listed in the applicable 19 * validation rules should be validated. 20 * @param bool $clearErrors whether to call [[clearErrors()]] before performing validation 21 * @return bool whether the validation is successful without any error. 22 * @throws InvalidParamException if the current scenario is unknown. 23 */ 24 public function validate($attributeNames = null, $clearErrors = true) 25 {
//清除错误 26 if ($clearErrors) { 27 $this->clearErrors(); 28 } 29
//前置验证 30 if (!$this->beforeValidate()) { 31 return false; 32 } 33
//取回场景列表,该方法返回一个场景列表和活跃属性,活跃属性是指当前场景中需要验证的。格式:
```php
* [
* 'scenario1' => ['attribute11', 'attribute12', ...],
* 'scenario2' => ['attribute21', 'attribute22', ...],
* ...
* ]
默认情况下,活跃属性被认为安全,可以批量指定。如果不在批量指定之列,则认为不安全,这样请加一个!作为前缀。这个方法的默认实现会返回所有在rules中能找到的场景声明,一个特别的场景
是SCENARIO_DEFAULT,包括rules中的所有场景。每个场景将都通过运用到场景的验证规则关联到被验证的属性。
34 $scenarios = $this->scenarios(); 35 $scenario = $this->getScenario(); 36 if (!isset($scenarios[$scenario])) { 37 throw new InvalidParamException("Unknown scenario: $scenario"); 38 } 39 40 if ($attributeNames === null) { 41 $attributeNames = $this->activeAttributes(); 42 } 43 44 foreach ($this->getActiveValidators() as $validator) { 45 $validator->validateAttributes($this, $attributeNames); 46 } 47 $this->afterValidate(); 48 49 return !$this->hasErrors(); 50 }
注释翻译:这是一个执行数据验证的方法,这个方法将验证规则应用到当前场景,下面的标准用以决定规则当前是否可用。
规则必须将属性关联到当前场景。规则必须对当前场景有效。
这个方法前后都要调用beforeValidate和afterValidate。中间产生的错误可由getErrors,[[getFirstErrors()]] and [[getFirstError()]]取得。$attributeNames,用以验证的属性列表,如果参数为空,那意味着可应用的规则中的属性都要被验证。也即是这
1 public function getValidators() 2 { 3 if ($this->_validators === null) { 4 $this->_validators = $this->createValidators(); 5 } 6 return $this->_validators; 7 }
注释释义:
个参数指定哪些是需要被验证的,不指定就验证所有。
场景的使用方法参考:Yii2 - 场景scenarios用法
捋一下,场景中如果没有在后代model中重写scenarios,那么在所有场景(action)都会执行所有rules指定字段的所有验证(同一字段存在多种验证,同一验证也可能应用到多个字段)。那么场景指定各个场景要验证的字段后,rules中就要用on关键字将指定场景关联到该条rules,这样场景跟验证规则跟字段就一一对应起来了,如果没有on指定场景,那就是应用到所有场景。在controller中应用某场景还需要使用$model->setScenario('update')或者$model->scenario = 'update'来应用。
$this->scenarios();中有一个方法$this->getValidators(),获取验证器。
返回rules中声明的验证器,不同于getActiveValidators的是,这个只返回应用到当前场景的验证器。因为它的返回结果是一个ArrayObject,因此可以手动插入或移除,在model behaviors中很有用。
接下来:
1 /** 2 * Creates validator objects based on the validation rules specified in [[rules()]]. 3 * Unlike [[getValidators()]], each time this method is called, a new list of validators will be returned. 4 * @return ArrayObject validators 5 * @throws InvalidConfigException if any validation rule configuration is invalid 6 */ 7 public function createValidators() 8 { 9 $validators = new ArrayObject; 10 foreach ($this->rules() as $rule) { 11 if ($rule instanceof Validator) { 12 $validators->append($rule); 13 } elseif (is_array($rule) && isset($rule[0], $rule[1])) { // attributes, validator type 14 $validator = Validator::createValidator($rule[1], $this, (array) $rule[0], array_slice($rule, 2)); 15 $validators->append($validator); 16 } else { 17 throw new InvalidConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.'); 18 } 19 } 20 return $validators; 21 }
通过rules中指定的验证规则创建相应验证器。再看看Validator::createValidator方法。
1 /** 2 * Creates a validator object. 3 * @param string|Closure $type the validator type. This can be either: 4 * * a built-in validator name listed in [[builtInValidators]]; 5 * * a method name of the model class; 6 * * an anonymous function; 7 * * a validator class name. 8 * @param yiiaseModel $model the data model to be validated. 9 * @param array|string $attributes list of attributes to be validated. This can be either an array of 10 * the attribute names or a string of comma-separated attribute names. 11 * @param array $params initial values to be applied to the validator properties. 12 * @return Validator the validator 13 */ 14 public static function createValidator($type, $model, $attributes, $params = []) 15 { 16 $params['attributes'] = $attributes; 17 18 if ($type instanceof Closure || $model->hasMethod($type)) { 19 // method-based validator 20 $params['class'] = __NAMESPACE__ . 'InlineValidator'; 21 $params['method'] = $type; 22 } else { 23 if (isset(static::$builtInValidators[$type])) { 24 $type = static::$builtInValidators[$type]; 25 } 26 if (is_array($type)) { 27 $params = array_merge($type, $params); 28 } else { 29 $params['class'] = $type; 30 } 31 } 32 33 return Yii::createObject($params); 34 }
$attribute即被验证的字段。$type指验证类,比如required,unique等,可以是闭包,可以是model上自定义的方法,将创建一个内联验证器,也可以是内置的若干验证器,或者是个带有指定class的数组,根据那个class以及其中的验证方法来验证,很灵活很强大。最后一种就是直接就是指定的type本身就是一个类。这是内联验证器的默认方式。
1 public static $builtInValidators = [ 2 'boolean' => 'yiivalidatorsBooleanValidator', 3 'captcha' => 'yiicaptchaCaptchaValidator', 4 'compare' => 'yiivalidatorsCompareValidator', 5 'date' => 'yiivalidatorsDateValidator', 6 'datetime' => [ 7 'class' => 'yiivalidatorsDateValidator', 8 'type' => DateValidator::TYPE_DATETIME, 9 ], 10 'time' => [ 11 'class' => 'yiivalidatorsDateValidator', 12 'type' => DateValidator::TYPE_TIME, 13 ], 14 'default' => 'yiivalidatorsDefaultValueValidator', 15 'double' => 'yiivalidatorsNumberValidator', 16 'each' => 'yiivalidatorsEachValidator', 17 'email' => 'yiivalidatorsEmailValidator', 18 'exist' => 'yiivalidatorsExistValidator', 19 'file' => 'yiivalidatorsFileValidator', 20 'filter' => 'yiivalidatorsFilterValidator', 21 'image' => 'yiivalidatorsImageValidator', 22 'in' => 'yiivalidatorsRangeValidator', 23 'integer' => [ 24 'class' => 'yiivalidatorsNumberValidator', 25 'integerOnly' => true, 26 ], 27 'match' => 'yiivalidatorsRegularExpressionValidator', 28 'number' => 'yiivalidatorsNumberValidator', 29 'required' => 'yiivalidatorsRequiredValidator', 30 'safe' => 'yiivalidatorsSafeValidator', 31 'string' => 'yiivalidatorsStringValidator', 32 'trim' => [ 33 'class' => 'yiivalidatorsFilterValidator', 34 'filter' => 'trim', 35 'skipOnArray' => true, 36 ], 37 'unique' => 'yiivalidatorsUniqueValidator', 38 'url' => 'yiivalidatorsUrlValidator', 39 'ip' => 'yiivalidatorsIpValidator', 40 ];
内联的有以上这么多方法。
1 /** 2 * Returns the attribute names that are subject to validation in the current scenario. 3 * @return string[] safe attribute names 4 */ 5 public function activeAttributes() 6 { 7 $scenario = $this->getScenario(); 8 $scenarios = $this->scenarios(); 9 if (!isset($scenarios[$scenario])) { 10 return []; 11 } 12 $attributes = $scenarios[$scenario]; 13 foreach ($attributes as $i => $attribute) { 14 if ($attribute[0] === '!') { 15 $attributes[$i] = substr($attribute, 1); 16 } 17 } 18 19 return $attributes; 20 }
返回从属于当前场景的安全属性名列表。
返回应用到当前场景的验证器。$attribute是指需要返回验证器的属性名。如果为null,所有model中的属性将返回。
1 foreach ($this->getActiveValidators() as $validator) { 2 $validator->validateAttributes($this, $attributeNames); 3 }
这个循环对当前场景活跃属性进行验证,验证代码如下:
1 /** 2 * Validates the specified object. 3 * @param yiiaseModel $model the data model being validated 4 * @param array|null $attributes the list of attributes to be validated. 5 * Note that if an attribute is not associated with the validator - it will be 6 * ignored. If this parameter is null, every attribute listed in [[attributes]] will be validated. 7 */ 8 public function validateAttributes($model, $attributes = null) 9 { 10 if (is_array($attributes)) { 11 $newAttributes = []; 12 foreach ($attributes as $attribute) { 13 if (in_array($attribute, $this->getAttributeNames(), true)) { 14 $newAttributes[] = $attribute; 15 } 16 } 17 $attributes = $newAttributes; 18 } else { 19 $attributes = $this->getAttributeNames(); 20 } 21 22 foreach ($attributes as $attribute) { 23 $skip = $this->skipOnError && $model->hasErrors($attribute) 24 || $this->skipOnEmpty && $this->isEmpty($model->$attribute); 25 if (!$skip) { 26 if ($this->when === null || call_user_func($this->when, $model, $attribute)) { 27 $this->validateAttribute($model, $attribute); 28 } 29 } 30 } 31 }
验证指定的对象,第一个参数$model,要验证的model,第二个参数要被验证的属性列表。注意如果属性没有没有关联到验证器会忽略,如果这个参数为空,attributes中的属性都会被验证。
对scenarios方法的详解:
1 /** 2 * Returns a list of scenarios and the corresponding active attributes. 3 * An active attribute is one that is subject to validation in the current scenario. 4 * The returned array should be in the following format: 5 * 6 * ```php 7 * [ 8 * 'scenario1' => ['attribute11', 'attribute12', ...], 9 * 'scenario2' => ['attribute21', 'attribute22', ...], 10 * ... 11 * ] 12 * ``` 13 * 14 * By default, an active attribute is considered safe and can be massively assigned. 15 * If an attribute should NOT be massively assigned (thus considered unsafe), 16 * please prefix the attribute with an exclamation character (e.g. `'!rank'`). 17 * 18 * The default implementation of this method will return all scenarios found in the [[rules()]] 19 * declaration. A special scenario named [[SCENARIO_DEFAULT]] will contain all attributes 20 * found in the [[rules()]]. Each scenario will be associated with the attributes that 21 * are being validated by the validation rules that apply to the scenario. 22 * 23 * @return array a list of scenarios and the corresponding active attributes. 24 */ 25 public function scenarios() 26 { 27 $scenarios = [self::SCENARIO_DEFAULT => []]; 28 //先做一遍清空,循环类数组对象 29 foreach ($this->getValidators() as $validator) { 30 //rules中指定的on场景清空 31 foreach ($validator->on as $scenario) { 32 $scenarios[$scenario] = []; 33 } 34 //rules中指定的except场景清空 35 foreach ($validator->except as $scenario) { 36 $scenarios[$scenario] = []; 37 } 38 } 39 40 //场景变量中的第一个为self::SCENARIO_DEFAULT,后面依次为上面循环的on和except中的场景 41 //这些场景应该指活跃场景,这些场景是需要验证的,其他不必。 42 $names = array_keys($scenarios); 43 44 //此处对验证器循环,检查未绑定有和排除了验证的场景,全部置真。 45 //而未绑定有,并且不在绑定了排除验证场景的,置真。就是说这些是需要验证的。 46 //检查有绑定场景,则将这些置真来验证,其他的不验证。 47 foreach ($this->getValidators() as $validator) { 48 if (empty($validator->on) && empty($validator->except)) { 49 foreach ($names as $name) { 50 //rules中没有指定on和except,那$names就只有default 51 //此时会将验证器上所有的属性循环到$scenarios['default']中去。 52 //意即所有的属性在所有场景都要验证 53 foreach ($validator->attributes as $attribute) { 54 $scenarios[$name][$attribute] = true; 55 } 56 } 57 } elseif (empty($validator->on)) { 58 //rules中on为空,except设置 59 foreach ($names as $name) { 60 //不在except中,并且在rules中,在场景中设置真。不在场景中的根本不进入这个验证列表。 61 //结果还是留下default,实际情况也是如此,所有场景都不验证,符合逻辑 62 if (!in_array($name, $validator->except, true)) { 63 foreach ($validator->attributes as $attribute) { 64 $scenarios[$name][$attribute] = true; 65 } 66 } 67 } 68 } else { 69 //这里会留下on绑定的场景的属性。除外的就被排除了。 70 foreach ($validator->on as $name) { 71 foreach ($validator->attributes as $attribute) { 72 $scenarios[$name][$attribute] = true; 73 } 74 } 75 } 76 } 77 78 //上面是跟rules所限定的验证器比对,在的并符合需要验证条件的置真。 79 //然后与场景list中的设置进行比较,过滤调场景中没有的属性 80 foreach ($scenarios as $scenario => $attributes) { 81 if (!empty($attributes)) { 82 $scenarios[$scenario] = array_keys($attributes); 83 } 84 } 85 86 return $scenarios; 87 }
1 /** 2 * Returns the validators applicable to the current [[scenario]]. 3 * @param string $attribute the name of the attribute whose applicable validators should be returned. 4 * If this is null, the validators for ALL attributes in the model will be returned. 5 * @return yiivalidatorsValidator[] the validators applicable to the current [[scenario]]. 6 */ 7 public function getActiveValidators($attribute = null) 8 { 9 $validators = []; 10 $scenario = $this->getScenario(); 11 foreach ($this->getValidators() as $validator) { 12 if ($validator->isActive($scenario) && ($attribute === null || in_array($attribute, $validator->getAttributeNames(), true))) { 13 $validators[] = $validator; 14 } 15 } 16 return $validators; 17 }
以上返回应用到当前场景的验证器,$attribute指定要返回的验证器属性名。如果为null,所有的都返回。这里先获取到场景名,默认是default,然后循环当前model上的验证器,如果在
当前这个场景验证器活跃,并且传入的属性为null,或者该属性存在于验证器上,写入验证器数组。
1 /** 2 * Returns a value indicating whether the validator is active for the given scenario and attribute. 3 * 4 * A validator is active if 5 * 6 * - the validator's `on` property is empty, or 7 * - the validator's `on` property contains the specified scenario 8 * 9 * @param string $scenario scenario name 10 * @return bool whether the validator applies to the specified scenario. 11 */ 12 public function isActive($scenario) 13 { 14 return !in_array($scenario, $this->except, true) && (empty($this->on) || in_array($scenario, $this->on, true)); 15 }
返回一个代表验证器在给定场景和属性上是否激活。激活的判断标准是,验证器的on属性为空,或者on属性包含指定的场景。
1 /** 2 * Returns cleaned attribute names without the `!` character at the beginning 3 * @return array attribute names. 4 * @since 2.0.12 5 */ 6 public function getAttributeNames() 7 { 8 return array_map(function($attribute) { 9 return ltrim($attribute, '!'); 10 }, $this->attributes); 11 }
在开始返回不带有!的干净的属性名。
1 /** 2 * Validates the specified object. 3 * @param yiiaseModel $model the data model being validated 4 * @param array|null $attributes the list of attributes to be validated. 5 * Note that if an attribute is not associated with the validator - it will be 6 * ignored. If this parameter is null, every attribute listed in [[attributes]] will be validated. 7 */ 8 public function validateAttributes($model, $attributes = null) 9 { 10 if (is_array($attributes)) { 11 $newAttributes = []; 12 foreach ($attributes as $attribute) { 13 if (in_array($attribute, $this->getAttributeNames(), true)) { 14 $newAttributes[] = $attribute; 15 } 16 } 17 $attributes = $newAttributes; 18 } else { 19 $attributes = $this->getAttributeNameas(); 20 } 21 22 foreach ($attributes as $attribute) { 23 $skip = $this->skipOnError && $model->hasErrors($attribute) 24 || $this->skipOnEmpty && $this->isEmpty($model->$attribute); 25 if (!$skip) { 26 if ($this->when === null || call_user_func($this->when, $model, $attribute)) { 27 $this->validateAttribute($model, $attribute); 28 } 29 } 30 } 31 }
验证的核心代码,验证指定的对象,参数$model是被验证的数据模型,$attributes要被验证的属性列表,如果他没有关联的到验证器就忽略,如果该参数为null,所有的属性都将被验证。
如果是数组,定义一个空数组,并对该数组循环,传入的就是该活跃场景下所有属性,一般为default,全部。场景中的属性如果不在验证器里面,就忽略掉,不存入新数组,如果不是数组,
就直接从验证器上取出属性。
$skip = $this->skipOnError && $model->hasErrors($attribute) || $this->skipOnEmpty && $this->isEmpty($model->$attribute);
定义的$this->skipOnError默认为真,$this->skipOnEmpty也为真,其他两个就是检测是否有错或者空。前两个和后两个合成的结果可以有一个为假,但必须同时为假,已经有两个属性默认为真的,那前后两个其他属性必须都为假,就是说该被验证的属性既不能为空,也不能有错。