YII2 REST

YII2 Rest — авторизация

Не так давно я начал писать простенькое SPA приложение на ангуляре 2 и YII2, и вроде бы с рестом на юии все просто и понятно, но почему то авторизация у меня никак не получалась… Основной проблемой было то что я не мог достучаться к методу апи для автроризации…Пробовал я и правила урл менеджера менять и c фильтром vierbFilter игрался но что никак не удавалось…Еще  все осложнялось тем, что необходимо делать кроссдоменный запрос т. к. фронт у меня пока что  на локалке, а бек уже на сервере… В общем ниже я приведу пример авторизации на YII2 REST API с использованием CORS.

 

Структура БД

 

Для начала немного о том как все будет работать — с фронта мы передаем логин и пароль методом POST, вроде как не очень то и безопасно, но лучше я пока что не умею… Далее мы проверяем существует ли юзер с таким логином и проверяем верность введенного пароля…Если все верно выдаем юзеру токен, с помощью которого будем его далее идентифицировать.

В БД у нас будет таблица с токенами, в ней у нас будет хранится токены, время когда токен станет неактивным и id юзера за которым токен закреплен.

 

bd_structure

В общем на картинке представлена структура БД. Повторите ее и дальше следуйте в gii ( domain.your/api/web/index.php?r=gii) . Хочу уточнить что я использую YII2 advanced версию приложения, в которой переименовал директорию frontend в api.  В gii сгенерируйте модель для таблицы токена, у меня она называется Token (common/models/token), модель я генерировал c ActiveQuery. Далее создайте контроллер LoginController в папке api/controllers. Ниже я приведу код модели и код контроллера.

Модель Token

 

<?php
namespace common\models;
use Yii;
/**
* This is the model class for table "tokens".
*
* @property integer $id
* @property integer $user_id
* @property string $token
* @property string $expire_time
*
* @property User $id0
*/
class Token extends \yii\db\ActiveRecord
{
/**
* @inheritdoc
*/
public static function tableName()
{
return 'tokens';
}
/**
* @inheritdoc
*/
public function rules()
{
return [
[['user_id', 'token'], 'required'],
[['user_id'], 'integer'],
[['expire_time'], 'safe'],
[['token'], 'string', 'max' => 255],
[['id'], 'exist', 'skipOnError' => true, 'targetClass' => User::className(), 'targetAttribute' => ['id' => 'id']],
];
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'user_id' => 'User ID',
'token' => 'Token',
'expire_time' => 'Time',
];
}
/**
* @return \yii\db\ActiveQuery
*/
public function getId0()
{
return $this->hasOne(User::className(), ['id' => 'id']);
}
/**
* @inheritdoc
* @return TokenQuery the active query used by this AR class.
*/
public static function find()
{
return new TokenQuery(get_called_class());
}
}

 

LoginController

 

<?php
/**
* Created by PhpStorm.
* User: Андрей
* Date: 05.09.2017
* Time: 19:24
*/
namespace api\controllers;
use Yii;
use yii\web\Controller;
use yii\filters\VerbFilter;
use yii\filters\AccessControl;
use yii\filters\ContentNegotiator;
use yii\web\Response;
use common\models\User;
use common\models\Token;
class LoginController extends Controller
{
public function behaviors()
{
return [
'corsFilter' => [
'class' => \yii\filters\Cors::className(),
'cors' => [
'Origin' => ['*'],
'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
'Access-Control-Request-Headers' => ['*'],
],
],
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'index'=>['post', 'options'],
]
],
'access' => [
'class' => AccessControl::className(),
'rules' => [
[
'allow' => true,
'actions' => [ 'index'],
'roles' => ['?'],
],
],
],
'contentNegotiator' => [
'class' => ContentNegotiator::className(),
'formats' => [
'application/json' => Response::FORMAT_JSON,
],
],
];
}
public function beforeAction($action)
{
if (in_array($action->id, ['index'])) {
$this->enableCsrfValidation = false;
}
return parent::beforeAction($action);
}
/* отключаем цсрф чтобы не иметь 400 ошибки при запросе*/
public function actionIndex()
{
$this->enableCsrfValidation = false;
$params = Yii::$app->request->getBodyParams();
$user = User::findByEmail(Yii::$app->request->getBodyParam('login'));
if (!$user) {
$user = User::findByUsername(Yii::$app->request->getBodyParam('login'));
if (!$user)
$result =  [
'success' => 0,
'message' => 'No such user found'
];
}
if($user)
if (!$user->validatePassword(Yii::$app->request->getBodyParam('password'))) {
$result =   [
'success' => 0,
'message' => 'Incorrect password'
];
}
else{
$token = new Token();
$token->token = Yii::$app->getSecurity()->generateRandomString(12);
$token->user_id = $user->id;
$token->expire_time = time() + 3600 * 5;
$token->save();
Token::deleteAll('expire_time < ' . time());
$result =   [
'success' => 1,
'username' =>  $user->username,
'payload' => $user,
'token' => $token->token
];
}
echo json_encode($result);
}
}

 

Теперь  добавим связь модели User, она нам еще пригодится

public  function getToken()
{
return $this->hasMany(Token::className(), ['user_id'=>'id']);
}

А так же в модель User добавим методы, благодаря 1 методу мы будем удалять устаревшие токены, а  второй метод будет использован для авторизации с помощью email

public static function findIdentityByAccessToken($token, $type = null)
{
Token::deleteAll('time < ' . time());
return static::find()->with('token')->one();
}
public static function findByEmail($email)
{
return static::findOne(['email' => $email, 'status' => self::STATUS_ACTIVE]);
}

Дальше очень важный момент с настройкой конфига REST…Ниже приведен файл common/config/.main.php

<?php
$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' => 'app-api',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log'],
'controllerNamespace' => 'api\controllers',
'components' => [
'request' => [
'csrfParam' => '_csrf-api',
],
'user' => [
'identityClass' => 'common\models\User',
'enableAutoLogin' => false,
'enableSession' => false,
],
'session' => [
'name' => 'advanced-api',
],
'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0,
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning'],
],
],
],
'errorHandler' => [
'errorAction' => 'site/error',
],
'urlManager' => [
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
['class' => 'yii\rest\UrlRule', 'controller' => ['user']],
'POST logins' => 'login/index',
],
],
'request' => [
'parsers' => [
'application/json' => 'yii\web\JsonParser',
]
],
],
'params' => $params,
];

 

В этом файле особое внимание нужно обратить на urlManager и его элемент rules. На данный момент у меня 2 yii2 rest контроллера, для них необходимо указать правила. В классическом варианте можно просто написать 'rules' => [ ['class' => 'yii\rest\UrlRule', 'controller' => ['user', 'login']], ], Но тогда при POST запросе к контроллеру login мы будем обращаться к методу Create….А нам то нужен Index ( ну или как вы его там у себя назвали ). Для этого немного изменим наше правило урл менеджера 'rules' => [ ['class' => 'yii\rest\UrlRule', 'controller' => ['user']], 'POST logins' => 'login/index', ], Теперь вроде порядок и осталось только настроить CORS

 

Кроссдоменные запросы к YII2 REST

 

Сначала немного о кроссдоменных запросах, а потом о CORS…Если мы шлем AJAX

запрос к чужому серверу, то браузер добавит специальные заголовки с информацией о домене с которого идет запрос…Сервер в свою очередь на основании этих заголовков решит обрабатывать как обработать этот запрос и добавит свои особые заголовки..Как то так…Более подробно о CORS

Yii2 предоставлят нам удобный способ использования CORS путем добавления фильтра corsFilter в поведение контроллера или модуля. ( corsFilter в YII2 ) У меня это выглядит так (api/controllers/LoginController.php)

'corsFilter' => [
'class' => \yii\filters\Cors::className(),
'cors' => [
'Origin' => ['*'],
'Access-Control-Request-Method' => [ 'POST', 'OPTIONS'],
'Access-Control-Request-Headers' => ['*'],
],
],

Options посылается в случае если у нас сложый корс…я буду использовать простой ( заголовок аякса Content-Type: application/x-www-form-urlencoded ) Поэтому Options мне в принципе не нужен…И кроссдоменные аякс запросы в YII2 мне нужны только на стадии разработки…так то у меня все будет на одном домене…В таком случае заголовки корс можно добавить в .htaccess или добавить в index.php след код

if (isset($_SERVER['HTTP_ORIGIN'])) {
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');    // cache for 1 day
}
// Access-Control headers are received during OPTIONS requests
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']))
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']))
header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");
}

Этот код точно сработает и добавит нужные вам заголовки…Обратите внимание что я разрешаю доступ всем доменам…

И вот еще мой  файл api/web/.htaccess

Options +FollowSymLinks
IndexIgnore */*
RewriteEngine on
# if a directory or a file exists, use it directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# otherwise forward it to index.php
RewriteRule . index.php

Проделав все эти манипуляции мы можем передавать пост запрос к logins для авторизации с использованием YII2 REST.
Адрес запроса — http://domain.your/api/web/logins

 

Авторизация со стороны клиента

 

На клиенте для авторизации с  YII2 REST я использую ANGULAR. Я создал сервис и ниже привожу его код.

import {Injectable} from '@angular/core';
import {Http} from '@angular/http';
import {Response, Headers, URLSearchParams} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import {User} from '../user';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
@Injectable()
export class UserHttpService{
public baseUrl = 'http://cvbuilder.web-andryshka.ru/api/web/';
public user: User = new User();
constructor(private http: Http){ }
setUser(userData:any){
this.user.username = userData.username;
this.user.token = userData.token;
}
getUsername(){
return this.user.username ;
}
getToken(){
return this.user.token ;
}
login(login:string, password:string){
let headers = new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' });
var params = new URLSearchParams();
params.set('login', login);
params.set('password', password);
var result = this.http.post(this.baseUrl + 'logins', params.toString(), { headers: headers })
.map(res => res.json())
.catch((error:any) =>{  return Observable.throw(error);});
return result;
}
}

Таким образом мы реализовали простую авторизацию на Yii2. И при отправке соответствующего логина и пароля мы в ответ получим токен…который  будем передавать с каждым запросом требующим уатентификации в заголовке bearer (я буду использовать bearer авторизацию в yii2 )

YII2 REST Bearer авторизация
YII2 REST Bearer авторизация

 


One thought on “YII2 Rest — авторизация

Добавить комментарий