Wednesday, November 5, 2014

Laravel 4, Token Based Restful Authentication

SPA болон Mobile device-уудтай харилцах Rest нөөцөд хамгаалалт хийхэд basic auth-аас илүүтэйгээр token-д суурилсан хамгаалалт нь илүү зохимжтой байдаг. Энэ постоор laravel 4 дээр үүнийг хэрхэн хэрэгжүүлэхийг харуулъя.

API-аа эхлээд тодорхойлъё :

хэрэглэгч шинээр бүртгүүлэхэд
    POST /api/v1/users/
payload :
    user: {
        username: 'хэрэглэгчийн нэр',
        email: 'и-мэйл хаяг',
        password: 'нууц үг'
    }

хэрэглэгч системд нэвтрэхэд
    POST /api/v1/auth
payload :
    {
        email: 'и-мэйл хаяг',
        password: 'нэвтрэх нууц үг'
    }
хүсэлтийн хариуд хэрэглэгчийн мэдээлэл шинээр үүссэн token утгын хамт буцна.

хэрэглэгч системээс гарахад
    DELETE /api/v1/auth
header :
    X-Auth-Token : 'токен утга'

хамгаалагдсан нөөцрүү хандахад
    GET, POST, UPDATE /api/v1/protected
header :
    X-Auth-Token : 'токен утга'

Хэрэгжүүлэлт

'users' хүснэгт үүсгэх

>php artisan migrate:make create_users_table


<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration {
 /**
  * Run the migrations.
  *
  * @return void
  */
 public function up()
 {
  Schema::create('users', function($t) {
   $t->increments('id');
   $t->string('email', 100)->unique();
   $t->string('username', 50);
   $t->string('password', 50);
   $t->string('remember_token',100)->nullable();
   $t->timestamps();
  });
 }
 /**
  * Reverse the migrations.
  *
  * @return void
  */
 public function down()
 {
  Schema::drop('users');
 }
}

>php artisan migrate


User.php модел үүсгэх

<?php

use Illuminate\Auth\UserTrait;
use Illuminate\Auth\UserInterface;
use Illuminate\Auth\Reminders\RemindableTrait;
use Illuminate\Auth\Reminders\RemindableInterface;

class User extends Eloquent implements UserInterface, RemindableInterface {

 use UserTrait, RemindableTrait;

 /**
  * The database table used by the model.
  *
  * @var string
  */
 protected $table = 'users';

 /**
  * The attributes excluded from the model's JSON form.
  *
  * @var array
  */
 protected $hidden   = ['password'];
 protected $fillable = ['username', 'email', 'password', 'remember_token'];
}



Token-д суурилсан нэвтрэх, гарах, шалгах үйлдлийг хариуцсан service үүсгэх

үүний тул эхлээд facades хавтас үүсгэх хэрэгтэй
хавтас дотроо дээрхи гурван функцүүдээ агуулсан class эхлээд үүсгэнэ.

нэвтрэх функцд email болон нууц үг параметр болгон аваад users хүснэгтээс хайгаад олдвол шинээр 40 тэмдэгтийн урттай random token утга үүсгэж хадгалаад буцаана.

гарах функцд token утгаар нь users-ээс хайж олоод token хадгалах талбарт нь хоосон утга оноогоод буцаана

нэвтэрсэн эсэхийг шалгах функцийн хувьд token-ээр хайгаад олдвол true олдогүй бол false утга буцаана. хайх хэсэгт нь remember(1) гэж функц дуудалт хийснээр cache-лэлт хийгдэнэ. тодорхой хэмжээнд хурдсах байх.

<?php 

class TokenAuthClass
{
    public function login($email, $password) {
        $user = User::where('email', '=', $email)
                 ->where('password', '=', md5($password))
                 ->first();
        if ($user) {
            $user->remember_token = str_random(40);
            $user->save();
            return $user;
        }
        return null;
    }
    public function logout($token) {
        if ($token) {
            $user = User::where('remember_token','=',$token)->first();
            if ($user) {
             $user->remember_token = "";
             $user->save();
              return $user;
            }
            return null;
        }
        return null;
    }
    public function check($token) {
        if ($token) {
            $user = User::where('remember_token','=',$token)->remember(1)->first(); // cached
            return $user?true:false;
        }
        return false;
    }
}


Энэ классаа singleton service болгож ашиглахын тулд эхлээд TokenAuth нэртэй Facade-аас удамшсан класс үүсгэх хэрэгтэй


<?php

use Illuminate\Support\Facades\Facade;

class TokenAuth extends Facade 
{
 protected static function getFacadeAccessor() 
 {
  return 'TokenAuthAlias'; // singleton
 }
}


Дараа нь service provider-т бүртгүүлэх хэрэгтэй

<?php

use Illuminate\Support\ServiceProvider;

class TokenAuthServiceProvider extends ServiceProvider
{
 public function register() 
 {
  App::bind('TokenAuthAlias', function() {
   return new TokenAuthClass;
  });
 }
}


config/app.php дотор service-ээ бүртгүүлнэ.

 'providers' => array(
  ...
  'Illuminate\Workbench\WorkbenchServiceProvider',
  'TokenAuthServiceProvider',
 ),


facades хавтас дотор үүсгэсэн энэ гурван кодоо laravel-ийн class loader-т мөн бүртгүүлэх хэрэгтэй. Үүний тулд composer.json файлыг нээгээд facades хавтасаа зааж өгнө

 "autoload": {
  "classmap": [
   "app/commands",
   "app/controllers",
   "app/models",
   "app/database/migrations",
   "app/database/seeds",
   "app/tests/TestCase.php",
   "app/facades"
  ]
 },


>composer dump-autoload
командыг проект дотроо ажиллуулна.

Ингээд дурын газраасаа TokenAuth::login(), TokenAuth::check(), TokenAuth::logout() гэх мэтээр дуудан ажиллуулах боломжтой боллоо. Ингэж service үүсгэж ашигласнаар controller дотор хэт их үргэлжилсэн бизнес логик хийгдэхээс сэргийлж кодын бүтцийг зориулалтаар нь цэгцэлж ашиглах боломжтой болгоно.


Хэрэглэгчээр нэвтрэх үйлдлийг хэрэгжүүлэх

үүний тулд routes.php дээрээ route-үүдээ зааж өгнө. login хийхэд api/v1/auth хаягруу POST хүсэлт явуулна харин logout хийхэд энэ хаягруу DELETE хүсэлт явуулна.

Route::group(['prefix'=>'api/v1'], function() {
    Route::resource('auth', 'AuthController', ['only'=>['store']]);
    Route::delete('auth', ['uses' => 'AuthController@destroy']);
});


AuthController.php файл үүсгээд нэвтрэх гарах функцүүдээ хэрэгжүүлье.

<?php

class AuthController extends BaseController {
    public function store() {
        $user = TokenAuth::login(Input::get('email'), Input::get('password'));
        if ($user) {
            return Response::json(["user"=>$user]);
        }
        return Response::make("Cannot login", 401);
    }
    public function destroy() {
        $user = TokenAuth::logout(Request::header('X-Auth-Token'));
        if ($user) {
            return Response::make("Successfully logged out", 401);
        }
        return Response::make("Not logged in", 401);
    }

}

Одоо нэвтэрсний дараа token утга авах боломжтой боллоо. Үүнийгээ client-ийнхээ header-ийн X-Auth-Token талбарт нь оноогоод ашиглахад protected нөөцрүү хандах боломжтой болно.
Үүнийг хэрэгжүүлэхийн тулд 'auth.token' гэсэн laravel-ийн filter үүсгэе. filters.php файлыг нээгээд дараах байдлаар хэрэгжүүлнэ.

Route::filter('auth.token', function() 
{
 if (!TokenAuth::check(Request::header('X-Auth-Token'))) {
  return Response::make("please login", 401);
 }
});


За ингээд бараг бэлэн боллоо. Харин одоо rest нөөцүүддээ хамгаалалт хийхээр болвол дараах байдлаар ашиглана.

Route::group(['prefix'=>'api/v1', 'before'=>'auth.token'], function() {
 Route::resource('users', 'UserController',
        ['only'=>['index', 'show', 'show', 'store', 'destroy']]);
});
Route::get('/authtest', ['before' => 'auth.token', function() {
 return Response::make('Auth works');
}]);


mobile төхөөрөмжүүдээс холбогдож ашигладаг болгохын тулд filters.php файл дээр доорхи кодыг нэмж өгөөрэй.

App::before(function($request)
{
    header('Access-Control-Allow-Origin: *');
    header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
    header('Access-Control-Allow-Headers: Origin, Content-Type, Accept, Authorization, X-Request-With, X-Auth-Token');
    header('Access-Control-Allow-Credentials: true');
});