مشاركة مصادقة Yii1 / yii2

الصورة

هذه المقالة لا معنى لها بدون الجزء الأول ، حيث توجد إجابة "لماذا نفعل ذلك".

يتعلق الأمر بتقنية ترحيل المشروع بسلاسة من yii1 إلى yii2. جوهرها هو أن فروع المشروع على yii1 ونسخته الجديدة على yii2 تعملان معًا على نفس المجال في مضيف افتراضي واحد ، ويتم الترحيل تدريجيًا ، في خطوات صغيرة (من خلال الصفحات ، وحدات التحكم ، الوحدات النمطية ، إلخ).

كان الجزء الأول حول كيفية تشغيل مشروع مكشوف على yii2 في مضيف افتراضي موجود ، أي جعل كلا الفرعين يعملان معًا دون التدخل في بعضهما البعض.

بعد ذلك تبدأ المرحلة الأكثر صعوبة نفسياً: تحتاج إلى إنشاء الحد الأدنى من البنية التحتية للبداية. أود أن أفرد مهمتين: التصميم المكرر ومصادقة المستخدم من النهاية إلى النهاية.

يتم إعطاء ازدواجية التصميم في المقام الأول عن طريق مملة. إذا كنت غير محظوظ ، يمكنك ببساطة نسخ / إرجاع "1 في 1" القديم. أنا شخصياً أتحد دائماً مع إعادة التصميم. على سبيل المثال تم تحديث الواجهة والتصميم بشكل كبير وفي هذا الصدد ، فإن العمل ليس غبيًا. ولكن هنا لكل واحد منه - أنا أهتم كثيرًا بالواجهة والتصميم ، على العكس من ذلك ، يحب شخص ما الواجهة الخلفية ووحدة التحكم أكثر. ومع ذلك ، بغض النظر عن التفضيلات ، لا توجد طريقة لتجاوز هذه المهمة - سيكون عليك إنشاء واجهة ، وسيكون حجم العمل كبيرًا جدًا.

المصادقة من طرف إلى طرف هي أكثر إثارة للاهتمام ، وسيكون هناك عمل أقل. كما في المقال الأول ، لن يكون هناك أي وحي. شخصية المقال: البرنامج التعليمي لأولئك الذين يحلون هذه المشكلة لأول مرة.

إذا كانت هذه هي حالتك ، فعندئذ مزيد من التفاصيل تحت القطع

بادئ ذي بدء ، تحتاج إلى تقسيم الوظائف بين الفروع. لأن من المفهوم أن مرحلة الترحيل تكون في البداية فقط ، ثم ، على الأرجح ، كل العمل مع المستخدمين (التسجيل ، المصادقة ، استعادة كلمة المرور ، إلخ) يبقى على الموقع القديم. ويجب أن يرى الفرع الجديد على yii2 فقط المستخدمين المصادق عليهم بالفعل.

تختلف آليات المصادقة لـ yii1 / yii2 قليلاً وتحتاج إلى تعديل رمز yii2 حتى يتمكن من رؤية مستخدم مصادق عليه. لأن يتم تخزين بيانات المصادقة في الجلسة ، ما عليك سوى الاتفاق على المعلمات لقراءة بيانات الجلسة.

في جلسة من yii1 ، يتم تخزين هذا بطريقة أو بأخرى على النحو التالي:

print_r($_SESSION); Array ( [34e60d27092d90364d1807021107e5a3__id] => 123456 [34e60d27092d90364d1807021107e5a3__name] => tester [34e60d27092d90364d1807021107e5a3__states] => Array ( ) ) 

كيف يتم تخزينها معك - تحقق ، على سبيل المثال ، يتم إنشاء بادئة مفتاح بشكل مختلف في إصدارات مختلفة من yii1.

إليك البيانات التي تحتاجها من yii1

 Yii::app()->user->getStateKeyPrefix() Yii::app()->name Yii::app()->getBasePath() Yii::app()->getId() get_class(Yii::app()->user) 

أسهل طريقة هي إنشاء صفحة اختبار وعرض جميع البيانات الضرورية عليها - ستكون مطلوبة في المستقبل.

المصادقة في yii2


في Yii2 ، توجد جميع الوظائف التي نحتاجها في مكون المستخدم ( \ yii \ web \ User ) ، الذي يتحكم في حالة المصادقة.

  • في أسلوب getIdentity () ، يُطلق على RenOutStatus () ، حيث يتم البحث عن جلسة المصادقة بالمفتاح من المتغير $ idParam (يتم تخزين "__id" افتراضيًا هناك) ؛
  • في متغير الجلسة ، يتم تخزين معرف المستخدم باستخدام مفتاح $ idParam (على سبيل المثال ، من التطبيق / الطراز / المستخدم ).

يتم وصف خوارزمية المصادقة بالتفصيل في الدليل الرسمي .

بالطبع ، في السنة yii1 ، يتم تخزين الجلسات بمفتاح مختلف. لذلك ، تحتاج إلى جعل yii2 تبحث عن معرف المستخدم باستخدام نفس المفاتيح التي يتم تخزينها بها في yii1.

للقيام بذلك:

1. قم بتغيير فئة مكون المستخدم المسؤول عن إدارة حالة المصادقة الخاصة بنا ، الموروثة من yii / web / User في config / web.php

 'components' => [ 'user' => [ 'class' => 'app\models\WebUser', 'identityClass' => 'app\models\User', ], ] 

2. قم بضبط قيمة $ idParam في app \ models \ WebUser .

 public function init() { //  idParam (    //    ) $this->idParam = $this->getIdParamYii1(); } 

تحت المفسد سيكون هناك عدد قليل من الطرق التي تحاكي بطريقة أو بأخرى سلوك مماثل من yii1.

بشكل عام ، يمكنك فقط نسخ _keyPrefix الأصلي (أو حتى idParam على الفور ) من yii1 وعدم محاكاة جيله ، ولكن بعد ذلك سيكون الأمر مثل التعليمات "نسخ القمامة غير المفهومة".

في الواقع ، يمكنك النسخ لأن _keyPrefix في yii1 ثابت تقريبًا. يعتمد ذلك على اسم فئة مكون المستخدم وعلى معرف التطبيق ، والذي يتم الحصول عليه بدوره من موقع التطبيق واسمه.

 //   yii1  _keyPrefix $this->_keyPrefix = md5('Yii.'.get_class($this).'.'.Yii::app()->getId()); //   - ID  $this->_id=sprintf('%x',crc32($this->getBasePath().$this->name)); 

إذا اقتصرنا على مهمة المصادقة فقط ، فإن نسخ قيمة _keyPrefix يقلل بشكل كبير من كمية العمل. ولكن سيكون لدي أمثلة للاستخدام على نطاق أوسع.

مكون المستخدم (التطبيق / النماذج / WebUser)
 namespace app\models; use yii\web\User; class WebUser extends User { /** *    cookies  yii2, *     yii1 */ public $autoRenewCookie = false; /** * _keyPrefix    CWebUser  Yii1 */ private $_keyPrefix; /** *    Yii1,    */ private $paramsYii1 = [ //    user   Yii1 'classUserComponent' => 'CWebUser', // ID  Yii1 //  ,  Yii::app()->getId() 'appId' => '', //     Yii1 'appName' => 'My Web Application', //     yii1  \Yii::getAlias('@app') 'relPath' => '../htdocs/protected', ]; public function init() { //  idParam (    //    ) $this->idParam = $this->getIdParamYii1(); } } 

وطرق إضافية لها (WebUser). فصل لسهولة المشاهدة.

 /** *     Yii1,    ID  */ public function getIdParamYii1() { return $this->getStateKeyPrefix() . '__id'; } /** *    Yii 1 * @return string */ public function getStateKeyPrefix() { if ($this->_keyPrefix !== null) return $this->_keyPrefix; $class = $this->paramsYii1['classUserComponent']; return $this->_keyPrefix = md5('Yii.' . $class . '.' . $this->getAppIdYii1()); } /** *   getId()  CApplication * @return string ID  Yii1 */ public function getAppIdYii1() { if ($this->paramsYii1['appId']) return $this->paramsYii1['appId']; return $this->paramsYii1['appId'] = sprintf('%x', crc32($this->getBasePathYii1() . $this->paramsYii1['appName'])); } /** * @return string    Yii1 */ private function getBasePathYii1() { $basePath = realpath(\Yii::getAlias('@app') . DIRECTORY_SEPARATOR . $this->paramsYii1['relPath']); if (!$basePath) throw new InvalidConfigException('basePath  yii1  .'); return $basePath; } 

فقط لمهمة " الموافقة على شكل مفتاح الجلسة " ، يكون تحليل الأساليب معقدًا بعض الشيء ، لكنها مفيدة للأمثلة أدناه.

بعد ذلك ، في فرع جديد على yii2 ، يبدأ التعرف على المستخدمين المصرح لهم سابقًا في yii1 بالعمل. من الناحية المثالية ، سيكون من الضروري التوقف عند هذا ، لأن المسار الزلق يبدأ أكثر.

تسجيل دخول المستخدم في yii2


بعد الموافقة على تنسيق التخزين لمعرف المستخدم في الجلسة ، من الممكن أن يعمل تسجيل دخول المستخدم "تلقائيًا" حتى عام 2.

أعتقد أنه من غير الصحيح استخدام نموذج تسجيل الدخول على yii2 في وضع القتال دون تعطيل النموذج المقابل على yii1 ، ومع ذلك ، ستعمل الوظيفة الأساسية بعد القليل من التنسيق.

لأن نعتقد أنه أثناء تسجيل المستخدم ، وبالتالي تجزئة كلمات المرور ، غادرنا في yii1 ، ثم لتسجيل الدخول عبر yii2 ، نحتاج إلى التأكد من أن طريقة التحقق من كلمة المرور المحفوظة في yii2 يمكنها فهم ما تم تجزئته وتخزينه في Yii1.

تحقق مما تفعله هذه الأساليب.

على سبيل المثال ، إذا كان Yii2 في نموذج المستخدم القياسي الشرطي ، يتحقق المستخدم من كلمة المرور على النحو التالي:

 public function validatePassword($password) { return \Yii::$app->getSecurity()->validatePassword($password, $this->password); } 

ثم انظر إلى طريقة ValidatePassword ($ password ، $ hash) من yii \ base \ Security (Yii2)

التحقق من صحة كلمة المرور ()
 public function validatePassword($password, $hash) { if (!is_string($password) || $password === '') { throw new InvalidArgumentException('Password must be a string and cannot be empty.'); } if (!preg_match('/^\$2[axy]\$(\d\d)\$[\.\/0-9A-Za-z]{22}/', $hash, $matches) || $matches[1] < 4 || $matches[1] > 30 ) { throw new InvalidArgumentException('Hash is invalid.'); } if (function_exists('password_verify')) { return password_verify($password, $hash); } $test = crypt($password, $hash); $n = strlen($test); if ($n !== 60) { return false; } return $this->compareString($test, $hash); } 

وإذا تم تجزئة كلمة المرور على Yii1 في نموذج المستخدم على النحو التالي:

 public function hashPassword($password) { return CPasswordHelper::hashPassword($password); } 

ثم قارن مع checkPassword ($ password ، $ hash) من yii \ framework \ utils \ CPasswordHelper

علامة التجزئة ()
 public static function hashPassword($password,$cost=13) { self::checkBlowfish(); $salt=self::generateSalt($cost); $hash=crypt($password,$salt); if(!is_string($hash) || (function_exists('mb_strlen') ? mb_strlen($hash, '8bit') : strlen($hash))<32) throw new CException(Yii::t('yii','Internal error while generating hash.')); return $hash; } 


إذا كانت طرق التجزئة والتحقق مختلفة ، فأنت بحاجة إلى تغيير التحقق من الصحة في validatePassword () من app \ model \ User .

من مربعات أحدث إصدارات الإطار ، تتوافق تجزئات كلمة المرور Yii1 / Yii2. لكن هذا لا يعني على الإطلاق أنها ستكون متوافقة معك أو أنها ستتزامن في المستقبل. مع درجة عالية من الاحتمال ، ستختلف طرق تجزئة المشروع في Yii1 والتحقق في المشروع الجديد على Yii2.

Autologin في Yii2 على ملفات تعريف الارتباط من yii1


نظرًا لأن فرعًا على Yii2 يعرف بالفعل كيفية استخدام بيانات مصادقة المستخدم من Yii1 بشفافية ، فلماذا لا يتم إعداد تسجيل الدخول التلقائي بواسطة ملف تعريف الارتباط؟
إذا كانت مثل هذه الفكرة تؤثر عليك ، فإنني أنصحك بالتخلي عنها. لا أرى أي سبب وجيه لتمكين تسجيل الدخول التلقائي على Yii2 دون نقل عمل المستخدم (المصادقة ، أولاً وقبل كل شيء) إلى هذا الموضوع. على سبيل المثال أعني الحالة التالية:

يتم إجراء مصادقة المستخدم على Yii1 ، ولكن يجب أن يكون فرع Yii2 قادرًا على القيام بالتسجيل الذاتي على ملفات تعريف الارتباط المخزنة في Yii1.

لنكون صادقين ، هذا تحريف صريح. لن يكون الأمر سهلاً وأنيقًا ، وهنا فقط يمر الخط بين المهمة الواعية والمبررة للهجرة واختراع الدراجات غير الضرورية.

تكمن الصعوبة في أن يي محمية في كلا الفرعين من ملفات تعريف الارتباط المزيفة ، لذلك من الصعب الاتفاق على الطرق.

  • المكونات المضمنة في Yii2 هي: user ( \ yii \ web \ User ) ، Request ، Security + CookieCollection
  • في Yii1: CWebUser و CHttpRequest و CSecurityManager و CStatePersister و CCookieCollection

ومع ذلك ، فإن الحالات مختلفة. فيما يلي مثال على كيفية عمل تسجيل ذاتي للدراجات.

في yii2 ، نحن مهتمون بطريقة getIdentityAndDurationFromCookie () من \ yii \ web \ User . في السطر الأول من هذه الطريقة ، يجب الحصول على ملف تعريف الارتباط المطلوب:

 $value = Yii::$app->getRequest()->getCookies()->getValue($this->identityCookie['name']); 

لكنها لن تكون ، لأنه المجموعة Yii :: $ app-> getRequest () -> getCookies () ستكون فارغة ، لأنه في طلب ملفات تعريف الارتباط يتم تحميلها مع التحقق من الصحة في loadCookies () وبالطبع ، لا يمر.

أسهل طريقة للشوكة هي السلوك القياسي عن طريق استبدال getIdentityAndDurationFromCookie () . على سبيل المثال ، مثل هذا:

  1. قم بتنزيل ملف تعريف الارتباط المطلوب مباشرة من $ _COOKIE superglobal حتى لا تتكيف مع آلية تحميل ملفات تعريف الارتباط القياسية.

    اسم ملف تعريف الارتباط هو _keyPrefix فقط ، والذي نعرفه بالفعل كيفية تلقيه (أو نسخه). لذلك ، قمنا بتغيير $ idCookie القياسي في init () .
  2. قم بفك تشفير ملف تعريف الارتباط الناتج "تقريبًا كما في yii1." كما يحلو لك. على سبيل المثال ، قمت بنسخ الطرق المطلوبة من CSecurityManager .

أدناه ، في الواقع ، هو الرمز.

نحن نعمل في التطبيق / النماذج / WebUser
1. تعيين اسم ملفات تعريف الارتباط IdentityCookie وفقًا لـ yii1

 public function init() { $this->idParam = $this->getIdParamYii1(); //     $this->identityCookie = ['name' => $this->getStateKeyPrefix()]; } 


2. أضف طريقتين إضافيتين

 /** *    */ protected function getIdentityAndDurationFromCookie() { $id = $this->getIdIdentityFromCookiesYii1(); if (!$id) { return null; } $class = $this->identityClass; $identity = $class::findOne($id); if ($identity !== null) { return ['identity' => $identity, 'duration' => 0]; } return null; } /** *  ID identity  cookies,   yii1 * * @return null|integer  ID     null */ protected function getIdIdentityFromCookiesYii1() { if (!isset($_COOKIE[$this->identityCookie['name']])) return null; $cookieValue = $_COOKIE[$this->identityCookie['name']]; // Cookies  yii1 ,  $utilSecurity = new UtilYii1Security($this->getBasePathYii1()); $data = $utilSecurity->validateData($cookieValue); if ($data === false) { return null; } $data = @unserialize($data); if (is_array($data) && isset($data[0], $data[1], $data[2], $data[3])) { list($id, $name, $duration, $states) = $data; return $id; } return null; } 


يستخدم الكود فئة UtilYii1Security معينة - هذا عبارة عن نسخ ولصق معدّل للطرق الضرورية من CSecurityManager ، بحيث يبدو من ناحية الأصلي ، ولكن مع التبسيط. على سبيل المثال ، في CSecurityManager ، هناك العديد من الخيارات لتوليد HMAC (رمز مصادقة الرسائل المستندة إلى التجزئة) ، والتي تعتمد على إصدار php ووجود mbstring. لكن منذ ذلك الحين من المعروف أن yii1 تعمل في نفس بيئة yii2 ، ثم يتم تبسيط المهمة ، وبالتالي ، الكود أيضًا.

لأن من الواضح تمامًا أن عكازًا صريحًا مكتوب هنا ، فلا داعي لمحاولة جعله عالميًا وإعطائه شكلًا رشيقًا ، يكفي "سجنه" في ظل ظروفك.

UtilYii1Security.php
 <?php namespace app\components; /* *   CSecurityManager  Yii1, *     * */ use yii\base\Exception; use yii\base\Model; use yii\base\InvalidConfigException; class UtilYii1Security { /** * ,   yii1 */ const STATE_VALIDATION_KEY = 'Yii.CSecurityManager.validationkey'; /** *  ,    yii1 */ public $hashAlgorithm = 'sha1'; /** *   cookies */ private $_validationKey; /** *    Yii1 */ private $basePath; /** *      yii1 */ private $stateFile; /** * @param string $basePath -    Yii1 *        stateFile */ public function __construct($basePath) { $this->basePath = $basePath; $this->stateFile = $this->basePath . DIRECTORY_SEPARATOR . 'runtime' . DIRECTORY_SEPARATOR . 'state.bin'; if (!realpath($this->stateFile)) throw new InvalidConfigException('    '); } public function validateData($data, $key = null) { if (!is_string($data)) return false; $len = $this->strlen($this->computeHMAC('test')); if ($this->strlen($data) >= $len) { $hmac = $this->substr($data, 0, $len); $data2 = $this->substr($data, $len, $this->strlen($data)); return $this->compareString($hmac, $this->computeHMAC($data2, $key)) ? $data2 : false; } else return false; } public function computeHMAC($data, $key = null) { if ($key === null) $key = $this->getValidationKey(); return hash_hmac($this->hashAlgorithm, $data, $key); } public function getValidationKey() { if ($this->_validationKey !== null) return $this->_validationKey; if (($key = $this->loadStateValidationKey(self::STATE_VALIDATION_KEY)) !== null) { $this->_validationKey = $key; } return $this->_validationKey; } //  validationKey    Yii1 private function loadStateValidationKey($key) { $content = $this->loadState(); if ($content) { $content = unserialize($content); if (isset($content[$key])) return $content[$key]; } return false; } //      Yii1 protected function loadState() { $filename = $this->stateFile; $file = fopen($filename, "r"); if ($file && flock($file, LOCK_SH)) { $contents = @file_get_contents($filename); flock($file, LOCK_UN); fclose($file); return $contents; } return false; } public function compareString($expected, $actual) { $expected .= "\0"; $actual .= "\0"; $expectedLength = $this->strlen($expected); $actualLength = $this->strlen($actual); $diff = $expectedLength - $actualLength; for ($i = 0; $i < $actualLength; $i++) $diff |= (ord($actual[$i]) ^ ord($expected[$i % $expectedLength])); return $diff === 0; } private function strlen($string) { return mb_strlen($string, '8bit'); } private function substr($string, $start, $length) { return mb_substr($string, $start, $length, '8bit'); } } 


تسلسل الإجراءات


عند الترحيل من yii1 إلى yii2 من حيث المصادقة ، اتبعت التسلسل التالي:

  1. جعل مصادقة المستخدم شفافة بين الفروع.
    على سبيل المثال بحيث يقبل فرع yii2 المستخدمين المصادق عليهم مع yii1. إنها سريعة وليست صعبة.
  2. نقل المصادقة (تسجيل دخول المستخدم) من yii1 إلى yii2.
    تعطيله في نفس الوقت في الفرع القديم. لاحظ أنه بعد ذلك سيتوقف autologin عن ملفات تعريف الارتباط عن العمل لأنه لم تعد ملفات تعريف الارتباط من yii1 مناسبة ، ولا يزال هناك عدد قليل من الصفحات الجديدة على yii2.
  3. أدخل على الأقل الصفحة الرئيسية للموقع إلى yii2
    بحيث يمكنك استخدام autologin لملفات تعريف الارتباط الجديدة المخزنة في yii2.
    إن وجود autologin ، على الأقل في الفرع الرئيسي ، سيساعد على إخفاء autologin المفقود في الفرع السابق.
  4. تحقق من أن yii1 يفهم المصادقة في yii2.
    من خلال التفاوض على مفاتيح الجلسة.
  5. نقل تسجيل المستخدم إلى yii2.
    يجب أن يتم النقل بتنسيق تجزئة كلمة المرور المخزنة مسبقًا. ربما احتفظ بتنسيق التجزئة القديم أو قدم تنسيقًا جديدًا ، ولكن حتى يفهم تسجيل الدخول كلا النوعين.
  6. ضع في اعتبارك إضافة المستخدمين إلى الموقع خدمة تمنح yii2 "خارج الصندوق".
    أعني تنفيذ واجهة IdentityInterface الخاصة بالمستخدم ، والتي تتيح المصادقة عن طريق الرمز المميز ، واستعادة كلمة المرور ، وما إلى ذلك. ربما لديك بالفعل الحزام المناسب ، ولكن فجأة لا؟ ثم يعد هذا خيارًا رائعًا لتحسين الخدمة بأقل جهد.

    إذا كانت الإجابة بنعم ، فسيؤدي ذلك إلى تنفيذ (ترحيل) الحساب الشخصي في السنة الثانية (على الأقل جزئيًا).
    إذا كان الجواب "لا" ، فلا يزال عليك التفكير في ترحيل حسابك الشخصي (حتى بدون منتجات جديدة).

ملاحظة:


لا يصف دائمًا الحلول غير الواضحة في مهمة محددة ولا يلزم تطبيقها جميعًا.

لم يتم وصفها لغرض قول "افعل ما أفعل". على سبيل المثال ، من الممكن إنشاء yii2 autologin لملفات تعريف الارتباط من yii1 ، ولكن ، بعبارة ملطفة ، ليست جيدة (ويجب تبرير مثل هذا العكاز بطريقة ما).

لكنني أمضيت بعض الوقت بالفعل في هذا الانتقال خطوة بخطوة للمشاريع وسأكون سعيدًا إذا أنقذه شخص ما ، بالنظر إلى تجربتي ،.

Source: https://habr.com/ru/post/ar419999/


All Articles