If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeNo vamos a hablar específicamente de la seguridad en este tutorial; lo haremos en nuestro próximo curso y le prestaremos la debida atención. Pero, incluso olvidando la seguridad y el inicio de sesión y todo eso, es muy probable que tu API tenga algún concepto de "usuarios". En nuestro caso, un "usuario" publicará un listado de quesos y se convertirá en su "propietario". Y puede que después, para comprar un listado de quesos, un usuario envíe un mensaje a otro usuario. Es hora de llevar nuestra aplicación al siguiente nivel creando esa entidad.
Y aunque te diga que no pienses en la seguridad, en lugar de crear la entidad usuario con make:entity
como haría normalmente, voy a utilizar make:user
,
php bin/console make:user
Sí, esto configurará algunas cosas relacionadas con la seguridad... pero nada que vayamos a utilizar todavía. Mira la parte 2 de esta serie para ver todas esas cosas.
En cualquier caso, llama a la clase User
, y quiero almacenar los usuarios en la base de datos. Para el nombre único de visualización, voy a hacer que los usuarios se registren a través del correo electrónico, así que usa eso. Y entonces:
¿Esta aplicación necesita hacer un hash o comprobar las contraseñas de los usuarios?
Hablaremos más de esto en el tutorial de seguridad. Pero si los usuarios van a tener que iniciar sesión en tu sitio a través de una contraseña y tu aplicación va a ser la responsable de comprobar si esa contraseña es válida -no te limitas a enviar la contraseña a algún otro servicio para que la verifique-, entonces responde que sí. No importa si el usuario va a introducir la contraseña a través de una aplicación de iPhone que habla con tu API o a través de un formulario de inicio de sesión: responde que sí si tu aplicación es responsable de gestionar las contraseñas de los usuarios.
Utilizaré el hashtag de contraseñas Argon2i. Pero si no ves esta pregunta, ¡no pasa nada! A partir de Symfony 4.3, no es necesario que elijas un algoritmo de hashing de contraseñas porque Symfony puede elegir el mejor disponible de forma automática. Algo realmente genial.
¡Vamos a ver qué hace esto! Me alegra decir que... ¡no mucho! En primer lugar, ahora tenemos una entidadUser
. Y... no tiene nada de especial: tiene algunos métodos adicionales relacionados con la seguridad, como getRoles()
, getPassword()
, getSalt()
y eraseCredentials()
, pero no afectarán a lo que estamos haciendo. En su mayor parte, tenemos una entidad normal y aburrida con $id
, $email
, una propiedad de matriz $roles
, y $password
, que finalmente almacenará la contraseña con hash.
... lines 1 - 2 | |
namespace App\Entity; | |
use Doctrine\ORM\Mapping as ORM; | |
use Symfony\Component\Security\Core\User\UserInterface; | |
/** | |
* @ORM\Entity(repositoryClass="App\Repository\UserRepository") | |
*/ | |
class User implements UserInterface | |
{ | |
/** | |
* @ORM\Id() | |
* @ORM\GeneratedValue() | |
* @ORM\Column(type="integer") | |
*/ | |
private $id; | |
/** | |
* @ORM\Column(type="string", length=180, unique=true) | |
*/ | |
private $email; | |
/** | |
* @ORM\Column(type="json") | |
*/ | |
private $roles = []; | |
/** | |
* @var string The hashed password | |
* @ORM\Column(type="string") | |
*/ | |
private $password; | |
... lines 35 - 112 | |
} |
Esto también ha creado la entidad normal UserRepository
y ha hecho un par de cambios ensecurity.yaml
: ha creado encoders
- esto podría decir auto
para ti, gracias a la nueva característica de Symfony 4.3 - y el proveedor de usuarios. Cosas de las que hablaremos más adelante. Así que... olvida que están aquí y en su lugar di... ¡vaya! ¡Tenemos una entidadUser
!
security: | |
encoders: | |
App\Entity\User: | |
algorithm: argon2i | |
... lines 5 - 6 | |
providers: | |
# used to reload user from session & other features (e.g. switch_user) | |
app_user_provider: | |
entity: | |
class: App\Entity\User | |
property: email | |
... lines 13 - 33 |
... lines 1 - 2 | |
namespace App\Repository; | |
use App\Entity\User; | |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | |
use Symfony\Bridge\Doctrine\RegistryInterface; | |
/** | |
* @method User|null find($id, $lockMode = null, $lockVersion = null) | |
* @method User|null findOneBy(array $criteria, array $orderBy = null) | |
* @method User[] findAll() | |
* @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) | |
*/ | |
class UserRepository extends ServiceEntityRepository | |
{ | |
public function __construct(RegistryInterface $registry) | |
{ | |
parent::__construct($registry, User::class); | |
} | |
... lines 21 - 49 | |
} |
Gracias al comando, la entidad tiene una propiedad email
, y pienso hacer que los usuarios se registren utilizando eso. Pero también quiero que cada usuario tenga un "nombre de usuario" que podamos mostrar públicamente. Vamos a añadirlo: busca tu terminal y ejecuta
php bin/console make:entity
Actualiza User
y añade username
como string
, 255, no anulable en la base de datos, y pulsa intro para terminar.
Ahora abre User
... y desplázate hasta getUsername()
. El comando make:user
generó esto y devolvió $this->email
... porque eso es lo que elegí como mi nombre "para mostrar" por seguridad. Ahora que realmente tenemos un campo de nombre de usuario, devuelve $this->username
.
... lines 1 - 10 | |
class User implements UserInterface | |
{ | |
... lines 13 - 35 | |
/** | |
* @ORM\Column(type="string", length=255) | |
*/ | |
private $username; | |
... lines 40 - 62 | |
public function getUsername(): string | |
{ | |
return (string) $this->username; | |
} | |
... lines 67 - 124 | |
} |
Ah, y mientras hacemos esta clase, simplemente, increíble, el comando make:user
sabía que email
debía ser único, así que añadió unique=true
. Añadamos también eso a username
: unique=true
.
... lines 1 - 35 | |
/** | |
* @ORM\Column(type="string", length=255, unique=true) | |
*/ | |
private $username; | |
... lines 40 - 126 |
¡Esa es una bonita entidad! Vamos a sincronizar nuestra base de datos ejecutando:
php bin/console make:migration
Muévete... y vuelve a comprobar el SQL: CREATE TABLE user
- ¡se ve bien!
... lines 1 - 12 | |
final class Version20190509185722 extends AbstractMigration | |
{ | |
... lines 15 - 19 | |
public function up(Schema $schema) : void | |
{ | |
// this up() migration is auto-generated, please modify it to your needs | |
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); | |
$this->addSql('CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, username VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), UNIQUE INDEX UNIQ_8D93D649F85E0677 (username), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); | |
} | |
... lines 27 - 34 | |
} |
Ejecútalo con:
php bin/console doctrine:migration:migrate
¡Perfecto! Tenemos una nueva y preciosa entidad Doctrine... pero en lo que respecta a la Plataforma API, seguimos teniendo sólo un recurso API: CheeseListing
.
Lo siguiente: vamos a exponer User
como un Recurso API y a utilizar todos nuestros nuevos conocimientos para perfeccionar ese nuevo recurso en... unos 5 minutos.
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^2.1", // v2.4.3
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // 1.10.2
"doctrine/doctrine-bundle": "^1.6", // 1.11.2
"doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
"doctrine/orm": "^2.4.5", // v2.7.2
"nelmio/cors-bundle": "^1.5", // 1.5.5
"nesbot/carbon": "^2.17", // 2.19.2
"phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
"symfony/asset": "4.2.*|4.3.*|4.4.*", // v4.3.11
"symfony/console": "4.2.*", // v4.2.12
"symfony/dotenv": "4.2.*", // v4.2.12
"symfony/expression-language": "4.2.*|4.3.*|4.4.*", // v4.3.11
"symfony/flex": "^1.1", // v1.17.6
"symfony/framework-bundle": "4.2.*", // v4.2.12
"symfony/security-bundle": "4.2.*|4.3.*", // v4.3.3
"symfony/twig-bundle": "4.2.*|4.3.*", // v4.2.12
"symfony/validator": "4.2.*|4.3.*", // v4.3.11
"symfony/yaml": "4.2.*" // v4.2.12
},
"require-dev": {
"symfony/maker-bundle": "^1.11", // v1.11.6
"symfony/stopwatch": "4.2.*|4.3.*", // v4.2.9
"symfony/web-profiler-bundle": "4.2.*|4.3.*" // v4.2.9
}
}