Herramientas de usuario

Herramientas del sitio


cursos:yii2:restapi:servidor_rest_api

Servidor REST API con Yii2

Podemos crear fácilmente un servidor REST API con Yii2, que puede hacer las mismas funciones que node.js, aprovechando parte de una aplicación que ya tengamos con Yii2 o partiendo de cero.

Los pasos son:

Habilitar URL's limpias y configurar reglas

En este documento se explica como habilitar las URL's limpias. Para el caso del servidor REST API debemos asignar la propiedad enableStrictParsing de urlManager a true, y definir la propiedad rules,que especificarán la relación entre rutas y controladore/acciones. Ver este documento. Si vamos a definir muchas reglas, es cómodo separar esta información en un fichero aparte y cargarlo en config/web.php:

 'urlManager' => [
        'enablePrettyUrl' => true,
        'enableStrictParsing' => true,
        'showScriptName' => false,
        'rules' => require(__DIR__ . '/rules.php')
        ]

Y así, en rules.php ponemos las reglas:

<?php
return  [
['class' => 'yii\rest\UrlRule',
    'pluralize'=>false,
    'controller' => ['CONT1','CONT2','...'],
],
['class' => 'yii\rest\UrlRule',
    'controller' => ['user'],
    'pluralize'=>false,
    'extraPatterns'=>['POST authenticate'=>'authenticate']
]
 
];
// CONT1,CONT2...son los controladores de nuestra aplicación: usuarios, articulos, tienda, eventos, etc..

Es habitual que en los verbos POST, PUT y PATCH, los datos del objeto a actualizar se envíen en formato JSON, como es el caso de ANGULAR JS. Para que Yii pueda procesarlos correctamente, tenemos que activar el parser de JSON en el componente Request→parsers:

 'request' => [
    'cookieValidationKey' => '....',
    'parsers' => [
        'application/json' => 'yii\web\JsonParser',
    ]
 ],

Controladores

Los controladores para servicios REST API derivan de la clase yii\rest\ActiveController. Para crear un controlador que defina los servicios básicos (GET, GET id,POST, PUT, PATCH, DELETE), basta con especificar el modelo con el que va a trabajar el controlador

namespace app\controllers;
use yii\rest\ActiveController;
 
class UsuariosController extends ActiveController
{
    public $modelClass = 'app\models\Usuarios';
}

Con esto, ya podemos hacer pruebas de solicitar datos a nuestro servidor. Es conveniente utilizar algún programa para poder hacer tests de nuestro servidor. Uno muy conveniente es POSTMAN, que tiene versión de escritorio y también puede instalarse como plugin de Chrome. Si accedemos a la ruta: http://SERVIDOR/APLICACION/web/usuarios, debería salir una lista de usuarios, en formato JSON. (Si lo hacemos en el navegador, se mostrará en XML, porque Yii detecta que se está accediendo de esa forma).

Selección de datos a Recibir

En el servidor es posible definir qué atributos queremos enviar en las peticiones GET, y que atributos puede solicitar el usuario bajo demanda. Para ello, utilizaremos los métodos fields() y extrafields() en los modelos implicados. El método fields() devuelve un array con las propiedades que se devolverán por defecto, si no se especifican en la petición

public function fields(){
    return ['id','nombre','estado','email','rol'];
}

Por defecto, fields() devuelve todos los atributos del modelo. Podemos eliminar o añadir atributos a partir de los predefinidos en la clase base:

public function fields(){
    $fields=array_diff(parent::fields(),['password','authkey']); //Nunca devuelve password ni authkey
    return array_merge($fields,['estadoText','comentarios']); //Añade estadoText y el array de comentarios definidos mediante una relación con el modelo Comentarios
}

El cliente puede especificar los atributos que desea que se devuelvan, que han de estar en la lista generada en el modelo mediante fields(). Para ello, se añade el parámetro ?fields=propiedades en la URL:

http://SERVIDOR/usuarios/7?fields=id,nombre,email // Únicamente se reciben estas 3 propiedades

El método extrafields es similar a fields(), y devuelve las propiedades opcionales que se pueden obtener, mediante el parámetro ?expand=propiedades en la URL

public function extrafields(){
    return ['grupos','apuntes','eventos','saldo']); 
}

Así, para obtener los apuntes del usuario:

http://SERVIDOR/usuarios/7?extrafields=apuntes // Devuelve el usuario junto con sus apuntes

Autenticación

Podemos obligar a que en determinados controladores o peticiones sea necesario que el usuario se identifique. A diferencia de las aplicaciones estándar, donde se suelente utilizar las sesiones de usuario ($_SESSION de php), en las aplicaciones Rest API se suele enviar en cada petición lo necesario para que el servidor identifique al usuario. Hay diversas formas de hacerlo.Ver documentación. Una solución típica es implementar una acción de autenticación con usuario y contraseña, que devuelve un token (cadena aleatoria asociada a cada usuario), y en las sucesivas peticiones se envía este token en las HEADERS de la petición http para identificar al usuario. (Bearer token). Para ello,los pasos a seguir son: En la configuraciób de rules de urlManager (rules.php, o web.php si no utilizamos un fichero aparte), definimos un verbo particular para esta acción

'rules'=>....
 
['class' => 'yii\rest\UrlRule',
    'controller' => ['user'],
    'pluralize'=>false,
    'extraPatterns'=>['POST authenticate'=>'authenticate',
            'OPTIONS authenticate'=>'authenticate',
            ]
],
...

Creamos un controlador UserController que contiene la acción de autenticación, utilizando el modelo Usuario:

namespace app\controllers;
use yii\rest\Controller;
 
class UserController extends Controller
{
    public $modelClass = 'app\models\Usuarios';
 
  public function actionAuthenticate(){
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
      // Si se envían los datos en formato raw dentro de la petición http, se recogen así:
      $params=json_decode(file_get_contents("php://input"), false);
      @$username=$params->username;
      @$password=$params->password;
      // Si se envían los datos de la forma habitual (form-data), se reciben en $_POST:
      //$username=$_POST['username'];
      //$password=$_POST['password' ];
 
      if($u=\app\models\Usuarios::findOne(['usuario'=>$username]))
          if($u->password==md5($password)) {//o crypt, según esté en la BD
 
              return ['token'=>$u->token,'id'=>$u->id,'nombre'=>$u->nombre];
          }
 
      return ['error'=>'Usuario incorrecto. '.$username];
    }
  }

En la tabla de usuarios necesitaremos un campo 'token', asociado a cada usuario, que podemos generar de forma aleatoria en el momento del alta del mismo. Este dato es el que se devuelve en la autenticación y se empleará en las sucesivas peticiones para identificar al usuario.

El modelo Usuarios ha de implementar UserIdentityInterface, tal como se detalla aquí Por defecto, Yii utiliza el modelo User para la autenticación. Si lo cambiamos por Usuarios, debemos modificar la configuración de components en web.php:

  'components' => [
    ...
    'user' => [
            'identityClass' => 'app\models\Usuarios',
            'enableAutoLogin' => true,
        ],
    ...

En el modelo Usuarios implementaremos el método que localiza al usuario a partir del token:

public static function findIdentityByAccessToken($token, $type = null) {
                return self::findOne(['token' => $token]);
 
        }

En las acciones donde queramos que el usuario tenga que identificarse, activaremos el behavior authenticator en el método behaviors() del controlador.

...
use yii\filters\auth\HttpBearerAuth;
 
public function behaviors() {
       $behaviors = parent::behaviors();
            ...
       $behaviors['authenticator'] = [
          'class' => HttpBearerAuth::className(),
          'except' => ['accion1', 'accion2'],
       ];
       return $behaviors;
}

accion1 y accion2 serían acciones que no necesitan autenticación. (Por ejemplo, en el controlador user, las acciones options y authenticate no deben requerir autenticación.

Desde la aplicación cliente, para solicitar el token haremos un petición POST a SERVIDOR/user/authenticate, pasándole como datos username y password. Si es correcto, devolverá un json con el token:

{id:"2",nombre:"carlos...",token:"3424jgasdoasdl234lasdasdaasdkhf"}

Si ejecutamos una acción protegida por token, recibiremos un error “Unauthorized access”. En las peticiones protegidas, el token se envía en los Headers de la petición http (o https) de la forma

Bearer 3424jgasdoasdl234lasdasdaasdkhf

Personalización de acciones

Si queremos personalizar una acción sobreescribiremos el método actions del controlador. Podemos eliminar acciones , añadir o modificar las predefinidas. Por ejemplo, si queremos que un usuario solamente pueda acceder a información suya respecto a la tabla Apuntes:

namespace app\controllers;
use Yii;
use yii\rest\ActiveController;
use yii\data\ActiveDataProvider;
use app\models\Apuntes;
 
class ApuntesController extends ActiveController
{
    public $modelClass = 'app\models\Apuntes';
 
    public function actions() {
            $actions = parent::actions();
            //Eliminamos acciones de crear y eliminar apuntes. Eliminamos update para personalizarla
            unset($actions['delete'], $actions['create'],$actions['update']);
            // Redefinimos el método que prepara los datos en el index
            $actions['index']['prepareDataProvider'] = [$this, 'indexProvider'];
            return $actions;
    }
 
    public function indexProvider() {
            $uid=Yii::$app->user->identity->id;
            return new ActiveDataProvider([
                'query' => Apuntes::find()->where('usuarios_id='.$uid )->orderBy('id')
            ]);
    }
    public function actionUpdate($id){
       // Hacemos lo queramos y devolvemos información con return (un array, un objeto...)
        $uid=Yii::$app->user->identity->id;
        $model=Apuntes::findOne($id);
        if(!$model) {//No existe
            throw new NotFoundHttpException('No existe ese apunte');
        } else {
            if($uid!=$model->usuarios_id) //No es mío
                throw new NotFoundHttpException('Acceso no permitido');
 
            $model->load(Yii::$app->getRequest()->getBodyParams(), '');
            if ($model->save()) {
                $response = Yii::$app->getResponse();
                $response->setStatusCode(201);
            }
            return $model;
        }
    }
 
}

También podemos añadir nuevas acciones. Para ello, hay que incluirlas en rules.php, incluyendo también el métod OPTIONS en el caso de que la acción vaya por GET. Por ejemplo, para devolver datos estadísticos de entradas mediante una acción llamada entradas/estadistica:

...
['class' => 'yii\rest\UrlRule',
    'controller' => ['entradas'],
    'pluralize'=>false,
    'extraPatterns'=>['OPTIONS estadistica'=>'estadistica','GET estadistica'=>'estadistica']
],

En la acción podemos devolver cualquier cosa: un array, un objecto, el resultado de una SQL: (Ver https://www.yiiframework.com/doc/guide/2.0/es/db-dao

   EntradasController extends ApiController
    ...
 
      public function actionEstadistica(){
          return Yii::$app->db->createcommand('select fecha,count(*) num 
              from entradas group by fecha order by fecha desc')->queryAll();
      }

Acceso desde otros servidores. CRSF y CORS

Para evitar el problema de referencias cruzadas, CRSF, lo desactivaremos asignando a false $enableCsrfValidation en el controlador.

use Yii;
use yii\filters\Cors;
 
class UsuariosController extends \yii\rest\ActiveController {
   public $enableCsrfValidation = false;
 
   public function behaviors() {
        $behaviors = parent::behaviors();
        // Si queremos habilitar la autenticación mediante Bearer token
        $behaviors['authenticator'] = [
        'class' =>  HttpBearerAuth::className(),
        'except' => ['options','authenticate'],
        ];
 
        return $behaviors;
    }
 

Si queremos habilitar el acceso a nuestra API desde otros servidores, podemos activar CORS

   public function behaviors() {
        $behaviors = parent::behaviors();
        ...
        $behaviors['corsFilter'] = [
        'class' => Cors::className(),
        'cors' => [
            'Origin' => ['*'],
            'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
            'Access-Control-Request-Headers' => ['*'],
            'Access-Control-Allow-Credentials' => $this->authenable,
        ],
        ];
        ...
        return $behaviors;
  }

Para no tener que repetir esta configuración en todos los controladores, podemos crear una clase base, de la que deriven todos los controladores de nuestra API (salvo SiteController) ApiController.php:

<?php 
namespace app\controllers;
 
use Yii;
use yii\filters\auth\CompositeAuth;
use yii\filters\Cors;
use yii\filters\auth\HttpBearerAuth;
use yii\filters\auth\QueryParamAuth;
 
class ApiController extends \yii\rest\ActiveController {
       public $enableCsrfValidation= false; 
       public $authenable=true;
       public $authexcept=[]; //Acciones que No llevan autenticación 
 
       public function beforeAction($a){
                header('Access-Control-Allow-Origin: *');
                return parent::beforeAction($a);
       }
 
       public function behaviors() {
                $behaviors = parent::behaviors();
                unset($behaviors['authenticator']);
                $behaviors['corsFilter'] = [
                        'class' => Cors::className(),
                        'cors' => [
                                'Origin' => ['*'],
                                'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
                                'Access-Control-Request-Headers' => ['*'],
                                'Access-Control-Allow-Credentials' => !in_array($this->action->id,$this->authexcept),
                               'Access-Control-Max-Age' => 86400
                        ],
                ];
 
                if (!$this->authenable)
                        return $behaviors;
                $behaviors['authenticator'] = [
                        'class' => HttpBearerAuth::className(),
                        'except' => array_merge(['options','authenticate'],$this->authexcept),
                ];
 
                return $behaviors;
        }
}

y así, creamos el resto de controladores derivando de éste:

<?php
 
namespace app\controllers;
use Yii;
use app\models\Noticias;
 
class NoticiasController extends ApiController
{
	public $modelClass = 'app\models\Noticias';
  ...
 
}

Si en algún controlador necesitamos desactivar la autenticación del Bearer, pero mantener el chequeo de CORS, asignaremos la variable $authenable a false:

<?php
 
namespace app\controllers;
use Yii;
use app\models\Noticias;
 
class AuthController extends ApiController
{
	public $authenable=false;  // En autenticación no chequea el Bearer
  ...
 
}

Y si hay acciones que se han de autenticar y otras no, las definimos en la propiedad $authexcept:

<?php
 
namespace app\controllers;
use Yii;
use app\models\Noticias;
 
class AuthController extends ApiController
{
	public $authexcept['index','view'];  // Index y view no se autentifican con Bearer token
  ...
 
}