
Это руководство является частью серии « Создай свой стартап с помощью PHP» на Envato Tuts +. В этой серии я проведу вас через запуск стартапа от концепции до реальности, используя мое приложение Meeting Planner в качестве примера из реальной жизни. На каждом этапе я буду публиковать код Планировщика собраний в качестве примеров с открытым исходным кодом, из которых вы можете извлечь уроки. Я также буду решать вопросы, связанные с бизнесом по мере их возникновения.
В предыдущем выпуске я рассмотрел в первую очередь безопасность веб-сервера и контроль доступа . В сегодняшнем эпизоде я расскажу о дополнительных мерах защиты, которые я добавил в Meeting Planner. Поскольку весь код написан на Yii2 Framework для PHP, я смог использовать эту среду для ряда этих укреплений. Если вы хотите узнать больше о Yii2, ознакомьтесь с нашей параллельной серией Программирование с Yii2 .
Вы можете попробовать Планировщик собраний прямо сейчас, запланировав свою первую встречу . Не стесняйтесь оставлять отзывы о вашем опыте в комментариях ниже. Я также открыт для новых идей и тематических предложений для будущих уроков.
Здание повышенной безопасности
Реализация различных уровней безопасности для Meeting Planner займет несколько эпизодов. Теперь, когда сервер настроен более надежно, я хочу рассказать вам о других областях безопасности для кода приложения.
Защитные ключи и коды
Очевидно, что важно сохранять аутентификацию ключей вдали от хакеров, но также довольно легко публиковать их в GitHub. Рассказы о случайной регистрации файлов с паролем службы или ключом API.
Чтобы предотвратить это в Yii, я храню внешний файл .ini вне дерева кода. Это загружается в верхней части /frontend/config/main.php и используется для любой необходимой конфигурации компонента:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <?php $config = parse_ini_file(‘/var/secure/meetme.ini’, true); $params = array_merge(     require(__DIR__ . ‘/../../common/config/params.php’),     require(__DIR__ . ‘/../../common/config/params-local.php’),     require(__DIR__ . ‘/params.php’),     require(__DIR__ . ‘/params-local.php’) ); return [     ‘id’ => ‘mp-frontend’,     ‘name’ => ‘Meeting Planner’,     ‘basePath’ => dirname(__DIR__),     ‘bootstrap’ => [‘log’],     ‘controllerNamespace’ => ‘frontend\controllers’,     ‘components’ => [       ‘authClientCollection’ => [               ‘class’ => ‘yii\authclient\Collection’,               ‘clients’ => [                   ‘facebook’ => [                       ‘class’ => ‘yii\authclient\clients\Facebook’,                       ‘clientId’ => $config[‘oauth_fb_id’],                       ‘clientSecret’ => $config[‘oauth_fb_secret’],                   ], | 
В приведенном выше примере вы можете увидеть секреты API Facebook, загруженные из файла инициализации.
Формат файла инициализации довольно прост:
| 01 02 03 04 05 06 07 08 09 10 11 | mysql_host=»localhost» mysql_un=»xxxxxxxxxxxxxxxxxxx» mysql_db=»xxxxxxxxxxxxxxxxxxx» mysql_pwd=»xxxxxxxxxxxxxxxxxxx» mailgun_user = «xxxxxxxxxxxxxxxxxxx@meetingplanner.io» mailgun_pwd = «xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx» mailgun_api_key=»key-9p-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx» mailgun_api_url=»https://api.mailgun.net/v2″ mailgun_public_key=»pubkey-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx» oauth_fb_id=»1xxxxxxxxxxxxxxxxxxx3″ oauth_fb_secret=»bcxxxxxxxxxxxxxxxxxxxda» | 
Yii2 рекомендует вам разместить некоторые из этих настроек в каталоге / сред, особенно если они различаются между разработкой и производством.
Таким образом, важно, чтобы ваш файл .gitignore исключал локальные версии этих файлов:
| 1 2 3 4 5 | #local environment files /environments/prod/common/config/main-local.php /environments/prod/frontend/config/main-local.php /frontend/config/params-local.php /frontend/config/main-local.php | 
Вот пример одного из моих локальных файлов параметров, /frontend/config/params-local.php:
| 1 2 3 4 5 6 | <?php return [   ‘ga’ => ‘UA-xxxxxxxxxx-12’,   ‘urlPrefix’ => »,   ‘google_maps_key’ => ‘AIzzzzzz1111222222xxxxxxQ’, ]; | 
Я мог бы потратить еще больше времени на их организацию.
Блокировка плохих регистраций

Для альфа-релиза я разослал обновления волнами. И на ранних этапах Meeting Planner было больше плохих писем, чем я ожидал. Mailgun позволил легко идентифицировать отказов и отказов:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 | $badEmails=[ », ‘test2@gmail.com’, ‘1111@gmail.com’, ‘qwerty@gmail.com’, ‘amjadiqbalkhanniazi@gmail.com’, ‘admin@admin.com’, ‘rhizalpatra@fellow.lpkia.ac.id’, ‘tm@archi.com’, ‘test@test.com’, ‘web@yahoo.fr’, ‘a@a. ‘nobu@gmail.com’, ‘a@gmail.com’, ‘ct@gmail.com’, ‘sanjaydk@projectdemo. ‘varlog255q@hotmail.com’, ‘baah@baah.com’, ‘minhvnn1@gmail.com’, ‘test@gmail.com’, ‘test@mediabite.co.uk’, ‘ddd@c. ‘Imrky4@gmail.com’, ‘robomadybu@hotmail.com’, ‘mike@mike. ‘azazaz@azazaza.com’, ‘mama@mama.mn’, ‘qweqwe@qwe. ‘test@usertest.fr’, ‘demodemo@demo.com’, ‘qqq@dd.gh’, ‘gnfbb@h. ‘testsir@testsir.com’, ‘oi. ‘ar@yahoo.com’, ‘lex@gmail.com’, ‘Tester1234@gmail.com’, ‘mantaf@mail.com’, ‘aaa@aaa.com’, ‘oeui@gmail.com’, ‘risitesh. ‘ana@gmail.com’, ‘asdf@yahoo.com’, ‘noom@gmail.com’, ‘jomon@example.com’, ‘asdfasdf@yahoo.com’, ‘admin@yahoo.com’, ‘abinubli@mail.com’, ‘tes@tes.com’, ‘asdasdr@asd.com’, ‘something@some.com’, ‘ademin@example.com’, ‘d@dd.com’, ‘robo@gmail.com’, ‘toto@titi.com’, ‘fesfe@fseff. ‘master@wpthemeslist.com’, ‘teste@teste.com’, ‘barny182@hotmail.com’, ‘test@admin.com’, ‘billtian@test.com’, ‘Test@goggle.ca’, ‘jm@gmail.com’, ‘john-panin@qip.ru’, ‘loslos@loslos.com’, ‘ghfhf@jhgjgjk.com’, ‘lol@lol.com’, ‘tester1@gmail.com’, ‘g0952180828@gmail.com’, ‘testim@testim.com’, ‘mnml.name@gmail.com’, ‘endri. ‘geraldo_1989@hotmail.com’, ‘rob. ‘W3test@ya.ru’, ‘user@ya.ru’, ‘ed@ed. | 
Большинство из них, вероятно, из-за разрыва во времени, когда Meeting Planner был новым и бездействующим — во время лечения опухоли головного мозга и операции .
Совсем недавно, добавив социальные учетные записи , я сделал регистрацию на Meeting Planner довольно простой, но регистрация спама все еще возможна. Я хотел, чтобы людям было труднее регистрироваться с плохими электронными письмами.
К счастью, Yii предлагает несколько функций, которые поддерживают это.
Защитный код
 Yii2 теперь предлагает встроенную капчу.  Таким образом, любой, кто регистрируется с помощью старого метода электронной почты и пароля, должен ввести капчу.  Вы можете увидеть поле captcha ниже: 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | <p>Or, fill out the following fields to register manually:</p>        <div class=»col-lg-5″>            <?php $form = ActiveForm::begin([‘id’ => ‘form-signup’]);                <?= $form->field($model, ‘username’) ?>                <?=                $form->field($model, ’email’, [‘errorOptions’ => [‘class’ => ‘help-block’ ,’encode’ => false]])->textInput() ?>                <?= $form->field($model, ‘password’)->passwordInput() ?>                <?= $form->field($model, ‘captcha’)->widget(\yii\captcha\Captcha::classname(), [                      // configure additional widget properties here                  ]) ?>                <div class=»form-group»>                    <?= Html::submitButton(‘Signup’, [‘class’ => ‘btn btn-primary’, ‘name’ => ‘signup-button’]) ?>                </div>            <?php ActiveForm::end();        </div> | 
  Затем соответствие модели с капчей добавляется, как правило, для модели SignupForm : 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | <?php namespace frontend\models; use common\models\User; use yii\base\Model; use Yii; use yii\helpers\Html; use yii\validators\EmailValidator; /**  * Signup form  */ class SignupForm extends Model {     public $username;     public $email;     public $password;     public $captcha;     /**      * @inheritdoc      */     public function rules()     {         return [             [‘username’, ‘filter’, ‘filter’ => ‘trim’],             [‘username’, ‘required’],             [‘username’, ‘unique’, ‘targetClass’ => ‘\common\models\User’, ‘message’ => ‘This username has already been taken.’],             [‘username’, ‘string’, ‘min’ => 2, ‘max’ => 255],             [’email’, ‘filter’, ‘filter’ => ‘trim’],             [’email’, ‘required’],             [’email’, ’email’, ‘checkDNS’=>true, ‘enableIDN’=>true],             [’email’, ‘unique’, ‘targetClass’ => ‘\common\models\User’, ‘message’ => ‘This email address has already been taken.             [‘password’, ‘required’],             [‘password’, ‘string’, ‘min’ => 6],             [‘captcha’, ‘required’],             [‘captcha’, ‘captcha’],         ];     } | 
Если люди не введут правильный ответ с картинки, они не смогут зарегистрироваться. Это затрудняет автоматическую регистрацию спаммеров.
CheckDNS
  Я также хотел свести к минимуму регистрацию с поддельным адресом электронной почты.  Проверка checkDNS самом деле ищет действительную запись MX, основанную на домене адреса электронной почты: 
| 1 | [’email’, ’email’, ‘checkDNS’=>true, ‘enableIDN’=>true], | 
  Так, например, если я неправильно набрал gmail.com как gmal.com, checkDNS вернет false .  Для gmal.com нет зарегистрированной записи MX.  Точно так же нет ни одного для spambotolympics9922.com. 
В конечном счете, безопасность — это итеративный процесс. Всегда есть чем заняться.
Ограничение оскорбительных действий
Затем я хотел добавить общие ограничения на количество действий, которые могут выполнять люди, чтобы ограничить злоупотребления и не дать приложению стать громоздким.
Создание встречи
  Чтобы люди не findEmptyMeeting создавать множество пустых собраний, я создал findEmptyMeeting    который ищет пустую встречу и использует ее, когда кто-то пытается создать новую: 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | public function actionCreate()    {        // prevent creation of numerous empty meetings        $meeting_id = Meeting::findEmptyMeeting(Yii::$app->user->getId());        //echo $meeting_id;exit;        if ($meeting_id===false) {        // otherwise, create a new meeting          $model = new Meeting();          $model->owner_id= Yii::$app->user->getId();          $model->sequence_id = 0;          $model->meeting_type = 0;          $model->save();          $model->initializeMeetingSetting($model->id,$model->owner_id);          $meeting_id = $model->id;        }        $this->redirect([‘view’, ‘id’ => $meeting_id]);    } | 
Другими словами, если пользователь отправляется на создание новой встречи 1700 раз, ему всегда будет представлена первая пустая встреча, которую он создал.
Предельная частота действий
  Я также создал обычно структурированный метод withinLimit для повторного использования вокруг приложения, который может предотвратить слишком много действий за слишком короткое время.  В приведенном ниже примере проверяется, что за последний час и последний день было создано не более n заседаний: 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 | public static function withinLimit($user_id,$minutes_ago = 180) {      // how many meetings created by this user in past $minutes_ago      $cnt = Meeting::find()        ->where([‘owner_id’=>$user_id])        ->andWhere(‘created_at>’.(time()-($minutes_ago*60)))        ->count();      if ($cnt >= Meeting::NEAR_LIMIT ) {        return false;      }      // check in last DAY_LIMIT      $cnt = Meeting::find()        ->where([‘owner_id’=>$user_id])        ->andWhere(‘created_at>’.(time()-(24*3600)))        ->count();      if ($cnt >= Meeting::DAY_LIMIT ) {          return false;      }      return true;    } | 
  Каждый раз, когда кто-то пытается создать встречу, мы проверяем в withinLimit чтобы узнать, могут ли они.  Если нет, мы показываем сообщение об ошибке: 
| 1 2 3 4 5 6 | public function actionCreate()    {        if (!Meeting::withinLimit(Yii::$app->user->getId())) {          Yii::$app->getSession()->setFlash(‘error’, Yii::t(‘frontend’,’Sorry, there are limits on how quickly you can create meetings. Visit support if you need assistance.’));          return $this->redirect([‘index’]);        } | 
Ограничение количества действий
  Я также хотел ограничить общее количество действий.  Например, каждый участник встречи может добавить только семь раз даты встречи для каждой встречи.  В MeetingTime.php я установил MEETING_LIMIT , чтобы его можно было изменить позже: 
| 1 | const MEETING_LIMIT = 7; | 
  Затем MeetingTime::withinLimit() проверяет, чтобы любой пользователь предлагал не более семи раз: 
| 01 02 03 04 05 06 07 08 09 10 11 | public static function withinLimit($meeting_id) {      // how many meetingtimes added to this meeting      $cnt = MeetingTime::find()        ->where([‘meeting_id’=>$meeting_id])        ->count();        // per user limit option: ->where([‘suggested_by’=>$user_id])      if ($cnt >= MeetingTime::MEETING_LIMIT ) {        return false;      }      return true;    } | 
  Когда они идут, чтобы создать MeetingTime , метод создания контроллера проверяет пределы: 
| 1 2 3 4 5 6 | public function actionCreate($meeting_id)    {      if (!MeetingTime::withinLimit($meeting_id)) {        Yii::$app->getSession()->setFlash(‘error’, Yii::t(‘frontend’,’Sorry, you have reached the maximum number of date times per meeting. Contact support if you need additional help or want to offer feedback.’));        return $this->redirect([‘/meeting/view’, ‘id’ => $meeting_id]);      } | 
Обеспечение работы CRON
  Наконец, сегодня я хотел защитить доступ к удаленным рабочим местам cron.  Есть несколько интересных подходов, описанных на веб-сайтах.  Сейчас я проверяю, что $_SERVER['REMOTE_ADDR'] (запрашивающий IP-адрес) — это тот же сервер, что и $_SERVER['SERVER_ADDR'] , локальный IP-адрес.  $_SERVER['REMOTE_ADDR'] безопасно использовать для безопасности — другими словами, я читал, что его нельзя подделать. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | // only cron jobs and admins can run this controller’s actions    public function beforeAction($action)    {      // your custom code here, if you want the code to run before action filters,      // which are triggered on the [[EVENT_BEFORE_ACTION]] event, eg PageCache or AccessControl      if (!parent::beforeAction($action)) {          return false;      }      // other custom code here      if (( $_SERVER[‘REMOTE_ADDR’] == $_SERVER[‘SERVER_ADDR’] ) ||          (!\Yii::$app->user->isGuest && \common\models\User::findOne(Yii::$app->user->getId())->isAdmin()))       {         return true;       }      return false;    } | 
Для собственного тестирования я также разрешаю вошедшему в систему администратору запускать задания cron.
В конце концов, я также могу добавить пароль в мои задания cron и переместить их в операции командной строки.
  Заглядывая вперед 
За последние два эпизода я выполнил множество улучшений безопасности, но еще многое предстоит сделать. В моем списке есть более глубокий обзор безопасности доступа, особенно через AJAX, отслеживание и блокировка IP-адресов, а также тщательная фильтрация всего пользовательского ввода.
Опять чего вы ждете? Запланируйте свою первую встречу и поделитесь своим мнением в комментариях. Буду также признателен за ваши комментарии по вопросам безопасности.
Как всегда, вы можете посмотреть предстоящие уроки в серии « Построение стартапа с помощью PHP» или подписаться на меня @reifman . Есть еще несколько важных функций.