Laravel(5.1)&Emberに基づく.js(1.13.0)のユーザー認証システム


Laravel自体は完全なユーザー認証ソリューションを提供しており、PHPによって駆動されるマルチページアプリケーションでは、Laravelはユーザー認証の問題を完璧に解決することができます.しかしSPAではlaravelはAPI serverに退化し、ページルーティングとフォームコミットはフロントエンドフレームワークによって完全に制御され、この場合2つの問題に直面する.
  • フロントエンドでページアクセス権制御をどのように実現しますか?
  • ajaxリクエストをどのように許可しますか?

  • フロントエンドでページアクセス制御を実現するにはどうすればいいですか?
    Ember.js 1.13.0 authentication機能は提供されていません.ember-simple-authというサードパーティ拡張子を使用しました.これはGithubのホームページです.
    https://github.com/simplabs/ember-simple-auth
    まず、ember-cliプロジェクトのルートディレクトリに拡張子をインストールします.
    ember install ember-cli-simple-auth

    そしてember/config/environment.jsファイルで構成します.具体的な構成オプションはドキュメントに詳しく説明されています.私の構成は以下の通りです.
    // ember/config/environment.js
    
    ENV['simple-auth'] = {
        authorizer: 'authorizer:custom'    //              
    };

    Emer-simple-authは、routeがmixinを継承している限り、事前に定義されたいくつかの動作または機能を得る一連のmixinクラスを定義します.例えば、私のember/app/routes/application.jsの内容は以下の通りです.
    // ember/app/routes/application.js 
    
    import ApplicationRouteMixin from 'simple-auth/mixins/application-route-mixin';
    
    export default Ember.Route.extend(ApplicationRouteMixin, {
        actions: {
            invalidateSession: function() {
                this.get('session').invalidate();
            }
        }
    });

    アプリケーション-route-mixinは、一連のactionsメソッドを事前に定義しています.セッション上のイベントがトリガーされると、対応するactionがイベントを処理するために呼び出されます.ember/app/routes/applicationでもいいです.js独自のactionでこれらのメソッドを上書きします(ember-simple-authは、フロントエンドで生成されたすべての認可情報を保存するローカルlocalStorageでセッションオブジェクトを維持します).
    次に、authenticated-route-mixinを許可されたユーザーのみがアクセスできるページルーティングに追加します.
    // ember/app/routes/user.js 
    
    import AuthenticatedRouteMixin from 'simple-auth/mixins/authenticated-route-mixin';
    
    export default Ember.Route.extend(AuthenticatedRouteMixin,{
        model: function(params) {
            return this.store.find('user',params.user_id);
        }
    });

    authenticated-route-mixinは、許可されたユーザーのみが/userにアクセスできることを保証します.権限がない場合は、デフォルトで/loginにリダイレクトします.だからember/app/routes/login.jsにunauthenticated-route-mixinを追加する必要があります.
    // ember/app/routes/login.js 
    
    import UnauthenticatedRouteMixin from 'simple-auth/mixins/unauthenticated-route-mixin';
    
    export default Ember.Route.extend(UnauthenticatedRouteMixin);

    unauthenticated-route-mixinは、/loginにとって、パスが許可されずにアクセスできることを保証します.
    ajaxリクエストを許可するにはどうすればいいですか?
    カスタムauthenticator:ember/app/authenticators/custom.js
    // ember/app/authenticators/custom.js
    
    import Base from 'simple-auth/authenticators/base';
    
    export default Base.extend({
    
        /**
         * Check auth state of frontend
         *
         * @param data (  session     )
         * @returns {ES6Promise.Promise}
         */
        restore: function(data) {
            return new Ember.RSVP.Promise(function(resolve, reject)
            {
                if ( data.is_login ){
                    resolve(data);
                }
                else{
                    reject();
                }
            });
        },
    
        /**
         * Permission to login by frontend
         *
         * @param obj credentials
         * @returns {ES6Promise.Promise}
         */
        authenticate: function(credentials) {
    
            var authUrl = credentials.isLogin ? '/auth/login' : '/auth/register'
    
            return new Ember.RSVP.Promise(function(resolve, reject) {
    
                Ember.$.ajax({
    
                    url:  authUrl,
                    type: 'POST',
                    data: { email: credentials.identification, password: credentials.password }
    
                }).then(function(response) {
    
                    if(response.login === 'success'){
                        resolve({ is_login : true });
                    }
    
                }, function(xhr, status, error) {
    
                    reject(xhr.responseText);
    
                });
            });
        },
    
        /**
         * Permission to logout by frontend
         *
         * @returns {ES6Promise.Promise}
         */
        invalidate: function() {
    
            return new Ember.RSVP.Promise(function(resolve) {
    
                Ember.$.ajax({
    
                    url: '/auth/logout',
                    type: 'GET'
    
                }).then(function(response) {
    
                    if(response.logout === 'success'){
                        resolve();
                    }
                });
            });
        }
    });

    restore,authenticate,invalidateの3つの関数はそれぞれ授権を取得し,授権を行い,授権を取り消すために用いられる.
    カスタムauthorizer:ember/app/authorizers/custom.js
    // ember/app/authorizers/custom.js
    
    import Base from 'simple-auth/authorizers/base';
    
    export default Base.extend({
    
        authorize: function(jqXHR, requestOptions)
        {
            var _this = this;
    
            Ember.$.ajaxSetup({
    
                headers:
                {
                    'X-XSRF-TOKEN': Ember.$.cookie('XSRF-TOKEN')    //       
                },
    
                complete : function(response, state)
                {
                    //           
                    if(response.status===403 && _this.get('session').isAuthenticated)  
                    {
                        _this.get('session').invalidate();
                    }
                }
            });
        }
    });

    authorize関数は2つのことをしました.
  • ajaxリクエストごとに'X-XSRF-TOKEN'header
  • を追加
  • サーバが返す認証状態をチェックし、
  • を処理する.
    具体的には、
    ヘッダーの内容はlaravelが設定した'XSRF-TOKEN'cookieの値であり、laravelは各リクエストからheader('X-XSRF-TOKEN')を読み取り、tokenの値が合法かどうかを検証し、検証に合格した場合、laravel/app/Http/Middleware/VerifyCsrfToken.phpで実現される安全なリクエストであると考えられる.
    次に、laravelにミドルウェアを新規作成し、VerifyAuthと名前を付けます.
    <?php
    
    // laravel/app/Http/Middleware/VerifyAuth.php
    
    namespace App\Http\Middleware;
    
    use Closure;
    use Illuminate\Contracts\Auth\Guard;
    
    class VerifyAuth
    {
        protected $include = ['api/*'];    //          URL
    
        protected $auth;
    
        public function __construct(Guard $auth)
        {
            $this->auth = $auth;
        }
    
        /**
         * Handle an incoming request.
         *
         * @param  \Illuminate\Http\Request  $request
         * @param  \Closure  $next
         * @abort  403
         * @return  mixed
         */
        public function handle($request, Closure $next)
        {
            if( $this->shouldPassThrough($request) || $this->auth->check() )
            {
                return $next($request);
            }
    
            abort(403, 'Unauthorized action.');     //    ,        
        }
    
    
        /**
         * Determine if the request has a URI that should pass through auth verification.
         *
         * @param  \Illuminate\Http\Request  $request
         * @return bool
         */
        protected function shouldPassThrough($request)
        {
            foreach ($this->include as $include) {
                if ($request->is($include)) {
                    return false;
                }
            }
    
            return true;
        }
    }

    AUTH要求は権限に対する操作であり、それ以外の要求は無効な要求としてフロントエンドに再ルーティングされたり、エラーが投げ出されたりするため、API要求に対してのみ権限検証を行います.要求が許可されていない場合、サーバは403エラーを投げ出して、フロントエンドにユーザログインまたは登録が必要であることを通知する.
    最後にlaravelappHttpControllersAuthAuthController.phpでは、すべての認証ロジックを実現します.
    <?php
    
    namespace App\Http\Controllers\Auth;
    
    use App\User;
    use Validator;
    use Response;
    use Auth;
    use Illuminate\Http\Request;
    use App\Http\Controllers\Controller;
    use Illuminate\Foundation\Auth\ThrottlesLogins;
    use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
    
    class AuthController extends Controller
    {
        use AuthenticatesAndRegistersUsers, ThrottlesLogins;
    
        protected $remember = true;    //             
    
        public function __construct()
        {
            $this->middleware('guest', ['except' => 'getLogout']);
        }
    
        public function postLogin(Request $credentials)    //   
        {
            return $this->logUserIn($credentials);
        }
    
    
        public function getLogout()    //   
        {
            Auth::logout();
            return Response::json(['logout'=>'success']);
        }
    
    
        public function postRegister(Request $credentials)    //         
        {
            $newUser = new User;
        
            $newUser->email = $credentials['email'];
            $newUser->password = bcrypt($credentials['password']);
        
            $newUser->save();
        
            return $this->logUserIn($credentials);
        }
        
        
        protected function logUserIn(Request $credentials)    //       
        {
            $loginData = ['email' => $credentials['email'], 'password' => $credentials['password']];
        
            if ( Auth::attempt($loginData, $this->remember) )
            {
                return Response::json(['login'=>'success']);
            }
            else
            {
                return Response::json(['login'=>'failed']);
            }
        }
    }

    まとめ
    ページアクセス権を設定すると、未許可のユーザーが自分のページに属していないことを防止できますが、総じてフロントエンドはユーザーに完全に露出しているため、ユーザーの権限状態はサーバによって維持される必要があります.フロントエンドは、ajaxリクエストごとにドメイン間攻撃を防止するtokenを追加する一方で、各リクエストが戻った後にhttp status codeが403権限エラーであるかどうかを確認し、そうであればログインページにリダイレクトしてユーザーに権限を取得するように要求する.