Spanish (Español) translation by Rafael Chavarría (you can also view the original English article)



Este tutorial es parte de la serie Construyendo Tu Startup Con PHP en Envato Tuts+. En esta serie, te estaré guiando a través de lanzar una startup desde concepto a realidad usando mi aplicación Planificador de Reuniones como un ejemplo de la vida real. Cada paso a lo largo del camino, lanzaré el código de Planificador de Reuniones como ejemplos de código abierto de los cuales puedes aprender. También abordaré temas de negocios relacionados con startup conforme se presenten.
En el episodio anterior, cubrí primariamente seguridad de servidor web y control de acceso. En el episodio de hoy, discutiré garantías adicionales que agregué a Planificador de Reuniones. Ya que todo el código está escrito en el Framework Yii2 para PHP, pude sacar ventaja del framework para un número de estas fortificaciones. Si te gustaría aprender más acerca de Yii2, revisa nuestra serie paralela Programando con Yii2.
Puedes probar nuestro Planificador de Reuniones ahora mismo programando tu primera reunión. Siéntete libre de publicar retroalimentación sobre tu experiencia en los comentarios de abajo. También estoy abierto a ideas de nuevas características y sugerencias de temas para futuros tutoriales.
Construyendo Seguridad Incrementada.
Implementar los varios niveles de seguridad para Planificador de Reuniones tomará varios episodios. Ahora que el servidor está configurado de manera más robusta, quiero guiarte a través de las áreas de seguridad para el código.
Protegiendo Llaves y Códigos
Obviamente, es importante para mantener llaves de autentificación lejos de hackers, pero también es bastante fácil publicarlos en GitHub. Se cuentan historias de registros accidentales de archivos con una contraseña de servicio o clave de API.
Para prevenir esto en Yii, mantengo un archivo .ini externo fuera del árbol de código. Esto se carga en la parte superior de /frontend/config/main.php y es usado para cualquier componente de configuración que sea necesario:
1 |
<?php
|
2 |
$config = parse_ini_file('/var/secure/meetme.ini', true); |
3 |
|
4 |
$params = array_merge( |
5 |
require(__DIR__ . '/../../common/config/params.php'), |
6 |
require(__DIR__ . '/../../common/config/params-local.php'), |
7 |
require(__DIR__ . '/params.php'), |
8 |
require(__DIR__ . '/params-local.php') |
9 |
);
|
10 |
|
11 |
return [ |
12 |
'id' => 'mp-frontend', |
13 |
'name' => 'Meeting Planner', |
14 |
'basePath' => dirname(__DIR__), |
15 |
'bootstrap' => ['log'], |
16 |
'controllerNamespace' => 'frontend\controllers', |
17 |
'components' => [ |
18 |
'authClientCollection' => [ |
19 |
'class' => 'yii\authclient\Collection', |
20 |
'clients' => [ |
21 |
'facebook' => [ |
22 |
'class' => 'yii\authclient\clients\Facebook', |
23 |
'clientId' => $config['oauth_fb_id'], |
24 |
'clientSecret' => $config['oauth_fb_secret'], |
25 |
],
|
En el ejemplo de arriba, puedes ver los secretos de la API de Facebook desde el archivo de inicialización.
El formato de inicialización es bastante simple:
1 |
mysql_host="localhost" |
2 |
mysql_un="xxxxxxxxxxxxxxxxxxx" |
3 |
mysql_db="xxxxxxxxxxxxxxxxxxx" |
4 |
mysql_pwd="xxxxxxxxxxxxxxxxxxx" |
5 |
mailgun_user = "xxxxxxxxxxxxxxxxxxx@meetingplanner.io" |
6 |
mailgun_pwd = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" |
7 |
mailgun_api_key="key-9p-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" |
8 |
mailgun_api_url="https://api.mailgun.net/v2" |
9 |
mailgun_public_key="pubkey-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" |
10 |
oauth_fb_id="1xxxxxxxxxxxxxxxxxxx3" |
11 |
oauth_fb_secret="bcxxxxxxxxxxxxxxxxxxxda" |
Yii2 te alienta a que coloques algunos de estos ajustes en el directorio /enviroments, especialmente cuando los ajusten varían entre desarrollo y producción.
Así pues, es importante que tu archivo .gitignore excluya las versiones locales de estos archivos:
1 |
#local environment files
|
2 |
/environments/prod/common/config/main-local.php |
3 |
/environments/prod/frontend/config/main-local.php |
4 |
/frontend/config/params-local.php |
5 |
/frontend/config/main-local.php |
Aquí hay un ejemplo de uno de mis archivos de parámetro, /frontend/config/params-local.php:
1 |
<?php
|
2 |
return [ |
3 |
'ga' => 'UA-xxxxxxxxxx-12', |
4 |
'urlPrefix' => '', |
5 |
'google_maps_key' => 'AIzzzzzz1111222222xxxxxxQ', |
6 |
];
|
Podría probablemente pasar incluso más tiempo organizando estos mejor.
Bloqueando Inicios de Sesión Incorrectos



Para la liberación alpha, mandé actualizaciones en olas. Y, en las etapas tempranas de Planificador de Reuniones, hubo un número mayor de correos incorrectos de lo que esperaba. Mailgun facilitó identificar los rebotes y fallas:
1 |
$badEmails=[ '', 'test2@gmail.com', '1111@gmail.com', 'qwerty@gmail.com', |
2 |
'amjadiqbalkhanniazi@gmail.com', 'admin@admin.com', 'rhizalpatra@fellow.lpkia.ac.id', 'tm@archi.com', |
3 |
'test@test.com', 'web@yahoo.fr', 'a@a. a', 'ailaa@aa.com', 'be@yahoo.fr', 'vico@gmail.com', |
4 |
'nobu@gmail.com', 'a@gmail.com', 'ct@gmail.com', 'sanjaydk@projectdemo. biz', 'trial@gmail.com', |
5 |
'varlog255q@hotmail.com', 'baah@baah.com', 'minhvnn1@gmail.com', 'test@gmail.com', |
6 |
'test@mediabite.co.uk', 'ddd@c. hu', 'ddd@ymail.com', 'a. chetan@saisoftex.com', 'user02@local.com', |
7 |
'Imrky4@gmail.com', 'robomadybu@hotmail.com', 'mike@mike. mike', 'abcd@gmail.com', |
8 |
'azazaz@azazaza.com', 'mama@mama.mn', 'qweqwe@qwe. qwe', 'testere@wp.pl', 'kaze@hotmail.com', |
9 |
'test@usertest.fr', 'demodemo@demo.com', 'qqq@dd.gh', 'gnfbb@h. vo', 'admin@admin123.com', |
10 |
'testsir@testsir.com', 'oi. hd@yeah1.vn', 'loi. hd@yeah1.vn', 'test@email.com', 'salom@salom.com', |
11 |
'ar@yahoo.com', 'lex@gmail.com', 'Tester1234@gmail.com', 'mantaf@mail.com', 'aaa@aaa.com', |
12 |
'oeui@gmail.com', 'risitesh. biswal14@yahoo.com', 'ttt@wp.pl', 'nnn@nnn.net', 'nnn2@nnn.net', |
13 |
'ana@gmail.com', 'asdf@yahoo.com', 'noom@gmail.com', 'jomon@example.com', 'asdfasdf@yahoo.com', |
14 |
'admin@yahoo.com', 'abinubli@mail.com', 'tes@tes.com', 'asdasdr@asd.com', 'something@some.com', |
15 |
'ademin@example.com', 'd@dd.com', 'robo@gmail.com', 'toto@titi.com', 'fesfe@fseff. fes', |
16 |
'master@wpthemeslist.com', 'teste@teste.com', 'barny182@hotmail.com', 'test@admin.com', |
17 |
'billtian@test.com', 'Test@goggle.ca', 'jm@gmail.com', 'john-panin@qip.ru', 'loslos@loslos.com', |
18 |
'ghfhf@jhgjgjk.com', 'lol@lol.com', 'tester1@gmail.com', 'g0952180828@gmail.com', 'testim@testim.com', |
19 |
'mnml.name@gmail.com', 'endri. azizi. 92@gmail.com', '123123@gmail.com', 'myfriend@gmai.com', |
20 |
'geraldo_1989@hotmail.com', 'rob. test. 999@gmail.com', 'j@c. com', 'Agung. andika@mhs.uinjkt.ac.id', |
21 |
'W3test@ya.ru', 'user@ya.ru', 'ed@ed. fl', 'ed@ed.es', ]; |
La mayoría de estos son probables de la brecha de tiempo cuando Planificador de Reuniones era nuevo y estaba sin usar--durante mi tratamiento y cirugía de tumor cerebral.
Más recientemente, agregando estos inicios de sesión sociales, he hecho el registro para Planificador de Reuniones bastante sencillo, pero los registros de spam aún son posibles. Quise hacer más difícil para la gente que se registrara con emails incorrectos.
Afortunadamente, Yii ofrece un par de características que soporta esto.
Captcha
Yii2 ahora ofrece un captcha integrado. Así que, cualquiera que se registre con el método de la vieja escuela de email y contraseña tiene que ingresar un captcha. Puedes ver el campo captcha
abajo:
1 |
<p>Or, fill out the following fields to register manually:</p> |
2 |
<div class="col-lg-5"> |
3 |
<?php $form = ActiveForm::begin(['id' => 'form-signup']); ?> |
4 |
<?= $form->field($model, 'username') ?> |
5 |
<?=
|
6 |
$form->field($model, 'email', ['errorOptions' => ['class' => 'help-block' ,'encode' => false]])->textInput() ?> |
7 |
<?= $form->field($model, 'password')->passwordInput() ?> |
8 |
<?= $form->field($model, 'captcha')->widget(\yii\captcha\Captcha::classname(), [ |
9 |
// configure additional widget properties here
|
10 |
]) ?> |
11 |
<div class="form-group"> |
12 |
<?= Html::submitButton('Signup', ['class' => 'btn btn-primary', 'name' => 'signup-button']) ?> |
13 |
</div>
|
14 |
<?php ActiveForm::end(); ?> |
15 |
</div>
|
Entonces, en conformidad con el captcha es agregada una regla para el modelo SignupForm
:
1 |
<?php
|
2 |
namespace frontend\models; |
3 |
|
4 |
use common\models\User; |
5 |
use yii\base\Model; |
6 |
use Yii; |
7 |
use yii\helpers\Html; |
8 |
use yii\validators\EmailValidator; |
9 |
|
10 |
/**
|
11 |
* Signup form
|
12 |
*/
|
13 |
class SignupForm extends Model |
14 |
{
|
15 |
public $username; |
16 |
public $email; |
17 |
public $password; |
18 |
public $captcha; |
19 |
|
20 |
/**
|
21 |
* @inheritdoc
|
22 |
*/
|
23 |
public function rules() |
24 |
{
|
25 |
return [ |
26 |
['username', 'filter', 'filter' => 'trim'], |
27 |
['username', 'required'], |
28 |
['username', 'unique', 'targetClass' => '\common\models\User', 'message' => 'This username has already been taken.'], |
29 |
['username', 'string', 'min' => 2, 'max' => 255], |
30 |
['email', 'filter', 'filter' => 'trim'], |
31 |
['email', 'required'], |
32 |
['email', 'email', 'checkDNS'=>true, 'enableIDN'=>true], |
33 |
['email', 'unique', 'targetClass' => '\common\models\User', 'message' => 'This email address has already been taken. '.Html::a('Looking for your password?', ['site/request-password-reset'])], |
34 |
['password', 'required'], |
35 |
['password', 'string', 'min' => 6], |
36 |
['captcha', 'required'], |
37 |
['captcha', 'captcha'], |
38 |
];
|
39 |
}
|
Si la gente no quiere ingresar la respuesta captcha correcta, no pueden registrarse. Esto hace el registro automatizado más difícil para spammers.
CheckDNS
También quise minimizar el registro con cuentas de correo falsas. La validación checkDNS
de Yii de hecho busca un registro MX válido basado en el dominio de la dirección de email:
1 |
['email', 'email', 'checkDNS'=>true, 'enableIDN'=>true], |
Así qué, por ejemplo, si me equivocara al teclear gmail.com como gmal.com, checkDNS
regresaría false
. No hay registro MX para gmal.com. De manera similar, no hay ninguno para pambotolympics9922.com.
Por último, la seguridad es un proceso iterativo. Siempre hay más por hacer.
Limitando Acciones Abusivas.
Después, quise agregar límites comunes al número de acciones que la gente podría hacer, para limitar abuso y prevenir a la aplicación de volverse pesada.
Creación de Reunión
Para prevenir a la gente de crear muchas reuniones vacías, creé un findEmptyMeeting
que busca una reunión vacía y la re-usa cuando alguien intenta crear una nueva:
1 |
public function actionCreate() |
2 |
{
|
3 |
// prevent creation of numerous empty meetings
|
4 |
$meeting_id = Meeting::findEmptyMeeting(Yii::$app->user->getId()); |
5 |
//echo $meeting_id;exit;
|
6 |
if ($meeting_id===false) { |
7 |
// otherwise, create a new meeting
|
8 |
$model = new Meeting(); |
9 |
$model->owner_id= Yii::$app->user->getId(); |
10 |
$model->sequence_id = 0; |
11 |
$model->meeting_type = 0; |
12 |
$model->save(); |
13 |
$model->initializeMeetingSetting($model->id,$model->owner_id); |
14 |
$meeting_id = $model->id; |
15 |
}
|
16 |
$this->redirect(['view', 'id' => $meeting_id]); |
17 |
}
|
En otras palabras, si un usuario va a crear una nueva reunión 1,700 veces, siempre se les presentará la primer reunión vacía que crearon.
Limitando Frecuencia de Acciones
También creé un método comúnmente estructurado withinLimit
para re-usar en la aplicación lo cuál previene demasiadas acciones en un corto tiempo. El ejemplo de abajo revisa que no haya más de n número de reuniones que se hayan creado en la última hora y último día:
1 |
public static function withinLimit($user_id,$minutes_ago = 180) { |
2 |
// how many meetings created by this user in past $minutes_ago
|
3 |
$cnt = Meeting::find() |
4 |
->where(['owner_id'=>$user_id]) |
5 |
->andWhere('created_at>'.(time()-($minutes_ago*60))) |
6 |
->count(); |
7 |
if ($cnt >= Meeting::NEAR_LIMIT ) { |
8 |
return false; |
9 |
}
|
10 |
// check in last DAY_LIMIT
|
11 |
$cnt = Meeting::find() |
12 |
->where(['owner_id'=>$user_id]) |
13 |
->andWhere('created_at>'.(time()-(24*3600))) |
14 |
->count(); |
15 |
if ($cnt >= Meeting::DAY_LIMIT ) { |
16 |
return false; |
17 |
}
|
18 |
return true; |
19 |
}
|
Cualquier vez que alguien intente crear una reunión, revisamos withinLimit
para ver si pueden. Si no, mostramos el mensaje de error flash
:
1 |
public function actionCreate() |
2 |
{
|
3 |
if (!Meeting::withinLimit(Yii::$app->user->getId())) { |
4 |
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.')); |
5 |
return $this->redirect(['index']); |
6 |
}
|
7 |
Limitando el Número de Acciones
También quise limitar el número general de acciones. Por ejemplo, cada participante de reunión puede solo agregar siete fechas de reunión por reunión. Dentro de MeetingTime.php, establezco MEETING_LIMIT
, de manera que pueda cambiar después:
1 |
const MEETING_LIMIT = 7; |
Después, MeetingTime::withinLimit()
revisa para asegurar que no más de siete horarios hayan sido sugeridos por cualquier usuario:
1 |
public static function withinLimit($meeting_id) { |
2 |
// how many meetingtimes added to this meeting
|
3 |
$cnt = MeetingTime::find() |
4 |
->where(['meeting_id'=>$meeting_id]) |
5 |
->count(); |
6 |
// per user limit option: ->where(['suggested_by'=>$user_id])
|
7 |
if ($cnt >= MeetingTime::MEETING_LIMIT ) { |
8 |
return false; |
9 |
}
|
10 |
return true; |
11 |
}
|
Cuando intentan crear un MeetingTime
, el controlador crea método que revisa los límites.
1 |
public function actionCreate($meeting_id) |
2 |
{
|
3 |
if (!MeetingTime::withinLimit($meeting_id)) { |
4 |
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.')); |
5 |
return $this->redirect(['/meeting/view', 'id' => $meeting_id]); |
6 |
}
|
7 |
Asegurando trabajos CRON
Finalmente hoy, quise asegurar acceso a trabajos cron remotos. Hay algunas aproximaciones interesantes descritas en las interwebs. Por ahora, estoy revisando que el $_SERVER['REMOTE_ADDR']
(la dirección IP de petición) es el mismo servidor que el hosting $_SERVER['SERVER_ADDR']
, la dirección IP local. $_SERVER['REMOTE_ADDR']
es seguro de usar para seguridad--en otras palabras, he leído que no puede ser falsificado.
1 |
// only cron jobs and admins can run this controller's actions
|
2 |
public function beforeAction($action) |
3 |
{
|
4 |
// your custom code here, if you want the code to run before action filters,
|
5 |
// which are triggered on the [[EVENT_BEFORE_ACTION]] event, e.g. PageCache or AccessControl
|
6 |
if (!parent::beforeAction($action)) { |
7 |
return false; |
8 |
}
|
9 |
// other custom code here
|
10 |
if (( $_SERVER['REMOTE_ADDR'] == $_SERVER['SERVER_ADDR'] ) || |
11 |
(!\Yii::$app->user->isGuest && \common\models\User::findOne(Yii::$app->user->getId())->isAdmin())) |
12 |
{
|
13 |
return true; |
14 |
}
|
15 |
return false; // or false to not run the action |
16 |
}
|
Para mis propias pruebas, también permito un administrador con sesión iniciada para ejecutar trabajos cron.
Eventualmente, también agregaré una contraseña a mis trabajos cron y los muevo a las operaciones de línea de comando.
Mirando Hacia el Futuro
He logrado muchas mejoras de seguridad a lo largo de los dos episodios anteriores, pero aún hay más por hacer. En mi lista corta hay una revisión más a fondo de seguridad de acceso, especialmente vía AJAX, seguimiento de bloqueo de direcciones IP y filtrando cuidadosamente toda la entrada del usuario.
De nuevo, ¿qué estás esperando? Programa tu primera reunión, y comparte tu retroalimentación en los comentarios. También apreciaría tus comentarios en temas de seguridad.
Como siempre, puedes esperar los próximos tutoriales en la serie Construyendo Tu Startup Con PHP o seguirme en @reifman. Hay algunas cuantas grandes características más por venir.